边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化,这些包括深度上的不连续、表面方向不连续、物质属性变化和场景照明变化, 边缘检测大幅度地减少了数据量,并且剔除了可以认为不相关的信息,保留了图像重要的结构属性。常用的描边也是先进行边缘然后再进行边缘处理。
一、卷积
卷积(Convolution)本质上来讲就是一种数学运算,跟减加乘除没有区别。在图像处理中用一个模板(这个模板就是卷积核(kernel))和一幅图像进行卷积,对于图像上的一个点,让模板的原点和该点重合,然后模板上的点和图像上对应的点相乘,最后将各点的积相加,就得到该点的卷积值。然后移动模板对正下一个点,对图像上的每个点都这样处理。卷积是一种积分运算,可以看作加权求和,可以用来消除噪声、特征增强, 把一个点的像素值用它周围的点的像素值的加权平均代替。
卷积核通常是一个四方形风格结构(如2x2、3x3),该网格区域内的每一个方格都有一个权重值。当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上,如下图所示,翻转核之后再依次计算核中每个元素和其覆盖的图像像素值的乘积,最后将各乘积累加,得到的结果就是该像素的新像素值。然后移动卷积核到下一个像素,进行同样的处理,至到所有像素都处理完。
二、sobel算子
卷积的神奇之处在于选择的卷积核,用于边缘检测的卷积核也叫边缘检测算子,先后有好几种边缘检测算子被提出来。
- Roberts算子
- Prewitt算子
Prewitt算子利用像素点上下、左右邻点灰度差,在边缘处达到极值检测边缘。对噪声具有平滑作用,但是定位精度不够高。
- Sobel算子
Sobel 算子主要用作边缘检测,它是一个离散的一阶差分算子,用来计算图像亮度函数的一阶梯度之近似值。在图像的任何一点使用此算子,将会产生该点对应的梯度矢量或是其法矢量。与Prewitt算子相比,Sobel算子对于像素的位置的影响做了加权,可以降低边缘模糊程度,因此效果更好。
该算子包含两组3x3的矩阵,分别为横向及纵向,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。如果以A代表原始图像,Gx及Gy分别代表经横向及纵向边缘检测的图像灰度值,其公式如下:
具体计算如下:
Gx = (-1)*f(x-1, y-1) + 0*f(x,y-1) + 1*f(x+1,y-1)
+(-2)*f(x-1,y) + 0*f(x,y)+2*f(x+1,y)
+(-1)*f(x-1,y+1) + 0*f(x,y+1) + 1*f(x+1,y+1)
= [f(x+1,y-1)+2*f(x+1,y)+f(x+1,y+1)]-[f(x-1,y-1)+2*f(x-1,y)+f(x-1,y+1)]
Gy =1* f(x-1, y-1) + 2*f(x,y-1)+ 1*f(x+1,y-1)
+0*f(x-1,y) 0*f(x,y) + 0*f(x+1,y)
+(-1)*f(x-1,y+1) + (-2)*f(x,y+1) + (-1)*f(x+1, y+1)
= [f(x-1,y-1) + 2f(x,y-1) + f(x+1,y-1)]-[f(x-1, y+1) + 2*f(x,y+1)+f(x+1,y+1)]
其中f(a,b), 表示图像(a,b)点的灰度值;
图像的每一个像素的横向及纵向灰度值通过以下公式结合,来计算该点灰度的大小:
G=Gx2+Gy2
通常,为了提高效率 使用不开平方的近似值:
G=∣Gx∣+∣Gy∣
如果梯度G大于某一阀值则认为该点(x,y)为边缘点。Sobel算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘。对噪声具有平滑作用,提供较为精确的边缘方向信息,边缘定位精度不够高。当对精度要求不是很高时,是一种较为常用的边缘检测方法。
Sobel算子的计算速度比Roberts算子慢,但其较大的卷积核在很大程度上平滑了输入图像,使算子对噪声的敏感性降低。与Roberts算子相比,通常也会为相似的边缘产生更高的输出值。与Roberts算子一样,操作时输出值很容易溢出仅支持小整数像素值(例如8位整数图像)的图像类型的最大允许像素值。当发生这种情况时,标准做法是简单地将溢出的输出像素设置为最大允许值。通过使用支持范围更大的像素值的图像类型,可以避免此问题。
三、ARCore计算机视觉示例
打开ARCore SDK自带的Computer vision示例。
在Hierarchy窗口中选中ComputerVisionController,在Inspector窗口中双击编辑ComputerVisionController.cs脚本。先不管其他辅助操作的功能如获取摄像头图像、显示信息等,我们直接查看EdgeDetector.Detect()方法,这里采用的边缘检测就是使用的Sobel算子卷积。 private static void Sobel(byte[] outputImage, IntPtr inputImage, int width, int height, int rowStride)
{
// Adjust buffer size if necessary.
int bufferSize = rowStride * height;
if (bufferSize != s_ImageBufferSize || s_ImageBuffer.Length == 0)
{
s_ImageBufferSize = bufferSize;
s_ImageBuffer = new byte[bufferSize];
}
// Move raw data into managed buffer.
System.Runtime.InteropServices.Marshal.Copy(inputImage, s_ImageBuffer, 0, bufferSize);
// 边缘检测的阈值
int threshold = 128 * 128;
for (int j = 1; j < height - 1; j++)
{
for (int i = 1; i < width - 1; i++)
{
// Offset of the pixel at [i, j] of the input image.
int offset = (j * rowStride) + i;
// Neighbour pixels around the pixel at [i, j].
int a00 = s_ImageBuffer[offset - rowStride - 1];
int a01 = s_ImageBuffer[offset - rowStride];
int a02 = s_ImageBuffer[offset - rowStride + 1];
int a10 = s_ImageBuffer[offset - 1];
int a12 = s_ImageBuffer[offset + 1];
int a20 = s_ImageBuffer[offset + rowStride - 1];
int a21 = s_ImageBuffer[offset + rowStride];
int a22 = s_ImageBuffer[offset + rowStride + 1];
int xSum = -a00 - (2 * a10) - a20 + a02 + (2 * a12) + a22;
int ySum = a00 + (2 * a01) + a02 - a20 - (2 * a21) - a22;
if ((xSum * xSum) + (ySum * ySum) > threshold)
{
outputImage[(j * width) + i] = 0xFF; //是边缘则输出纯白
}
else
{
outputImage[(j * width) + i] = 0x1F; //不是边缘则输出黑色
}
}
}
}
通过之前的讲解,我们应该很容易理解这段代码,使用sobel算子做边缘检测的效果如下图所示: