邊緣提取算子(圖像邊緣提取)——canny算法的原理及實現 OpenCV (七)

canny邊緣檢測實現(C++、opencv)

1.作用:

       圖像邊緣信息主要集中在高頻段,通常說圖像銳化或檢測邊緣,實質就是高頻濾波。我們知道微分運算是求信號的變化率,具有加強高頻分量的作用。在空域運算中來說,對圖像的銳化就是計算微分。對於數字圖像的離散信號,微分運算就變成計算差分或梯度。圖像處理中有多種邊緣檢測(梯度)算子,常用的包括普通一階差分,Robert算子(交叉差分),Sobel算子等等,是基於尋找梯度強度。拉普拉斯算子(二階差分)是基於過零點檢測。通過計算梯度,設置閥值,得到邊緣圖像。

Canny邊緣檢測算子是一種多級檢測算法。1986年由John F. Canny提出,同時提出了邊緣檢測的三大準則:

  1. 低錯誤率的邊緣檢測:檢測算法應該精確地找到圖像中的儘可能多的邊緣,儘可能的減少漏檢和誤檢。
  2. 最優定位:檢測的邊緣點應該精確地定位於邊緣的中心。
  3. 圖像中的任意邊緣應該只被標記一次,同時圖像噪聲不應產生僞邊緣。

Canny的工作本質是,從數學上表達前面的三個準則。因此Canny的步驟如下:

  1. 對輸入圖像進行高斯平滑,降低錯誤率。
  2. 計算梯度幅度和方向來估計每一點處的邊緣強度與方向。
  3. 根據梯度方向,對梯度幅值進行非極大值抑制。本質上是對Sobel、Prewitt等算子結果的進一步細化。
  4. 用雙閾值處理和連接邊緣。

降噪

任何邊緣檢測算法都不可能在未經處理的原始數據上很好地處理,所以第一步是對原始數據與高斯平滑模板作卷積,得到的圖像與原始圖像相比有些輕微的模糊(blurred)。這樣,單獨的一個像素噪聲在經過高斯平滑的圖像上變得幾乎沒有影響。

尋找圖像中的亮度梯度

圖像中的邊緣可能會指向不同的方向,所以Canny算法使用4個mask檢測水平、垂直以及對角線方向的邊緣。原始圖像與每個mask所作的卷積都存儲起來。對於每個點我們都標識在這個點上的最大值以及生成的邊緣的方向。這樣我們就從原始圖像生成了圖像中每個點亮度梯度圖以及亮度梯度的方向。

在圖像中跟蹤邊緣

較高的亮度梯度比較有可能是邊緣,但是沒有一個確切的值來限定多大的亮度梯度是邊緣多大又不是,所以Canny使用了滯後閾值。

滯後閾值需要兩個閾值——高閾值與低閾值。假設圖像中的重要邊緣都是連續的曲線,這樣我們就可以跟蹤給定曲線中模糊的部分,並且避免將沒有組成曲線的噪聲像素當成邊緣。所以我們從一個較大的閾值開始,這將標識出我們比較確信的真實邊緣,使用前面導出的方向信息,我們從這些真正的邊緣開始在圖像中跟蹤整個的邊緣。在跟蹤的時候,我們使用一個較小的閾值,這樣就可以跟蹤曲線的模糊部分直到我們回到起點。

一旦這個過程完成,我們就得到了一個二值圖像,每點表示是否是一個邊緣點。

一個獲得亞像素精度邊緣的改進實現是在梯度方向檢測二階方向導數的過零點

 它在梯度方向的三階方向導數滿足符號條件

其中表示用高斯核平滑原始圖像得到的尺度空間表示L計算得到的偏導數。用這種方法得到的邊緣片斷是連續曲線,這樣就不需要另外的邊緣跟蹤改進。滯後閾值也可以用於亞像素邊緣檢測。 

 

2.實現:

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
////////////////////sobel算子/////////////////////////
//階乘
int factorial(int n){
	int fac = 1;
	//0的階乘
	if (n == 0)
		return fac;
	for (int i = 1; i <= n; ++i){
		fac *= i;
	}
	return fac;
}
 
//獲得Sobel平滑算子
cv::Mat getSobelSmoooth(int wsize){
	int n = wsize - 1;
	cv::Mat SobelSmooothoper = cv::Mat::zeros(cv::Size(wsize, 1), CV_32FC1);
	for (int k = 0; k <= n; k++){
		float *pt = SobelSmooothoper.ptr<float>(0);
		pt[k] = factorial(n) / (factorial(k)*factorial(n - k));
	}
	return SobelSmooothoper;
}
 
//獲得Sobel差分算子
cv::Mat getSobeldiff(int wsize){
	cv::Mat Sobeldiffoper = cv::Mat::zeros(cv::Size(wsize, 1), CV_32FC1);
	cv::Mat SobelSmoooth = getSobelSmoooth(wsize - 1);
	for (int k = 0; k < wsize; k++){
		if (k == 0)
			Sobeldiffoper.at<float>(0, k) = 1;
		else if (k == wsize - 1)
			Sobeldiffoper.at<float>(0, k) = -1;
		else
			Sobeldiffoper.at<float>(0, k) = SobelSmoooth.at<float>(0, k) - SobelSmoooth.at<float>(0, k - 1);
	}
	return Sobeldiffoper;
}
 
//卷積實現
void conv2D(cv::Mat& src, cv::Mat& dst, cv::Mat kernel, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){
	cv::Mat  kernelFlip;
	cv::flip(kernel, kernelFlip, -1);
	cv::filter2D(src, dst, ddepth, kernelFlip, anchor, delta, borderType);
}
 
 
//可分離卷積———先垂直方向卷積,後水平方向卷積
void sepConv2D_Y_X(cv::Mat& src, cv::Mat& dst, cv::Mat kernel_Y, cv::Mat kernel_X, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){
	cv::Mat dst_kernel_Y;
	conv2D(src, dst_kernel_Y, kernel_Y, ddepth, anchor, delta, borderType); //垂直方向卷積
	conv2D(dst_kernel_Y, dst, kernel_X, ddepth, anchor, delta, borderType); //水平方向卷積
}
 
//可分離卷積———先水平方向卷積,後垂直方向卷積
void sepConv2D_X_Y(cv::Mat& src, cv::Mat& dst, cv::Mat kernel_X, cv::Mat kernel_Y, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){
	cv::Mat dst_kernel_X;
	conv2D(src, dst_kernel_X, kernel_X, ddepth, anchor, delta, borderType); //水平方向卷積
	conv2D(dst_kernel_X, dst, kernel_Y, ddepth, anchor, delta, borderType); //垂直方向卷積
}
 
 
//Sobel算子邊緣檢測
//dst_X 垂直方向
//dst_Y 水平方向
void Sobel(cv::Mat& src, cv::Mat& dst_X, cv::Mat& dst_Y, cv::Mat& dst, int wsize, int ddepth, cv::Point anchor = cv::Point(-1, -1), int delta = 0, int borderType = cv::BORDER_DEFAULT){
 
	cv::Mat SobelSmooothoper = getSobelSmoooth(wsize); //平滑係數
	cv::Mat Sobeldiffoper = getSobeldiff(wsize); //差分系數
 
	//可分離卷積———先垂直方向平滑,後水平方向差分——得到垂直邊緣
	sepConv2D_Y_X(src, dst_X, SobelSmooothoper.t(), Sobeldiffoper, ddepth);
 
	//可分離卷積———先水平方向平滑,後垂直方向差分——得到水平邊緣
	sepConv2D_X_Y(src, dst_Y, SobelSmooothoper, Sobeldiffoper.t(), ddepth);
 
	//邊緣強度(近似)
	dst = abs(dst_X) + abs(dst_Y);
	cv::convertScaleAbs(dst, dst); //求絕對值並轉爲無符號8位圖
}
 
 
//確定一個點的座標是否在圖像內
bool checkInRang(int r,int c, int rows, int cols){
	if (r >= 0 && r < rows && c >= 0 && c < cols)
		return true;
	else
		return false;
}
 
//從確定邊緣點出發,延長邊緣
void trace(cv::Mat &edgeMag_noMaxsup, cv::Mat &edge, float TL,int r,int c,int rows,int cols){
	if (edge.at<uchar>(r, c) == 0){
		edge.at<uchar>(r, c) = 255;
		for (int i = -1; i <= 1; ++i){
			for (int j = -1; j <= 1; ++j){
				float mag = edgeMag_noMaxsup.at<float>(r + i, c + j);
				if (checkInRang(r + i, c + j, rows, cols) && mag >= TL)
					trace(edgeMag_noMaxsup, edge, TL, r + i, c + j, rows, cols);
			}
		}
	}
}
 
//Canny邊緣檢測
void Edge_Canny(cv::Mat &src, cv::Mat &edge, float TL, float TH, int wsize=3, bool L2graydient = false){
	int rows = src.rows;
	int cols = src.cols;
 
	//高斯濾波
	cv::GaussianBlur(src,src,cv::Size(5,5),0.8);
	//sobel算子
	cv::Mat dx, dy, sobel_dst;
	Sobel(src, dx, dy, sobel_dst, wsize, CV_32FC1);
 
	//計算梯度幅值
	cv::Mat edgeMag;
	if (L2graydient)   
        cv::magnitude(dx, dy, edgeMag); //開平方
	else  
        edgeMag = abs(dx) + abs(dy); //絕對值之和近似
 
	//計算梯度方向 以及 非極大值抑制
	cv::Mat edgeMag_noMaxsup = cv::Mat::zeros(rows, cols, CV_32FC1);
	for (int r = 1; r < rows - 1; ++r){
		for (int c = 1; c < cols - 1; ++c){
			float x = dx.at<float>(r, c);
			float y = dy.at<float>(r, c);
			float angle = std::atan2f(y, x) / CV_PI * 180; //當前位置梯度方向
			float mag = edgeMag.at<float>(r, c);  //當前位置梯度幅值
 
			//非極大值抑制
			//垂直邊緣--梯度方向爲水平方向-3*3鄰域內左右方向比較
			if (abs(angle)<22.5 || abs(angle)>157.5){
				float left = edgeMag.at<float>(r, c - 1);
				float right = edgeMag.at<float>(r, c + 1);
				if (mag >= left && mag >= right)
					edgeMag_noMaxsup.at<float>(r, c) = mag;
			}
		
			//水平邊緣--梯度方向爲垂直方向-3*3鄰域內上下方向比較
			if ((angle>=67.5 && angle<=112.5 ) || (angle>=-112.5 && angle<=-67.5)){
				float top = edgeMag.at<float>(r-1, c);
				float down = edgeMag.at<float>(r+1, c);
				if (mag >= top && mag >= down)
					edgeMag_noMaxsup.at<float>(r, c) = mag;
			}
 
			//+45°邊緣--梯度方向爲其正交方向-3*3鄰域內右上左下方向比較
			if ((angle>112.5 && angle<=157.5) || (angle>-67.5 && angle<=-22.5)){
				float right_top = edgeMag.at<float>(r - 1, c+1);
				float left_down = edgeMag.at<float>(r + 1, c-1);
				if (mag >= right_top && mag >= left_down)
					edgeMag_noMaxsup.at<float>(r, c) = mag;
			}
 
 
			//+135°邊緣--梯度方向爲其正交方向-3*3鄰域內右下左上方向比較
			if ((angle >=22.5 && angle < 67.5) || (angle >= -157.5 && angle < -112.5)){
				float left_top = edgeMag.at<float>(r - 1, c - 1);
				float right_down = edgeMag.at<float>(r + 1, c + 1);
				if (mag >= left_top && mag >= right_down)
					edgeMag_noMaxsup.at<float>(r, c) = mag;
			}
		}
	}
 
	//雙閾值處理及邊緣連接
	edge = cv::Mat::zeros(rows, cols, CV_8UC1);
	for (int r = 1; r < rows - 1; ++r){
		for (int c = 1; c < cols - 1; ++c){
			float mag = edgeMag_noMaxsup.at<float>(r, c);
			//大於高閾值,爲確定邊緣點
			if (mag >= TH)
				trace(edgeMag_noMaxsup, edge, TL, r, c, rows, cols);
			else if (mag < TL)
				edge.at<uchar>(r, c) = 0;
		}
	}
}
 
int main(){
	cv::Mat src = cv::imread("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Img\\lena.jpg");
 
	if (src.empty()){
		return -1;
	}
	if (src.channels() > 1) cv::cvtColor(src, src, CV_RGB2GRAY);
	cv::Mat edge,dst;
 
	//Canny
	Edge_Canny(src, edge, 20,60);
 
	//opencv自帶Canny
	cv::Canny(src, dst, 20, 80);
 
	cv::namedWindow("src", CV_WINDOW_NORMAL);
	imshow("src", src);
	cv::namedWindow("My_canny", CV_WINDOW_NORMAL);
	imshow("My_canny", edge);
	cv::namedWindow("Opencv_canny", CV_WINDOW_NORMAL);
	imshow("Opencv_canny", dst);
	cv::waitKey(0);
	return 0;
}
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/video/background_segm.hpp>
#include "opencv2/calib3d/calib3d.hpp"
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(int argc, char** argv) {
	cv::Mat temp_thin = cv::imread("../example/2.jpg", CV_LOAD_IMAGE_UNCHANGED);
	//cv::imshow("temp_thin",temp_thin);
	cv::Mat gray_image, dst, temp_thin_image, binary_image, edge_image;
	
	cvtColor(temp_thin, gray_image, CV_RGB2GRAY);

	GaussianBlur(gray_image, edge_image, Size(3, 3), 0, 0);
	//blur(gray, edge, Size(3, 3));
	Canny(edge_image, edge_image, 10, 150, 3, false);
	cv::imshow("edge_image", edge_image);

	std:; string anme_pic = "..\\example\\edge_image.bmp";
	cv::imwrite(anme_pic, edge_image);

	cv::waitKey(0);
	return 0;
}

3.效果

 

 

 

4.函數原型

void Canny(InputArray image, OutputArray edges, double threshold1, 
double threshold2, int apertureSize=3, bool L2gradient=false )

5.原理

  • 須滿足條件:抑制噪聲;精確定位邊緣。
  • 從數學上表達了三個準則[信噪比準則(低錯誤率)、定位精度準則、單邊緣響應準則],並尋找表達式的最佳解。
  • 屬於先平滑後求導的方法。

 

 

1、高斯平滑濾波(略)

2、計算圖像梯度的幅值和方向

可選用的模板:soble算子、Prewitt算子、一階差分卷積模板等等;

在此選用Prewitt算子爲例:

 

 

3、對幅值圖像進行非極大值抑制

首先將角度劃分成四個方向範圍 :水平(0°)、−45°、垂直(90°)、+45°。如下圖:

 

扇形區標號d1~d4,對應3*3領域的4種可能的組合,1-x-5 , 7-x-3 , 2-x-6 , 8-x-4。

在每一點上,領域中心 x 與沿着其對應的梯度方向的兩個像素相比,若中心像素爲最大值,則保留,否則中心置0,這樣可以抑制非極大值,保留局部梯度最大的點,以得到細化的邊緣。

4、用雙閾值算法檢測和連接邊緣

選取係數TH和TL,比率爲2:1或3:1。(一般取TH=0.3或0.2,TL=0.1);
取出非極大值抑制後的圖像中的最大梯度幅值,定義高低閾值。即:TH×Max,TL×Max (當然可以自己給定) ;
將小於低閾值的點拋棄,賦0;將大於高閾值的點立即標記(這些點就是邊緣點),賦1;
將小於高閾值,大於低閾值的點使用8連通區域確定(即:只有與TH像素連接時纔會被接受,成爲邊緣點,賦  1)。
 

6.參考

【1】 https://blog.csdn.net/weixin_40647819/article/details/91411424

【2】https://blog.csdn.net/likezhaobin/article/details/6892176

【3】https://docs.opencv.org/master/dd/d1a/group__imgproc__feature.html

【4】https://blog.csdn.net/weixin_40647819/article/details/80377343

【5】https://blog.csdn.net/liuzhuomei0911/article/details/51345591

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章