C# 中的 Canny 边缘检测






4.65/5 (55投票s)
Canny 边缘检测算法的实现。
引言
边缘检测的总体目的是显著减少图像中的数据量,同时保留用于进一步图像处理的结构特性。 存在多种算法,本教程重点介绍 John F. Canny (JFC) 在 1986 年开发的一种特定算法。 尽管它已经相当古老,但它已成为一种标准的边缘检测方法,至今仍在研究中使用。
JFC 的目标是开发一种在以下标准方面最优的算法:
1. **检测**:应最大化检测到真实边缘点的概率,同时应最小化错误检测到非边缘点的概率。 这对应于最大化信噪比。
2. **定位**:检测到的边缘应尽可能靠近真实的边缘。
3. **响应数量**:一个真实的边缘不应导致检测到多个边缘(可以认为这隐式包含在第一个要求中)。
通过 Canny 对这些标准的数学公式化,Canny 边缘检测器对于一类特定边缘(称为阶跃边缘)是最佳的。 这里提供了一种 C# 实现的算法。
背景
建议读者对 Canny 边缘检测方法进行更多研究,以了解详细的理论。
使用代码
Canny 边缘检测算法
该算法分为 5 个独立的步骤运行:
1. 平滑:模糊图像以消除噪声。
private void GenerateGaussianKernel(int N, float S ,out int Weight)
{
float Sigma = S ;
float pi;
pi = (float)Math.PI;
int i, j;
int SizeofKernel=N;
float [,] Kernel = new float [N,N];
GaussianKernel = new int [N,N];
float[,] OP = new float[N, N];
float D1,D2;
D1= 1/(2*pi*Sigma*Sigma);
D2= 2*Sigma*Sigma;
float min=1000;
for (i = -SizeofKernel / 2; i <= SizeofKernel / 2; i++)
{
for (j = -SizeofKernel / 2; j <= SizeofKernel / 2; j++)
{
Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = ((1 / D1) * (float)Math.Exp(-(i * i + j * j) / D2));
if (Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] < min)
min = Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
}
}
int mult = (int)(1 / min);
int sum = 0;
if ((min > 0) && (min < 1))
{
for (i = -SizeofKernel / 2; i <= SizeofKernel / 2; i++)
{
for (j = -SizeofKernel / 2; j <= SizeofKernel / 2; j++)
{
Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = (float)Math.Round(Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] * mult, 0);
GaussianKernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = (int)Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
sum = sum + GaussianKernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
}
}
}
else
{
sum = 0;
for (i = -SizeofKernel / 2; i <= SizeofKernel / 2; i++)
{
for (j = -SizeofKernel / 2; j <= SizeofKernel / 2; j++)
{
Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = (float)Math.Round(Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] , 0);
GaussianKernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j] = (int)Kernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
sum = sum + GaussianKernel[SizeofKernel / 2 + i, SizeofKernel / 2 + j];
}
}
}
//Normalizing kernel Weight
Weight= sum;
return;
}
以下子程序通过高斯滤波消除噪声:
private int[,] GaussianFilter(int[,] Data)
{
GenerateGaussianKernel(KernelSize, Sigma,out KernelWeight);
int[,] Output = new int[Width, Height];
int i, j,k,l;
int Limit = KernelSize /2;
float Sum=0;
Output = Data; // Removes Unwanted Data Omission due to kernel bias while convolution
for (i = Limit; i <= ((Width - 1) - Limit); i++)
{
for (j = Limit; j <= ((Height - 1) - Limit); j++)
{
Sum = 0;
for (k = -Limit; k <= Limit; k++)
{
for (l = -Limit; l <= Limit; l++)
{
Sum = Sum + ((float)Data[i + k, j + l] * GaussianKernel [Limit + k, Limit + l]);
}
}
Output[i, j] = (int)(Math.Round(Sum/ (float)KernelWeight));
}
}
return Output;
}
2. 寻找梯度:应在图像的梯度具有较大幅度的位置标记边缘。
使用 Sobel X 和 Y 掩码生成图像的 X 和 Y 梯度;下一个函数使用 Sobel 滤波器掩码实现微分:
private float[,] Differentiate(int[,] Data, int[,] Filter)
{
int i, j,k,l, Fh, Fw;
Fw = Filter.GetLength(0);
Fh = Filter.GetLength(1);
float sum = 0;
float[,] Output = new float[Width, Height];
for (i = Fw / 2; i <= (Width - Fw / 2) - 1; i++)
{
for (j = Fh / 2; j <= (Height - Fh / 2) - 1; j++)
{
sum=0;
for(k=-Fw/2; k<=Fw/2; k++)
{
for(l=-Fh/2; l<=Fh/2; l++)
{
sum=sum + Data[i+k,j+l]*Filter[Fw/2+k,Fh/2+l];
}
}
Output[i,j]=sum;
}
}
return Output;
}
3. 非极大值抑制:应仅标记局部最大值作为边缘。
我们找到梯度方向,并使用这些方向执行非极大值抑制(请阅读“数字图像处理- R Gonzales-Pearson Education”)。
// Perform Non maximum suppression:
// NonMax = Gradient;
for (i = 0; i <= (Width - 1); i++)
{
for (j = 0; j <= (Height - 1); j++)
{
NonMax[i, j] = Gradient[i, j];
}
}
int Limit = KernelSize / 2;
int r, c;
float Tangent;
for (i = Limit; i <= (Width - Limit) - 1; i++)
{
for (j = Limit; j <= (Height - Limit) - 1; j++)
{
if (DerivativeX[i, j] == 0)
Tangent = 90F;
else
Tangent = (float)(Math.Atan(DerivativeY[i, j] / DerivativeX[i, j]) * 180 / Math.PI); //rad to degree
//Horizontal Edge
if (((-22.5 < Tangent) && (Tangent <= 22.5)) || ((157.5 < Tangent) && (Tangent <= -157.5)))
{
if ((Gradient[i, j] < Gradient[i, j + 1]) || (Gradient[i, j] < Gradient[i, j - 1]))
NonMax[i, j] = 0;
}
//Vertical Edge
if (((-112.5 < Tangent) && (Tangent <= -67.5)) || ((67.5 < Tangent) && (Tangent <= 112.5)))
{
if ((Gradient[i, j] < Gradient[i + 1, j]) || (Gradient[i, j] < Gradient[i - 1, j]))
NonMax[i, j] = 0;
}
//+45 Degree Edge
if (((-67.5 < Tangent) && (Tangent <= -22.5)) || ((112.5 < Tangent) && (Tangent <= 157.5)))
{
if ((Gradient[i, j] < Gradient[i + 1, j - 1]) || (Gradient[i, j] < Gradient[i - 1, j + 1]))
NonMax[i, j] = 0;
}
//-45 Degree Edge
if (((-157.5 < Tangent) && (Tangent <= -112.5)) || ((67.5 < Tangent) && (Tangent <= 22.5)))
{
if ((Gradient[i, j] < Gradient[i + 1, j + 1]) || (Gradient[i, j] < Gradient[i - 1, j - 1]))
NonMax[i, j] = 0;
}
}
}
4. 双阈值:通过阈值确定潜在边缘。
5. 滞后边缘跟踪:通过抑制所有未连接到非常确定(强)边缘的边缘来确定最终边缘。
private void HysterisisThresholding(int[,] Edges)
{
int i, j;
int Limit= KernelSize/2;
for (i = Limit; i <= (Width - 1) - Limit; i++)
for (j = Limit; j <= (Height - 1) - Limit; j++)
{
if (Edges[i, j] == 1)
{
EdgeMap[i, j] = 1;
}
}
for (i = Limit; i <= (Width - 1) - Limit; i++)
{
for (j = Limit; j <= (Height - 1) - Limit; j++)
{
if (Edges[i, j] == 1)
{
EdgeMap[i, j] = 1;
Travers(i, j);
VisitedMap[i, j] = 1;
}
}
}
return;
}
//Recursive Procedure
private void Travers(int X, int Y)
{
if (VisitedMap[X, Y] == 1)
{
return;
}
//1
if (EdgePoints[X + 1, Y] == 2)
{
EdgeMap[X + 1, Y] = 1;
VisitedMap[X + 1, Y] = 1;
Travers(X + 1, Y);
return;
}
//2
if (EdgePoints[X + 1, Y - 1] == 2)
{
EdgeMap[X + 1, Y - 1] = 1;
VisitedMap[X + 1, Y - 1] = 1;
Travers(X + 1, Y - 1);
return;
}
//3
if (EdgePoints[X, Y - 1] == 2)
{
EdgeMap[X , Y - 1] = 1;
VisitedMap[X , Y - 1] = 1;
Travers(X , Y - 1);
return;
}
//4
if (EdgePoints[X - 1, Y - 1] == 2)
{
EdgeMap[X - 1, Y - 1] = 1;
VisitedMap[X - 1, Y - 1] = 1;
Travers(X - 1, Y - 1);
return;
}
//5
if (EdgePoints[X - 1, Y] == 2)
{
EdgeMap[X - 1, Y ] = 1;
VisitedMap[X - 1, Y ] = 1;
Travers(X - 1, Y );
return;
}
//6
if (EdgePoints[X - 1, Y + 1] == 2)
{
EdgeMap[X - 1, Y + 1] = 1;
VisitedMap[X - 1, Y + 1] = 1;
Travers(X - 1, Y + 1);
return;
}
//7
if (EdgePoints[X, Y + 1] == 2)
{
EdgeMap[X , Y + 1] = 1;
VisitedMap[X, Y + 1] = 1;
Travers(X , Y + 1);
return;
}
//8
if (EdgePoints[X + 1, Y + 1] == 2)
{
EdgeMap[X + 1, Y + 1] = 1;
VisitedMap[X + 1, Y + 1] = 1;
Travers(X + 1, Y + 1);
return;
}
//VisitedMap[X, Y] = 1;
return;
}
//Canny Class Ends
}
通过一个递归函数执行此操作,该函数通过两个阈值高阈值 (TH) 和低阈值 (TL) 以及 8 连通性分析执行双阈值处理:
关注点
请参阅 Gonzalez & woods 的“数字图像处理”,Pearson Education。