jashliao 用 VC++ 實現 fanfuhan OpenCV 教學042 ~ opencv-042-二值化圖像OTSU演算法(THRESH_OTSU)

jashliao 用 VC++ 實現 fanfuhan OpenCV 教學042 ~ opencv-042-二值化圖像OTSU演算法(THRESH_OTSU)

jashliao 用 VC++ 實現 fanfuhan OpenCV 教學042 ~ opencv-042-二值化圖像OTSU演算法(THRESH_OTSU)


資料來源: https://fanfuhan.github.io/

https://fanfuhan.github.io/2019/04/13/opencv-042/


GITHUB:https://github.com/jash-git/fanfuhan_ML_OpenCV

https://github.com/jash-git/jashliao-implements-FANFUHAN-OPENCV-with-VC

★前言:

★主題:
    otsu 大津算法介紹:
        OTSU算法是由日本學者OTSU於1979年提出的一種對圖像進行二值化的高效算法。
        利用閾值將原圖像分成前景,背景兩個圖象。
        前景:用n1,csum,m1來表示在當前閾值下的前景的點數,質量矩,平均灰度
        背景:用n2, sum-csum,m2來表示在當前閾值下的背景的點數,質量矩,平均灰度
        當取最佳閾值時,背景應該與前景差別最大,關鍵在於如何選擇衡量差別的標准,而在otsu算法中這個衡量差別的標准就是最大類間方差,在本程序中類間方差用sb表示,最大類間方差用fmax

    otsu 大津算法原理
        otsu 大津算法是一種圖像二值化算法,作用是確定將圖像分成黑白兩個部分的閾值。
        將圖像背景和前景分成黑白兩類很好理解,但是如何確定背景和前景的二值化界限(閾值)呢?
        對於不同的圖像,這個閾值可能不同,這就需要有一種算法來根據圖像的信息自適應地確定這個閾值。

        首先,需要將圖像轉換成灰度圖像,255個灰度等級。
        可以將圖像理解成255個圖層,每一層分布了不同的像素,這些像素垂直疊加合成了一張完整的灰度圖。

        我們的目的就是找到一個合適的灰度值,大於這個值的我們將它稱之為背景(灰度值越大像素越黑),小於這個值的我們將它稱之為前景(灰度值越小像素越白)。

        怎么確定這個值就是我們想要的值呢?
        這里引入方差的概念,方差越大,相關性越低,黑白越分明。
        我們將每一個灰度值之上下之間的像素的方差求出來不就行了嗎?找到方差最大的那個灰度值,那個就是我們想要的二值化分隔閾值。

        先定義几個符號代表的意義:
        h:圖像的寬度
        w:圖像的高度(h*w 得到圖像的像素數量)

        t :灰度閾值(我們要求的值,大於這個值的像素我們將它的灰度設置為255,小於的設置為0)
        n0:小於閾值的像素,前景
        n1:大於等於閾值的像素,背景
        n0 + n1 == h * w

        w0:前景像素數量占總像素數量的比例
        w0 = n0 / (h * w)

        w1:背景像素數量占總像素數量的比例
        w1 = n1 / (h * w)
        w0 + w1 == 1

        u0:前景平均灰度
        u0 = n0灰度累加和 / n0
        u1:背景平均灰度
        u1 = n1灰度累加和 / n1

        u:平均灰度
        u = (n0灰度累加和 + n1灰度累加和) / (h * w) 根據上面的關系
        u = w0 * u0 + w1 * u1

        g:類間方差(那個灰度的g最大,哪個灰度就是需要的閾值t)
        g = w0 * (u0 – u)^2 + w1 * (u1 – u)^2
        根據上面的關系,可以推出:(這個一步一步推導就可以得到)
        g = w0 * w1 * (u0 – u1) ^ 2

        然后,遍曆每一個灰度值,找到這個灰度值對應的 g
        找到最大的 g 對應的 t

C++

// VC_FANFUHAN_OPENCV042.cpp : 定義主控台應用程式的進入點。
//
/*
// Debug | x32
通用屬性
| C/C++
|	| 一般
|		| 其他 Include 目錄 -> C:\opencv\build\include
|
| 連結器
| 	|一一般
|		|  其他程式庫目錄 -> C:\opencv\build\x64\vc15\lib
|
| 	|一輸入
|		| 其他相依性 -> opencv_world411d.lib;%(AdditionalDependencies)
// Releas | x64
組態屬性
| C/C++
|	| 一般
|		| 其他 Include 目錄 -> C:\opencv\build\include
|
| 連結器
| 	|一般
|		| 其他程式庫目錄 -> C:\opencv\build\x64\vc15\lib
|
| 	|一輸入
|		| 其他相依性 -> opencv_world411.lib;%(AdditionalDependencies)
*/

#include "stdafx.h"
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>

using namespace std;
using namespace cv;

void blur_demo(Mat &image, Mat &sum);
void edge_demo(Mat &image, Mat &sum);
int getblockSum(Mat &sum, int x1, int y1, int x2, int y2, int i);

void showHistogram(InputArray src, cv::String StrTitle);
void backProjection_demo(Mat &mat, Mat &model);
void blur3x3(Mat &src, Mat *det);

void add_salt_pepper_noise(Mat &image);
void add_gaussian_noise(Mat &image);

void USMImage(Mat src, Mat &usm, float fltPar);

void pyramid_up(Mat &image, vector<Mat> &pyramid_images, int level);
void pyramid_down(vector<Mat> &pyramid_images);

void laplaian_demo(vector<Mat> &pyramid_images, Mat &image);

void pause()
{
	printf("Press Enter key to continue...");
	fgetc(stdin);
}
int main()
{
	Mat src = imread("../../images/master.jpg");//Mat src = imread("../../images/test.png");
	if (src.empty())
	{
		cout << "could not load image.." << endl;
		pause();
		return -1;
	}
	else
	{
		imshow("input_src", src);
		showHistogram(src, "Histogram_input_src");

		// 转为灰度图像
		Mat gray, binary;
		cvtColor(src, gray, COLOR_BGR2GRAY);
		//imshow("input_gray", gray);
		//showHistogram(gray, "Histogram_gray");

		Scalar m = mean(gray);
		int T = mean(src)[0];

		for (int i = 0; i < 5; ++i) {
			//        THRESH_BINARY = 0
			//        THRESH_BINARY_INV = 1
			//        THRESH_TRUNC = 2
			//        THRESH_TOZERO = 3
			//        THRESH_TOZERO_INV = 4
			threshold(gray, binary, 0, 255, i| THRESH_OTSU);
			imshow(format("binary_%d | THRESH_OTSU", i), binary);
		}

		waitKey(0);
		return 0;

	}

	return 0;
}

void laplaian_demo(vector<Mat> &pyramid_images, Mat &image)//拉普拉斯金字塔
{
	for (int i = pyramid_images.size() - 1; i > -1; --i)
	{
		Mat dst;
		if (i - 1 < 0)
		{
			pyrUp(pyramid_images[i], dst, image.size());
			subtract(image, dst, dst);//圖像相減
			dst = dst + Scalar(127, 127, 127); //调亮度, 实际中不能这么用
			imshow(format("laplaian_layer_%d", i), dst);
		}
		else
		{
			pyrUp(pyramid_images[i], dst, pyramid_images[i - 1].size());
			subtract(pyramid_images[i - 1], dst, dst);//圖像相減
			dst = dst + Scalar(127, 127, 127); //調亮度, 实际中不能这么用
			imshow(format("laplaian_layer_%d", i), dst);
		}
	}
}

void pyramid_down(vector<Mat> &pyramid_images)//高斯金字塔01
{
	for (int i = pyramid_images.size() - 1; i > -1; --i) {
		Mat dst;
		/*
		pyrUp(tmp, dst, Size(tmp.cols * 2, tmp.rows * 2))
		tmp: 當前影象, 初始化為原影象 src 。
		dst : 目的影象(顯示影象,為輸入影象的兩倍)
		Size(tmp.cols * 2, tmp.rows * 2) : 目的影象大小, 既然我們是向上取樣, pyrUp 期待一個兩倍於輸入影象(tmp)的大小。
		*/
		pyrUp(pyramid_images[i], dst);
		imshow(format("pyramid_down_%d", i), dst);
	}
}

void pyramid_up(Mat &image, vector<Mat> &pyramid_images, int level)//高斯金字塔02
{
	Mat temp = image.clone();
	Mat dst;
	for (int i = 0; i < level; ++i)
	{
		/*
		pyrDown( tmp, dst, Size( tmp.cols/2, tmp.rows/2 ))
		tmp: 當前影象, 初始化為原影象 src 。
		dst: 目的影象( 顯示影象,為輸入影象的一半)
		Size( tmp.cols/2, tmp.rows/2 ) :目的影象大小, 既然我們是向下取樣, pyrDown 期待一個一半於輸入影象( tmp)的大小。
		注意輸入影象的大小(在兩個方向)必須是2的冥,否則,將會顯示錯誤。
		最後,將輸入影象 tmp 更新為當前顯示影象, 這樣後續操作將作用於更新後的影象。
		tmp = dst;
		*/
		pyrDown(temp, dst);
		imshow(format("pyramid_up_%d", i), dst);
		temp = dst.clone();
		pyramid_images.push_back(temp);
	}
}

void USMImage(Mat src, Mat &usm, float fltPar)//圖像銳化增强演算法(USM)
{
	Mat blur_img;
	/*
	USM銳化公式表示如下:
	(源圖像– w*高斯模糊)/(1-w);其中w表示權重(0.1~0.9),默認為0.6
	OpenCV中的代碼實現步驟
	– 高斯模糊
	– 權重疊加
	– 輸出結果
	*/
	GaussianBlur(src, blur_img, Size(0, 0), 25);
	addWeighted(src, (1 + fltPar), blur_img, (fltPar*-1), 0, usm);//原圖 : 模糊圖片= 1.5 : -0.5 的比例進行混合
	imshow("usm", usm);
	showHistogram(usm, "Histogram_input_usm");
}

void blur_demo(Mat &image, Mat &sum)
{
	int w = image.cols;
	int h = image.rows;
	Mat result = Mat::zeros(image.size(), image.type());
	int x2 = 0, y2 = 0;
	int x1 = 0, y1 = 0;
	int ksize = 5;
	int radius = ksize / 2;
	int ch = image.channels();
	int cx = 0, cy = 0;
	for (int row = 0; row < h + radius; row++) {
		y2 = (row + 1)>h ? h : (row + 1);
		y1 = (row - ksize) < 0 ? 0 : (row - ksize);
		for (int col = 0; col < w + radius; col++) {
			x2 = (col + 1)>w ? w : (col + 1);
			x1 = (col - ksize) < 0 ? 0 : (col - ksize);
			cx = (col - radius) < 0 ? 0 : col - radius;
			cy = (row - radius) < 0 ? 0 : row - radius;
			int num = (x2 - x1)*(y2 - y1);
			for (int i = 0; i < ch; i++) {
				// 积分图查找和表,计算卷积
				int s = getblockSum(sum, x1, y1, x2, y2, i);
				result.at<Vec3b>(cy, cx)[i] = saturate_cast<uchar>(s / num);
			}
		}
	}
	imshow("blur_demo", result);
}

/**
* 3x3 sobel 垂直边缘检测演示
*/
void edge_demo(Mat &image, Mat &sum)
{
	int w = image.cols;
	int h = image.rows;
	Mat result = Mat::zeros(image.size(), CV_32SC3);
	int x2 = 0, y2 = 0;
	int x1 = 0, y1 = 0;
	int ksize = 3; // 算子大小,可以修改,越大边缘效应越明显
	int radius = ksize / 2;
	int ch = image.channels();
	int cx = 0, cy = 0;
	for (int row = 0; row < h + radius; row++) {
		y2 = (row + 1)>h ? h : (row + 1);
		y1 = (row - ksize) < 0 ? 0 : (row - ksize);
		for (int col = 0; col < w + radius; col++) {
			x2 = (col + 1)>w ? w : (col + 1);
			x1 = (col - ksize) < 0 ? 0 : (col - ksize);
			cx = (col - radius) < 0 ? 0 : col - radius;
			cy = (row - radius) < 0 ? 0 : row - radius;
			int num = (x2 - x1)*(y2 - y1);
			for (int i = 0; i < ch; i++) {
				// 积分图查找和表,计算卷积
				int s1 = getblockSum(sum, x1, y1, cx, y2, i);
				int s2 = getblockSum(sum, cx, y1, x2, y2, i);
				result.at<Vec3i>(cy, cx)[i] = saturate_cast<int>(s2 - s1);
			}
		}
	}
	Mat dst, gray;
	convertScaleAbs(result, dst);
	normalize(dst, dst, 0, 255, NORM_MINMAX);
	cvtColor(dst, gray, COLOR_BGR2GRAY);
	imshow("edge_demo", gray);
}

int getblockSum(Mat &sum, int x1, int y1, int x2, int y2, int i) {
	int tl = sum.at<Vec3i>(y1, x1)[i];
	int tr = sum.at<Vec3i>(y2, x1)[i];
	int bl = sum.at<Vec3i>(y1, x2)[i];
	int br = sum.at<Vec3i>(y2, x2)[i];
	int s = (br - bl - tr + tl);
	return s;
}

void add_gaussian_noise(Mat &image)//高斯雜訊
{
	Mat noise = Mat::zeros(image.size(), image.type());
	// 产生高斯噪声
	randn(noise, (15, 15, 15), (30, 30, 30));
	Mat dst;
	add(image, noise, dst);
	image = dst.clone();//dst.copyTo(image);//圖像複製
						//imshow("gaussian_noise", dst);
}

void add_salt_pepper_noise(Mat &image)//白雜訊
{
	// 随机数产生器
	RNG rng(12345);
	for (int i = 0; i < 1000; ++i) {
		int x = rng.uniform(0, image.rows);
		int y = rng.uniform(0, image.cols);
		if (i % 2 == 1) {
			image.at<Vec3b>(y, x) = Vec3b(255, 255, 255);
		}
		else {
			image.at<Vec3b>(y, x) = Vec3b(0, 0, 0);
		}
	}
	//imshow("saltp_epper", image);
}

void blur3x3(Mat &src, Mat *det)
{
	// 3x3 均值模糊,自定义版本实现
	for (int row = 1; row < src.rows - 1; row++) {
		for (int col = 1; col < src.cols - 1; col++) {
			Vec3b p1 = src.at<Vec3b>(row - 1, col - 1);
			Vec3b p2 = src.at<Vec3b>(row - 1, col);
			Vec3b p3 = src.at<Vec3b>(row - 1, col + 1);
			Vec3b p4 = src.at<Vec3b>(row, col - 1);
			Vec3b p5 = src.at<Vec3b>(row, col);
			Vec3b p6 = src.at<Vec3b>(row, col + 1);
			Vec3b p7 = src.at<Vec3b>(row + 1, col - 1);
			Vec3b p8 = src.at<Vec3b>(row + 1, col);
			Vec3b p9 = src.at<Vec3b>(row + 1, col + 1);

			int b = p1[0] + p2[0] + p3[0] + p4[0] + p5[0] + p6[0] + p7[0] + p8[0] + p9[0];
			int g = p1[1] + p2[1] + p3[1] + p4[1] + p5[1] + p6[1] + p7[1] + p8[1] + p9[1];
			int r = p1[2] + p2[2] + p3[2] + p4[2] + p5[2] + p6[2] + p7[2] + p8[2] + p9[2];

			det->at<Vec3b>(row, col)[0] = saturate_cast<uchar>(b / 9);
			det->at<Vec3b>(row, col)[1] = saturate_cast<uchar>(g / 9);
			det->at<Vec3b>(row, col)[2] = saturate_cast<uchar>(r / 9);
		}
	}
}

void backProjection_demo(Mat &image, Mat &model)//反向投影
{
	Mat image_hsv, model_hsv;
	cvtColor(image, image_hsv, COLOR_BGR2HSV);//彩色轉HSV
	cvtColor(model, model_hsv, COLOR_BGR2HSV);

	// 定义直方图参数与属性
	int h_bins = 32, s_bins = 32;
	int histSize[] = { h_bins, s_bins };//要切分的像素強度值範圍,預設為256。每個channel皆可指定一個範圍。例如,[32,32,32] 表示RGB三個channels皆切分為32區段
	float h_ranges[] = { 0, 180 }, s_ranges[] = { 0, 256 };
	const float* ranges[] = { h_ranges, s_ranges };
	int channels[] = { 0, 1 };

	Mat roiHist;//計算ROI的直方圖
	calcHist(&model_hsv, 1, channels, Mat(), roiHist, 2, histSize, ranges);
	normalize(roiHist, roiHist, 0, 255, NORM_MINMAX, -1, Mat());

	Mat roiproj, backproj;
	calcBackProject(&image_hsv, 1, channels, roiHist, roiproj, ranges);//使用反向投影 產生ROI(前景)的mask
	bitwise_not(roiproj, backproj);//產生背景的mask
	imshow("ROIProj", roiproj);
	imshow("BackProj", backproj);
}

void showHistogram(InputArray src, cv::String StrTitle)//直方圖
{
	bool blnGray = false;
	if (src.channels() == 1)
	{
		blnGray = true;
	}

	// 三通道/單通道 直方圖 紀錄陣列
	vector<Mat> bgr_plane;
	vector<Mat> gray_plane;

	// 定义参数变量
	const int channels[1] = { 0 };
	const int bins[1] = { 256 };
	float hranges[2] = { 0, 255 };
	const float *ranges[1] = { hranges };
	Mat b_hist, g_hist, r_hist, hist;
	// 计算三通道直方图
	/*
	void calcHist( const Mat* images, int nimages,const int* channels, InputArray mask,OutputArray hist, int dims, const int* histSize,const float** ranges, bool uniform=true, bool accumulate=false );
	1.輸入的圖像數組
	2.輸入數組的個數
	3.通道數
	4.掩碼
	5.直方圖
	6.直方圖維度
	7.直方圖每個維度的尺寸數組
	8.每一維數組的範圍
	9.直方圖是否是均勻
	10.配置階段不清零
	*/
	if (blnGray)
	{
		split(src, gray_plane);
		calcHist(&gray_plane[0], 1, 0, Mat(), hist, 1, bins, ranges);
	}
	else
	{
		split(src, bgr_plane);
		calcHist(&bgr_plane[0], 1, 0, Mat(), b_hist, 1, bins, ranges);
		calcHist(&bgr_plane[1], 1, 0, Mat(), g_hist, 1, bins, ranges);
		calcHist(&bgr_plane[2], 1, 0, Mat(), r_hist, 1, bins, ranges);
	}

	/*
	* 显示直方图
	*/
	int hist_w = 512;
	int hist_h = 400;
	int bin_w = cvRound((double)hist_w / bins[0]);
	Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);
	// 归一化直方图数据
	if (blnGray)
	{
		normalize(hist, hist, 0, histImage.rows, NORM_MINMAX, -1);
	}
	else
	{
		normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1);
		normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1);
		normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1);
	}

	// 绘制直方图曲线
	for (int i = 1; i < bins[0]; ++i)
	{
		if (blnGray)
		{
			line(histImage, Point(bin_w * (i - 1), hist_h - cvRound(hist.at<float>(i - 1))),
				Point(bin_w * (i), hist_h - cvRound(hist.at<float>(i))), Scalar(255, 255, 255),
				2, 8, 0);
		}
		else
		{
			line(histImage, Point(bin_w * (i - 1), hist_h - cvRound(b_hist.at<float>(i - 1))),
				Point(bin_w * (i), hist_h - cvRound(b_hist.at<float>(i))), Scalar(255, 0, 0),
				2, 8, 0);
			line(histImage, Point(bin_w * (i - 1), hist_h - cvRound(g_hist.at<float>(i - 1))),
				Point(bin_w * (i), hist_h - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0),
				2, 8, 0);
			line(histImage, Point(bin_w * (i - 1), hist_h - cvRound(r_hist.at<float>(i - 1))),
				Point(bin_w * (i), hist_h - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255),
				2, 8, 0);
		}


	}
	imshow(StrTitle, histImage);
}


Python

import cv2 as cv
import numpy as np
#
# THRESH_BINARY = 0
# THRESH_BINARY_INV = 1
# THRESH_TRUNC = 2
# THRESH_TOZERO = 3
# THRESH_TOZERO_INV = 4
#
src = cv.imread("D:/images/lena.jpg")
cv.namedWindow("input", cv.WINDOW_AUTOSIZE)
cv.imshow("input", src)
h, w = src.shape[:2]

# 自动阈值分割 OTSU
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
print("ret :", ret)
cv.imshow("binary", binary)

result = np.zeros([h, w*2, 3], dtype=src.dtype)
result[0:h,0:w,:] = src
result[0:h,w:2*w,:] = cv.cvtColor(binary, cv.COLOR_GRAY2BGR)
cv.putText(result, "input", (10, 30), cv.FONT_ITALIC, 1.0, (0, 0, 255), 2)
cv.putText(result, "binary, threshold = " + str(ret), (w+10, 30), cv.FONT_ITALIC, 1.0, (0, 0, 255), 2)
cv.imshow("result", result)
cv.imwrite("D:/binary_result.png", result)

cv.waitKey(0)
cv.destroyAllWindows()


結果圖:


★延伸說明/重點回顧:


發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *