> 技术文档 > OpenCV——边缘检测

OpenCV——边缘检测


边缘检测

  • 一、边缘检测
  • 二、边缘检测算子
    • 2.1、Sobel算子
    • 2.2、Scharr算子
    • 2.3、Laplacian算子
  • 三、Canny边缘检测
    • 3.1、Canny边缘检测的步骤
    • 3.2、Canny算法的实现

一、边缘检测

边缘是指图像中像素的灰度值发生剧烈变化的区域:
OpenCV——边缘检测
图像中的边缘主要有以下几种成因:

  1. 表面不连续:两个面的交界处会自然形成边缘
  2. 深度不连续:主要是视觉因素
  3. 颜色不连续:两种不同颜色的交汇处会形成边缘
  4. 照明不连续:受光线影响形成的阴影会产生边缘

边缘检测方法主要有以下两大类:

  • 通过灰度值曲线一阶导数的最大值来寻找边缘,如Sobel算子、Scharr算子、Prewitt算子、roberts算子等
  • 通过灰度值曲线二阶导数过零点来寻找边缘,如Laplacian算子、Canny边缘检测等

二、边缘检测算子

2.1、Sobel算子

Sobel算子是通过一阶导数的最大值进行边缘检测的,用Sobel算子进行边缘检测的步骤如下:

1. 将图像与x方向的Sobel算子进行卷积。x方向的Sobel算子(尺寸3*3)如下:

-1 -2 1-2 0 2-1 0 1

2. 将图像与y方向的Sobel算子进行卷积。y方向的Sobel算子(尺寸3*3)如下:

-1 -2 -1 0 0 0 1 2 1

3. 对图像中的像素计算近似梯度幅度:
4. 统计极大值所在位置,获得图像的边缘:

Sobel算子有着不同的尺寸和阶次。如果想自己生成Sobel算子,则可以用getDerivKernels()函数实现:

//生成边缘检测用的滤波器。ksize=CV_SCHARR时生成的是Scharr滤波器,其余情况下生成的是Sobel滤波器void Imgproc.getDerivKernels(Mat kx, Mat ky, int dx, int dy, int ksize, boolean normalize, int ktype)
  • kx:行滤波器的输出矩阵,类型为ktype
  • ky:列滤波器的输出矩阵,类型为ktype
  • dx:x方向上导数的阶次
  • dy:y方向上导数的阶次
  • ksize:生成滤波器的尺寸,可选参数有CV_SCHARR或者1、3、5、7
  • normalize:是否对滤波器系数进行归一化。如果要滤波器的图像的数据类型是浮点型,则一般需要进行归一化;如果处理的是8位图像,结果存储在16位图像中并希望保留所有的小数部分,则需要将normalize设为false
  • ktype:滤波器系数的类型,可以是CV_32F或CV_64F

该函数智能生成Sobel或Scharr算子。事实上,Sobel()函数和Scharr()函数内部调用的就是这个函数。

//用Sobel算子进行边缘检测void Imgproc.Sobel(Mat src, Mat dst, int ddepth, int dx, int dy, int ksize)
  • src:输入图像
  • dst:输出图像,和src具有相同的尺寸和通道数
  • ddepth:输出图像的深度
  • dx:x方向求导的阶数,通常只能是0或1。如果dx为0,则表示x方向上没有求导
  • dy:y方向求导的阶数,通常只能是0或1。如果dy为0,则表示y方向上没有求导
  • ksize:Sobel算子的尺寸,只能是1、3、5或7

由于图像的边缘可能从高灰度值变为低灰度值,也可能从低灰度值变为高灰度值,所以用Sobel()函数计算的结果可能为正也可能为负。为了正确地显示图像,还需要用convertScaleAbs()函数将计算结果转为绝对值:

//计算矩阵中数值的绝对值,并转换为8位数据类型,可在此过程中进行缩放。对矩阵中每个数据和函数依次执行三项操作:缩放、求绝对值、转换为CV_8U类型。如为多通道矩阵,函数则需对每个通道独立进行处理void Core.convertScaleAbs(Mat src, Mat dst, double alpha)
  • src:输入矩阵
  • dst:输出矩阵
  • alpha:缩放因子,可选
public class Sobel { static { OpenCV.loadLocally(); // 自动下载并加载本地库 } public static void main(String[] args) { //读取图像灰度图并显示 Mat src = Imgcodecs.imread(\"F:/IDEAworkspace/opencv/src/main/java/demo/butterfly.png\", Imgcodecs.IMREAD_GRAYSCALE); HighGui.imshow(\"src\", src); HighGui.waitKey(0); Mat grad = new Mat(); Mat gx = new Mat(); Mat gy = new Mat(); Mat abs_gx = new Mat(); Mat abs_gy = new Mat(); //提取x方向边缘 Imgproc.Sobel(src, gx, -1, 1, 0); Core.convertScaleAbs(gx, abs_gx); //提取y方向边缘 Imgproc.Sobel(src, gy, -1, 0, 1); Core.convertScaleAbs(gy, abs_gy); //显示x和y方向边缘 HighGui.imshow(\"Sobel-X\", gx); HighGui.waitKey(0); HighGui.imshow(\"Sobel-Y\", gy); HighGui.waitKey(0); //计算整副图像的边缘并显示 Core.addWeighted(abs_gx, 0.5, abs_gy, 0.5, 0, grad); HighGui.imshow(\"Sobel\", grad); HighGui.waitKey(0);//直接计算整幅图像边缘 Mat all = new Mat(); Imgproc.Sobel(src, all, -1, 1, 1); HighGui.imshow(\"all\", all); HighGui.waitKey(0); System.exit(0); }}

原图灰度图:
OpenCV——边缘检测

X方向边缘:
OpenCV——边缘检测

Y方向边缘:
OpenCV——边缘检测

整图边缘:
OpenCV——边缘检测

一次计算整幅图边缘:
OpenCV——边缘检测

2.2、Scharr算子

用Sobel算计进行边缘检测的效率较高,但它有一个缺点:当Sobel算子尺寸较小时精度比较低。如果Sobel滤波器的尺寸为33且梯度方向接近水平或垂直方向,则问题会变得愈发明显。为了解决这个问题,OpenCV引进了Scharr算子。Scharr算子其实是一个特殊尺寸33的滤波器,在getDerivKernels()函数中将ksize设为CV_SCHARR时就是Scharr算子。当滤波器尺寸为3*3时,使用Scharr算子的速度与Sobel算子的速度一样,但是准确度更高。

//x方向的Scharr算子-3 0 3-10 0 10-1 0 3//y方向Scharr算子-3 -10 -3 0 0 0 3 10 3

Scharr算子的滤波器尺寸只能是3*3,因为它的产生就是为了解决Sobel算子在该尺寸的问题

//用Scharr算子进行边缘检测void Imgproc.Scharr(Mat src, Mat dst, int ddepth, int dx, int dy)
  • src:输入图像
  • dst:输出图像,和src具有相同的尺寸和通道数
  • ddepth:输出图像的深度
  • dx:x方向求导的阶数,通常只能是0或1。如果dx为0,则表示x方向上没有求导
  • dy:y方向求导的阶数,通常只能是0或1。如果dy为0,则表示y方向上没有求导
public class Scharr { static { OpenCV.loadLocally(); // 自动下载并加载本地库 } public static void main(String[] args) { //读取图像灰度图并显示 Mat src = Imgcodecs.imread(\"/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/butterfly.png\", Imgcodecs.IMREAD_GRAYSCALE); HighGui.imshow(\"src\", src); HighGui.waitKey(0); Mat grad = new Mat(); Mat gx = new Mat(); Mat gy = new Mat(); Mat abs_gx = new Mat(); Mat abs_gy = new Mat(); //提取x方向边缘 Imgproc.Scharr(src, gx, -1, 1, 0); Core.convertScaleAbs(gx, abs_gx); //提取y方向边缘 Imgproc.Scharr(src, gy, -1, 0, 1); Core.convertScaleAbs(gy, abs_gy); //在屏幕上显示X与Y方向边缘 HighGui.imshow(\"Scharr-X\", gx); HighGui.waitKey(0); HighGui.imshow(\"Scharr-Y\", gy); HighGui.waitKey(0); //计算整幅图的边缘并显示 Core.addWeighted(abs_gx, 0.5, abs_gy, 0.5, 0, grad); HighGui.imshow(\"Scharr\", grad); HighGui.waitKey(0); System.exit(0); }}

原图:
OpenCV——边缘检测

X方向边缘:
OpenCV——边缘检测

Y方向边缘;
OpenCV——边缘检测

整图边缘:

OpenCV——边缘检测

2.3、Laplacian算子

Sobel算子和Scharr算子进行边缘检测的效率较高,但是它们具有方向性,需要先分别在x方向和y方向求导,然后根据两个结果经计算后才可以得到图像的边缘。Laplacian算子则没有方向性,不需要分方向计算。Laplacian算子和Sobel算子、Scharr算子的另一个区别是:Laplacian算子是一个基于二阶导数的边缘检测算子。ksize=1时的Laplacian算子如下:

0 1 01 -4 10 1 1
//用Laplacian算子进行边缘检测void Imgproc.Laplacian(Mat src, Mat dst, int ddepth, int ksize)
  • src:输入图像
  • dst:输出图像,和src具有相同的尺寸和通道数
  • ddepth:输出图像的深度
  • ksize:滤波器尺寸,必须为正奇数
public class Laplacian { static { OpenCV.loadLocally(); // 自动下载并加载本地库 } public static void main(String[] args) { //读取图像灰度图并显示 Mat src = Imgcodecs.imread(\"/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/butterfly.png\", Imgcodecs.IMREAD_GRAYSCALE); HighGui.imshow(\"src\", src); HighGui.waitKey(0); //高斯滤波后用Laplacian算子提取边缘 Mat dst = new Mat(); Imgproc.GaussianBlur(src, dst, new Size(3, 3), 5); Imgproc.Laplacian(src, dst, 0, 3); Core.convertScaleAbs(dst, dst); //显示 HighGui.imshow(\"Laplacian\", dst); HighGui.waitKey(0); System.exit(0); }}

原图:

OpenCV——边缘检测

Laplacian算子边缘:
OpenCV——边缘检测

三、Canny边缘检测

Canny边缘检测算法源自John F.Canny于1986年发表的论文,论文中提出了以下3个评价最优边缘检测的标准:

  1. 准确检测:算法能尽可能多地标识出图像的实际边缘,而遗漏或错标的边缘点应尽可能少
  2. 精确定位:检测出的边缘点的位置应与实际边缘中心尽可能接近
  3. 单次响应:每个边缘位置只能标识一次

3.1、Canny边缘检测的步骤

1. 平滑降噪:

在Canny边缘检测中,一般使用高斯平滑滤波器进行平滑降噪。高斯滤波器考虑了像素离滤波器中心的距离因素,距离越近权重越大,距离越远权重越小。以下是一个5*5的高斯滤波器:

2 4 5 4 24 9 12 9 45 12 15 12 5 * 1/1394 9 12 9 42 4 5 4 2

2. 梯度计算:
计算图像中每像素的梯度幅值和方向,主要分以下两步:

  1. 用Sobel算子分别检测x方向和y方向的边缘
  2. 计算梯度的幅值和方向。为了简化起见,梯度方向取0°、45°、90°和135°这四个值
    OpenCV——边缘检测

3. 非极大值抑制:

上一步得到的梯度图像存在边缘较粗及噪声干扰等问题,此时可以用非极大值抑制来影除非边缘的像素。Canny 中的非极大值抑制是沿着梯度方向对幅值进行比较,如图所示。图中A点位于边缘附近,箭头方向为梯度方向。选择梯度方向上A点附近的像素B和C来检验A点的梯度值是否为极大值,若为极大值,则A保留为(候选)边缘点,否则A点被抑制。由此可见,所谓非极大值抑制就是将不是极大值的 候选点予以剔除的过程。

OpenCV——边缘检测

4. 双阈值处理:

经过以上三步之后得到的边缘质量已经很高了,但还是存在一些伪边缘,因此Canny算法用双阈值法对边缘进行筛选。双阌值法设置 minVal 和 maxVal 两个阈值,当候选的边缘点的梯度幅值高于 maxVal 时被认为是真正的边界,当低于 minVal 时则被抛弃:如果介于两者之间,则要看这个点是否与某个被确定为真正的边界的像素相连,如果是,则认定为边界点,否则该点被抛弃。

如图所示,由于 A 点高于 maxVal,所以是真正的边界点;由C点虽然低于 maxVal 但高于minVal 并且与 A 点相连,所以也是真正的边界点,而 B 点介于 minVal 和 maxVal 之间,但没有与真正的边界点相连,因而被抛弃。为了达到较好的效果,选择合适的 maxVal 和 minVal 值非常重要。

OpenCV——边缘检测

3.2、Canny算法的实现

//用Canny算法进行边缘检测void Imgproc.Canny(Mat image, Mat edges, double threshold1, double threshold2, int apertureSize)
  • image:8位输入图像
  • edges:输出的边缘图像,必须是8位单通道图像,尺寸与输入图像相同
  • threshold1:阈值1
  • threshold2:阈值2。threshold1和threshold2谁大谁小没有规定,系统会自动选择较大值为maxVal,较小值为minVal
  • apertureSize:Sobel算子的尺寸

Canny边缘检测的过程虽然较为复杂,但是经过OpenCV封装后的Canny()函数却非常简单:

public class Canny { static { OpenCV.loadLocally(); // 自动下载并加载本地库 } public static void main(String[] args) { //读取图像灰度图并显示 Mat src = Imgcodecs.imread(\"/Users/acton_zhang/J2EE/MavenWorkSpace/opencv_demo/src/main/java/demo1/butterfly.png\", Imgcodecs.IMREAD_GRAYSCALE); HighGui.imshow(\"src\", src); HighGui.waitKey(0); //进行Canny边缘检测并显示 Mat dst = new Mat(); Imgproc.GaussianBlur(src, src, new Size(3, 3), 5); Imgproc.Canny(src, dst, 60, 200); HighGui.imshow(\"Canny\", dst); HighGui.waitKey(0); System.exit(0); }}

原图:

OpenCV——边缘检测

Canny算法检测:

OpenCV——边缘检测

可以看出Canny边缘检测的效果非常好。无论是Sobel算子、Scharr算子还是Laplacian算子检测的边缘都比较模糊,而Canny算法得出的边缘非常请清晰。当然,为了得到较好的边缘,Canny算法耗费的时间也比较长。