Canny算子边缘检测原理以及实现

10 篇文章 0 订阅

Canny算子边缘检测原理以及实现

原理解析

基于卷积运算的边缘检测算法,比如Sobel、Prewitt等,有以下几个缺点:

  • 没有充分利用边缘的梯度方向。
  • 最后输出的边缘二值图,只是简单的利用阈值进行处理,显然如果阈值过大,则会损失很多边缘信息;如果阈值过小,则会出现很多的噪声。

而Canny边缘检测基于这两点进行的改进,提出:

  • 基于梯度方向的非极大值抑制。
  • 双阈值的滞后阈值处理。

步骤

  1. 利用高斯滤波来平滑图像,目的是去噪声。
  2. 寻找图像的强度梯度,和方向。
  3. 利用非极大值抑制来消除边误检。(其实是对边缘强度图进行细化)
  4. 双阈值的滞后阈值处理。

1、高斯平滑(略)

2、计算梯度幅度和方向

这个步骤可采用的算子有:Roberts、Prewitt、Sobel、Scharr等。
一般采用soble算子,OpenCV也是如此,利用soble水平和垂直算子与输入图像卷积计算dx、dy。也可用Prewitt和Scharr,不建议用Roberts,因为Roberts算子边缘检测使用了很少的邻域像素来近似边缘强度,因为对图像的噪声具有高度敏感性。
下面采用soble算子为例:
soble算子具有分离性。图像f(x,y)分别于卷积核Sobel.x和卷积核Sobel.y卷积得到水平方向的强度梯度dx和垂直方向的强度梯度dy。
在这里插入图片描述
边缘强度为:
在这里插入图片描述
为了简化计算,幅值也可以作如下近似:
在这里插入图片描述
梯度方向为:
在这里插入图片描述

3、利用非极大值抑制来消除边误检。

在这里插入图片描述
如图a,在点(1,1)处放置坐标轴。图b,知道梯度方向后的邻域,将(1,1)点的边缘强度912与左下方的270以及右上方的292做比较,912>270&&912>292,所以这个点可以极大值,如果不全大于这两个值,则就需要抑制了,令这个点(1,1)=0。其他的点方法类似。
至于方向如何判断呢?梯度方向一般离散化为一下四种情况:
在这里插入图片描述
非极大值抑制的第二种方式:插值法
插值法能够更加的衡量梯度方向上的边缘强度。
在这里插入图片描述
在这里插入图片描述

4. 双阈值的滞后阈值处理

  • 选取系数TH和TL,比率为2:1或3:1
  • 将小于低阈值的点抛弃,赋0;将大于高阈值的点立即标记(这些点为确定边缘点)
  • 将小于高阈值,大于低阈值的点使用8连通区域确定

代码:

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
cv::Mat non_maximum_suppression_Inter(cv::Mat dx, cv::Mat dy);
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;
		}
	}
}
void Edge_Canny_in(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 = non_maximum_suppression_Inter(dx, dy);

	//双阈值处理及边缘连接
	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("l0.png");

	if (src.empty()) {
		return -1;
	}
	if (src.channels() > 1) cv::cvtColor(src, src, cv::ColorConversionCodes::COLOR_BGR2GRAY);
	cv::Mat edge, edge_in,dst;

	//Canny
	Edge_Canny(src, edge, 20, 60);
	Edge_Canny_in(src, edge_in, 20, 60);
	//opencv自带Canny
	cv::Canny(src, dst, 20, 60);

	cv::namedWindow("src");
	imshow("src", src);
	cv::namedWindow("My_canny");
	imshow("My_canny", edge);
	cv::namedWindow("My_canny_in");
	imshow("My_canny_in", edge_in);
	cv::namedWindow("Opencv_canny");
	imshow("Opencv_canny", dst);
	cv::waitKey(0);
	return 0;
}
//非极大值抑制 : 插值比较
cv::Mat non_maximum_suppression_Inter(cv::Mat dx, cv::Mat dy)
{
	//使用平方和开方的方式计算边缘强度
	cv::Mat edgeMag;
	cv::magnitude(dx, dy, edgeMag);
	//宽高
	int rows = dx.rows;
	int cols = dy.cols;
	//边缘强度的非极大抑制
	cv::Mat edgeMag_nonMaxSup = cv::Mat::zeros(dx.size(), dx.type());
	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);
			if (x == 0 && y == 0)
				continue;
			float angle = atan2f(y, x) / CV_PI * 180;
			//领域内八个方向上的边缘强度
			float leftTop = edgeMag.at<float>(r - 1, c - 1);
			float top = edgeMag.at<float>(r - 1, c);
			float rightBottom = edgeMag.at<float>(r + 1, c + 1);
			float right = edgeMag.at<float>(r, c + 1);
			float rightTop = edgeMag.at<float>(r - 1, c + 1);
			float leftBottom = edgeMag.at<float>(r + 1, c - 1);
			float bottom = edgeMag.at<float>(r + 1, c);
			float left = edgeMag.at<float>(r, c - 1);
			float mag = edgeMag.at<float>(r, c);
			//左上方与上方的插值 右下方和下方的插值
			if ((angle > 45 && angle <= 90) || (angle > -135 && angle <= -90))
			{
				float ratio = x / y;
				float top = edgeMag.at<float>(r - 1, c);
				//插值
				float leftTop_top = ratio * leftTop + (1 - ratio)*top;
				float rightBottom_bottom = ratio * rightBottom + (1 - ratio)*bottom;
				if (mag > leftTop_top && mag > rightBottom_bottom)
					edgeMag_nonMaxSup.at<float>(r, c) = mag;
			}
			//右上方和上方的插值 左下方和下方的插值
			if ((angle > 90 && angle <= 135) || (angle > -90 && angle <= -45))
			{
				float ratio = abs(x / y);
				float rightTop_top = ratio * rightTop + (1 - ratio)*top;
				float leftBottom_bottom = ratio * leftBottom + (1 - ratio)*bottom;
				if (mag > rightTop_top && mag > leftBottom_bottom)
					edgeMag_nonMaxSup.at<float>(r, c) = mag;
			}
			//左上方和左方的插值 右下方和右方的插值
			if ((angle >= 0 && angle <= 45) || (angle > -180 && angle <= -135))
			{
				float ratio = y / x;
				float rightBottom_right = ratio * rightBottom + (1 - ratio)*right;
				float leftTop_left = ratio * leftTop + (1 - ratio)*left;
				if (mag > rightBottom_right && mag > leftTop_left)
					edgeMag_nonMaxSup.at<float>(r, c) = mag;
			}
			//右上方和右方的插值 左下方和左方的插值
			if ((angle > 135 && angle <= 180) || (angle > -45 && angle <= 0))
			{
				float ratio = abs(y / x);
				float rightTop_right = ratio * rightTop + (1 - ratio)*right;
				float leftBottom_left = ratio * leftBottom + (1 - ratio)*left;
				if (mag > rightTop_right && mag > leftBottom_left)
					edgeMag_nonMaxSup.at<float>(r, c) = mag;
			}
		}
	}
	return edgeMag_nonMaxSup;
}
  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值