图像边缘检测之索贝尔算子、拉普拉斯算子、canny算子
1.Sobel算子
Sobel 算子又被叫做一阶微分算子,是一种差分近似微分的方法。其中根据核的不同又有水平和竖直梯度之分,对应着不同的卷积核。API如下:
cv::Sobel (
InputArray Src // 输入图像
OutputArray dst// 输出图像,大小与输入图像一致
int depth // 输出图像深度.
Int dx. // X方向,几阶导数
int dy // Y方向,几阶导数.
int ksize, SOBEL算子kernel大小,必须是1、3、5、7、
double scale = 1
double delta = 0
int borderType = BORDER_DEFAULT
)
需要注意的是输出图像的深度不应该和输入图像一样,因为Sobel算子的计算过程中会产生溢出,例如存在负值。因此在计算时,输入图像深度为CV_8U时候,输出图像深度最好是CV_16S,产生的负值后期在通过convertscaleAbs()函数调整至正值,从而保留完整的边缘计算信息。
求取导数的近似值,kernel=3时不是很准确,OpenCV使用改进版本Scharr函数。
2.Laplace算子
在二阶导数的时候,最大变化处的值为零即边缘是零值。通过二阶导数计算,依据此理论我们可以计算图像二阶导数,提取边缘。API如下
Laplacian(
InputArray src,
OutputArray dst,
int depth, //深度CV_16S
int kisze, // 3
double scale = 1,
double delta =0.0,
int borderType = 4
)
3.canny提取边缘
步骤:
高斯模糊 - GaussianBlur
灰度转换 - cvtColor
计算梯度 – Sobel/Scharr
非最大信号抑制
高低阈值输出二值图像
其中非最大信号抑制说明:
非极大值抑制(Non-Maximum Suppression,NMS),顾名思义就是抑制不是极大值的元素,可以理解为局部最大搜索。这个局部代表的是一个邻域,邻域有两个参数可变,一是邻域的维数,二是邻域的大小。这里不讨论通用的NMS算法(参考论文《Efficient Non-Maximum Suppression》对1维和2维数据的NMS实现),而是用于目标检测中提取分数最高的窗口的。例如在行人检测中,滑动窗口经提取特征,经分类器分类识别后,每个窗口都会得到一个分数。但是滑动窗口会导致很多窗口与其他窗口存在包含或者大部分交叉的情况。这时就需要用到NMS来选取那些邻域里分数最高(是行人的概率最大),并且抑制那些分数低的窗口。
在canny算法中体现为
1、将当前像素的边缘强度与正梯度方向和负梯度方向上的像素的边缘强度进行比较。
2、如果当前像素的边缘强度与具有相同方向的掩模中的其他像素相比是最大的(即,指向y方向的像素,则将其与其上方和下方的像素进行比较,垂直轴),该值将被保留。否则,该值将被抑制。
也就是选取领域内不同方向的梯度最大值处作为保留点。
高低阈值输出二值图像 :
T1, T2为阈值,凡是高于T2的都保留,凡是小于T1都丢弃,从高于T2的像素出发,凡是大于T1而且相互连接的,都保留。最终得到一个输出二值图像。推荐的高低阈值比值为 T2: T1 = 3:1/2:1其中T2为高阈值,T1为低阈值
最后两步操作使边缘变薄变清晰。API如下:
Canny(
InputArray src, // 8-bit的输入图像
OutputArray edges,// 输出边缘图像, 一般都是二值图像,背景是黑色
double threshold1,// 低阈值,常取高阈值的1/2或者1/3
double threshold2,// 高阈值
int aptertureSize,// Soble算子的size,通常3x3,取值3
bool L2gradient // 选择 true表示是L2来归一化,否则用L1归一化
)
具体的数学和信号原理当然更加复杂,不做阐述。进行代码演示:
/*图像边缘检测*/
#include"pch.h"
#include<opencv2/opencv.hpp>
#include<iostream>
#include<math.h>
using namespace std;
using namespace cv;
Mat src, gray_src, sobel_xgrad,sobel_ygrad,sobel_dst, lapace_dst, canny_dst;
int threshold_low = 50;
int threshold_max = 255;
void canny_demo(int, void*);
void threshold_demo(int, void*);
int main(int argc, char** argv) {
src = imread("test.png");
if (!src.data) {
printf("没找到图像");
return-1;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
GaussianBlur(src, src, Size(3, 3), 0, 0);
cvtColor(src, gray_src, CV_BGR2GRAY);
namedWindow("gray", CV_WINDOW_AUTOSIZE);
imshow("gray", gray_src);
/*Sobel算子*/
Sobel(gray_src, sobel_xgrad, CV_16S,1,0,3);
Sobel(gray_src, sobel_ygrad, CV_16S, 0,1,3);
convertScaleAbs(sobel_xgrad, sobel_xgrad);
convertScaleAbs(sobel_ygrad, sobel_ygrad);
namedWindow("sobel_xgrad", CV_WINDOW_AUTOSIZE);
namedWindow("sobel_ygrad", CV_WINDOW_AUTOSIZE);
imshow("sobel_xgrad", sobel_xgrad);
imshow("sobel_ygrad", sobel_ygrad);
addWeighted(sobel_xgrad, 0.5, sobel_ygrad, 0.5, 0, sobel_dst);
namedWindow("sobel_output", CV_WINDOW_AUTOSIZE);
imshow("sobel_output", sobel_dst);
/*遍历计算梯度值,这里截断0-255使图像变亮些*/
Mat sobel_xygrad;
sobel_xygrad.create(sobel_dst.size(), CV_8UC1);
int width = sobel_xgrad.cols;
int height = sobel_xgrad.rows;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int xg = sobel_xgrad.at<uchar>(row, col);
int yg = sobel_ygrad.at<uchar>(row, col);
int xy = xg + yg;
sobel_xygrad.at<uchar>(row, col) = saturate_cast<uchar>(xy);//限制范围0-255
}
}
namedWindow("sobel_xygrad", CV_WINDOW_AUTOSIZE);
imshow("sobel_xygrad", sobel_xygrad);
/*laplace算子*/
Laplacian(gray_src, lapace_dst, CV_16S, 5);
convertScaleAbs(lapace_dst, lapace_dst);
namedWindow("lapace_dst", CV_WINDOW_AUTOSIZE);
imshow("lapace_dst", lapace_dst);
/*canny算法*/
namedWindow("canny", CV_WINDOW_AUTOSIZE);
createTrackbar("cnnay", "canny", &threshold_low, threshold_max, canny_demo);
canny_demo(0, 0);
waitKey(0);
return 0;
}
void canny_demo(int, void*) {
Canny(gray_src, canny_dst, threshold_low, threshold_low*2);
imshow("canny", canny_dst);
}
结果如下图: