使用Canny算法检测边缘,该算法满足边缘检测的三个条件
- 低错误率:只检测到存在的边缘。
- 良好的定位:检测到的边缘在真实边缘的中心。
- 最小响应:每个边缘只有一个检测器响应,尽可能排除噪声。
算法步骤:
- 使用高斯模糊去噪声
- 计算图像每个像素点的梯度强度和方向。默认使用Sobel算子,也可以自定义
- 应用非极大值(Non-maximum)抑制。这样可以保留细边缘。
- 应用双阈值(Double-Threshold)检测来确定真实的和潜在的边缘。
- a. 如果像素的梯度大于上限阈值,则认为是边缘
- b. 如果像素的梯度小于下限阈值,则认为是非边缘
- c. 如果像素的梯度在两阈值之间,只有该像素于边缘像素(对应梯度大于大限阈值)时,才认为是边缘。
Canny推荐上限阈值与下限阈值的比在2:1 到 3:1 之间(为什么?还有这两个阈值如何取,是难点,网上有自适应阈值的算法,待研究)
- 边缘连接
//函数原型1
void Canny(InputArray src,
OutputArray edges,
double threshold1,
double threshold2,
int apertureSize = 3,
bool L2gradient = false)
//函数原型2
void Canny(InputArray dx,
InputArray dy,
OutputArray edges,
double threshold1,
double threshold2,
bool L2gradient = false)
参数 | 说明 |
---|---|
InputArray src | 输入图像, 单通道8位图像 |
InputArray dx | 对水平方向(x轴)求导的图像(CV_16SC1 或 CV_16SC3),可使用其它算子求导 |
InputArray dy | 对垂直方向(y轴)求导的图像(CV_16SC1 或 CV_16SC3),可使用其它算子求导 |
OutputArray edges | 输出边缘图像。大小与通道数与输入图像一致 |
double threshold1 | 最小阈值。像素梯度比该值小,不认为是边缘 |
double threshold2 | 最大阈值(比threshold1小时,会自动交换值)。像素梯度比该值大,认为是边缘 |
int apertureSize | Sobel算子的核大小,必须为3、5、7 |
L2gradient | 是否使用L2梯度计算,为true时更准确、但更耗时,按L2norm = ( d I / d x ) 2 + ( d I / d y ) 2 =\sqrt{(dI/dx)^2 + (dI/dy)^2} =(dI/dx)2+(dI/dy)2计算, 默认为false已足够,按L1norm==丨dI/dx丨+丨dI/dy丨 |
Mat srcColor;
Mat srcGray = new Mat();
string winName = "Canny Demo,按 T 切换L2,按Esc退出";
bool L2gradient = false;
string tbMinThresholdName = "Low";//最小阈值
string tbThresholdRatioName = "Ratio";//最大、最小阈值比 2~3之间
string tbApertureSizeName = "Size";//核
public void Run()
{
if (!Utils.SelectFile(out string fileName)) fileName = ImagePath.Fruits;
srcColor = Cv2.ImRead(fileName, ImreadModes.Color);
if (srcColor.Empty()) throw new Exception($"图像打开有误:{fileName}");
Cv2.CvtColor(srcColor, srcGray, ColorConversionCodes.BGR2GRAY);
//均值平滑
Cv2.Blur(srcGray, srcGray,new Size(3, 3));
Cv2.NamedWindow(winName, WindowFlags.AutoSize);
//下限阈值调整
Cv2.CreateTrackbar(tbMinThresholdName, winName, 255, LowThresholdOnChanged);
Cv2.SetTrackbarPos(tbMinThresholdName, winName, 100);
//上、下限阈值比例调整,建议最小阈值与最大阈值比,1:2至1:3之间
Cv2.CreateTrackbar(tbThresholdRatioName, winName, 30, ThresholdRatioOnChanged);
Cv2.SetTrackbarMin(tbThresholdRatioName, winName, 20);
Cv2.SetTrackbarPos(tbThresholdRatioName, winName, 20);
//Sobel算子核大小
Cv2.CreateTrackbar(tbApertureSizeName, winName, 3, SizeOnChanged);
Cv2.SetTrackbarMin(tbApertureSizeName, winName, 1);
Init = false;//初始化结束
Cv2.SetTrackbarPos(tbApertureSizeName, winName, 1);
bool loop = true;
while (loop)
{
var c = (Char)Cv2.WaitKey(50);
switch(c)
{
case 't':
case 'T':
L2gradient = !L2gradient;
OnChanged();
break;
case 'q':
case 'Q':
case (Char)27:
loop = false;
break;
}
}
Cv2.DestroyAllWindows();
}
bool Init = true;
private void OnChanged()
{
if (Init) return;//初始化中,避免初始化未完成和多次调用
using var dstEdgeSobel = new Mat();
//最大阈值,原文建议是最小阈值的2至3倍
var HighThreshold = LowThreshold * ThresholdRatio;
Cv2.Canny(srcGray, dstEdgeSobel, LowThreshold, HighThreshold, apertureSize, L2gradient);
using Mat dst = Mat.Zeros(srcColor.Size(), srcColor.Type());
//使用边缘掩膜复制
srcColor.CopyTo(dst, dstEdgeSobel);
//自定义对图像求导,下面的结果与上面一样
double scale = 1.0D;
if (apertureSize == 7)
{
scale = 1 / 16.0D;
LowThreshold = LowThreshold / 16.0D;
HighThreshold = HighThreshold / 16.0D;
}
using var dx = new Mat();
Cv2.Sobel(srcGray, dx, MatType.CV_16S, 1, 0, apertureSize, scale, 0, BorderTypes.Replicate);
using var dy = new Mat();
Cv2.Sobel(srcGray, dy, MatType.CV_16S, 0, 1, apertureSize, scale, 0, BorderTypes.Replicate);
using var dstEdgeCustom=new Mat();
//dx、dy可使用其它算子求导
Cv2.Canny(dx, dy, dstEdgeCustom, LowThreshold, HighThreshold, L2gradient);
//dstEdgeSobel与dstEdgeCustom一样
//Utils.CompareMat(dstEdgeSobel, dstEdgeCustom);
Utils.PutText(dst, $"t1={LowThreshold},t2={LowThreshold * ThresholdRatio},size={apertureSize},L2={L2gradient}");
Cv2.ImShow(winName, dst);
}
double LowThreshold = 0;
///
/// 调整Canny最小阈值
///
///
///
private void LowThresholdOnChanged(int pos, IntPtr userData)
{
LowThreshold = pos;
OnChanged();
}
double ThresholdRatio = 3;
///
/// 调用Canny最大阈值
///
///
///
private void ThresholdRatioOnChanged(int pos, IntPtr userData)
{
ThresholdRatio = pos / 10.0D;
OnChanged();
}
int apertureSize = 3;//3到7
private void SizeOnChanged(int pos, IntPtr userData)
{
apertureSize = pos * 2 + 1;
OnChanged();
}
OpenCvSharp函数示例(目录)
参考
https://docs.opencv.org/4.7.0/da/d5c/tutorial_canny_detector.html