边缘检测
一、边缘检测
边缘是指图像中像素的灰度值发生剧烈变化的区域:
图像中的边缘主要有以下几种成因:
- 表面不连续:两个面的交界处会自然形成边缘
- 深度不连续:主要是视觉因素
- 颜色不连续:两种不同颜色的交汇处会形成边缘
- 照明不连续:受光线影响形成的阴影会产生边缘
边缘检测方法主要有以下两大类:
- 通过灰度值曲线一阶导数的最大值来寻找边缘,如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个评价最优边缘检测的标准:
- 准确检测:算法能尽可能多地标识出图像的实际边缘,而遗漏或错标的边缘点应尽可能少
- 精确定位:检测出的边缘点的位置应与实际边缘中心尽可能接近
- 单次响应:每个边缘位置只能标识一次
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. 梯度计算:
计算图像中每像素的梯度幅值和方向,主要分以下两步:
- 用Sobel算子分别检测x方向和y方向的边缘
- 计算梯度的幅值和方向。为了简化起见,梯度方向取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算法耗费的时间也比较长。