OpenCV——边缘检测

发布于:2025-06-23 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、边缘检测

边缘是指图像中像素的灰度值发生剧烈变化的区域:
在这里插入图片描述
图像中的边缘主要有以下几种成因:

  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);
    }
}

原图灰度图:
在这里插入图片描述

X方向边缘:
在这里插入图片描述

Y方向边缘:
在这里插入图片描述

整图边缘:
在这里插入图片描述

一次计算整幅图边缘:
在这里插入图片描述

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);
    }
}

原图:
在这里插入图片描述

X方向边缘:
在这里插入图片描述

Y方向边缘;
在这里插入图片描述

整图边缘:

在这里插入图片描述

2.3、Laplacian算子

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

0  1  0
1 -4  1
0  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);
    }
}

原图:

在这里插入图片描述

Laplacian算子边缘:
在这里插入图片描述

三、Canny边缘检测

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

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

3.1、Canny边缘检测的步骤

1. 平滑降噪:

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

2  4  5  4  2
4  9 12  9  4
5 12 15 12  5    *   1/139
4  9 12  9  4
2  4  5  4  2

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

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

3. 非极大值抑制:

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

在这里插入图片描述

4. 双阈值处理:

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

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

在这里插入图片描述

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);
    }
}

原图:

在这里插入图片描述

Canny算法检测:

在这里插入图片描述

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


网站公告

今日签到

点亮在社区的每一天
去签到