基于canny算子的边缘检测
- 边缘检测是基于灰度突变来分割图像的常用方法。Canny边缘检测于1986年由JOHN CANNY首次在论文《A Computational Approach to Edge Detection》中提出,就此拉开了Canny边缘检测算法的序幕。
- canny边缘检测算法处理5个步骤
- 使用滤波算法,以平滑图像,滤除噪声,本博客使用的是高斯滤波。
- 计算图像中每个像素点的梯度强度和方向。
- 应用非极大值(NMS)抑制,以消除边缘检测带来的杂散响应。
- 应用双阈值(DT)检测来确定真实的和潜在的边缘。
- 通过抑制孤立的弱边缘最终完成边缘检测
5个步骤的详细解析
- 1)高斯滤波
- a.使用高斯滤波器与图像进行卷积,平滑图像,去除噪声。高斯滤波器核大小为s*s(s为奇数)。滤波器中心位置为k, 高斯滤波器核的产生式:
- b.归一化高斯滤波器核
- c.计算卷积
- a.使用高斯滤波器与图像进行卷积,平滑图像,去除噪声。高斯滤波器核大小为s*s(s为奇数)。滤波器中心位置为k, 高斯滤波器核的产生式:
2)计算梯度强度和方向
- 利用Sobel算子返回水平Gx和垂直Gy的一阶导数值。以此用来计算梯度强度G和方向thead。
3) 应用非极大值(Non-Maximum Suppression)抑制,以消除边缘检测带来的杂散响应
非极大值抑制是一种边缘稀疏技术,非极大值抑制的作用在于“瘦”边。对图像进行梯度计算后,仅仅基于梯度值提取的边缘仍然很模糊。对于标准3,对边缘有且应当只有一个准确的响应。而非极大值抑制则可以帮助将局部最大值之外的所有梯度值抑制为0,对梯度图像中每个像素进行非极大值抑制的算法是:1) 将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较。
2) 如果当前像素的梯度强度与另外两个像素相比最大,则该像素点保留为边缘点, 否则该像素点将被抑制。
通常为了更加精确的计算,在跨越梯度方向的两个相邻像素之间使用线性插值来得到要 比较的像素梯度,现举例如下:
- 4)双阈值检测
- 在施加非极大值抑制之后,剩余的像素可以更准确地表示图像中的实际边缘。然而,仍然存在由于噪声和颜色变化引起的一些边缘像素。为了解决这些杂散响应,必须用弱梯度值过滤边缘像素,并保留具有高梯度值的边缘像素,可以通过选择高低阈值来实现。如果边缘像素的梯度值高于高阈值,则将其标记为强边缘像素;如果边缘像素的梯度值小于高阈值并且大于低阈值,则将其标记为弱边缘像素;如果边缘像素的梯度值小于低阈值,则会被抑制。阈值的选择取决于给定输入图像的内容。
- 高低阈值分别为heightThes 和 lowThes.
确定阈值方法有:全局阈值、Otsu等方法。
- 5)到目前为止,被划分为强边缘的像素点已经被确定为边缘,因为它们是从图像中的真实 边缘中提取出来的。然而,对于弱边缘像素,将会有一些争论,因为这些像素可以从真实边缘提取也可以是因噪声或颜色变化引起的。为了获得准确的结果,应该抑制由后者引起的弱边缘。通常,由真实边缘引起的弱边缘像素将连接到强边缘像素,而噪声响应未连接。为了跟踪边缘连接,通过查看弱边缘像素及其8个邻域像素,只要其中一个为强边缘像素,则该弱边缘点就可以保留为真实的边缘。
- 1)高斯滤波
抑制孤立边缘点的伪代码描述如下:
源码解析
/**
* @brief cannyEdgeDetection 基础canny算子边缘检测
* @param img 原图
* @param result 结果图片
* @param guaSize 高斯核大小 奇数
* @param hightThres 高阈值 0-1
* @param lowThres 低阈值 0-1
* @return null
* @note code by jmu-stu jsc 2018-06
* 1.高斯滤波
* 2.计算梯度强度和方向
* 3.非极大值抑制
* 4.双阈值检测
* 5.抑制孤立低阈值点
*/
void cannyEdgeDetection(cv::Mat img, cv::Mat &result, int guaSize, double hightThres, double lowThres ){
// 高斯滤波
cv::Rect rect; // IOU区域
cv::Mat filterImg = cv::Mat::zeros(img.rows, img.cols, CV_64FC1);
img.convertTo(img, CV_64FC1);
result = cv::Mat::zeros(img.rows, img.cols, CV_64FC1);
int guassCenter = guaSize / 2; // 高斯核的中心 // (2* guassKernelSize +1) * (2*guassKernelSize+1)高斯核大小
double sigma = 1; // 方差大小
cv::Mat guassKernel = cv::Mat::zeros(guaSize, guaSize, CV_64FC1);
for(int i = 0; i< guaSize; i++){
for(int j = 0; j < guaSize; j++){
guassKernel.at<double>(i, j) = (1.0 / (2.0 * pi * sigma * sigma)) *
(double)exp(-(((double)pow((i - (guassCenter+ 1)),2) + (double)pow((j - (guassCenter + 1)),2)) / (2.0*sigma*sigma)));
// std::cout<<guassKernel.at<double>(i, j) << " ";
}
// std::cout<<std::endl;
}
cv::Scalar sumValueScalar = cv::sum(guassKernel);
double sum = sumValueScalar.val[0];
std::cout<<sum<<std::endl;
guassKernel = guassKernel / sum;
// for(int i = 0; i< guaSize; i++){
// for(int j = 0; j < guaSize; j++){
// std::cout<<guassKernel.at<double>(i, j) << " ";
// }
// std::cout<<std::endl;
// }
for(int i = guassCenter; i< img.rows - guassCenter; i++){
for(int j = guassCenter; j < img.cols - guassCenter; j++){
rect.x = j - guassCenter;
rect.y = i - guassCenter;
rect.width = guaSize;
rect.height = guaSize;
filterImg.at<double>(i, j) = cv::sum(guassKernel.mul(img(rect))).val[0];
// std::cout<<filterImg.at<double>(i,j) << " ";
}
// std::cout<<std::endl;
}
cv::Mat guassResult;
filterImg.convertTo(guassResult, CV_8UC1);
cv::imshow("guass-result", guassResult);
// std::cout<<cv::sum(guassKernel).val[0]<<std::endl;
// 计算梯度,用sobel算子
cv::Mat gradX = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // 水平梯度
cv::Mat gradY = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // 垂直梯度
cv::Mat grad = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // 梯度幅值
cv::Mat thead = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // 梯度角度
cv::Mat locateGrad = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); //区域
// x方向的sobel算子
cv::Mat Sx = (cv::Mat_<double>(3,3) << -1,0,1,
-2,0,2,
-1,0,1
);
// y方向sobel算子
cv::Mat Sy = (cv::Mat_<double>(3,3) << 1,2,1,
0,0,0,
-1,-2,-1
);
// 计算梯度赋值和角度
for(int i = 1 ; i < img.rows-1; i++ ){
for( int j = 1; j<img.cols-1; j++){
// 卷积区域 3*3
rect.x = j-1;
rect.y = i-1;
rect.width = 3;
rect.height = 3;
cv::Mat rectImg = cv::Mat::zeros(3,3,CV_64FC1);
filterImg(rect).copyTo(rectImg);
// 梯度和角度
gradX.at<double>(i,j) += cv::sum(rectImg.mul(Sx)).val[0];
gradY.at<double>(i,j) += cv::sum(rectImg.mul(Sy)).val[0];
grad.at<double>(i,j) = sqrt(pow(gradX.at<double>(i,j),2) + pow(gradY.at<double>(i,j),2));
thead.at<double>(i, j) = atan(gradY.at<double>(i,j)/gradX.at<double>(i,j));
// 设置四个区域
if(0 <= thead.at<double>(i,j) <= (pi/4.0)){
locateGrad.at<double>(i, j) = 0;
}
else if(pi/4.0 < thead.at<double>(i,j) <= (pi/2.0)){
locateGrad.at<double>(i, j) = 1;
}
else if(-pi/2.0 <= thead.at<double>(i,j) <= (-pi/4.0)){
locateGrad.at<double>(i, j) = 2;
}
else if(-pi/4.0 < thead.at<double>(i,j) < 0){
locateGrad.at<double>(i, j) = 3;
}
}
}
// debug
cv::Mat tempGrad;
grad.convertTo(tempGrad, CV_8UC1);
imshow("grad", tempGrad);
// 梯度归一化
double gradMax;
cv::minMaxLoc(grad, &gradMax); // 求最大值
if (gradMax != 0){
grad = grad / gradMax;
}
// debug
cv::Mat tempGradN;
grad.convertTo(tempGradN, CV_8UC1);
imshow("gradN", tempGradN);
// 双阈值确定
cv::Mat caculateValue = cv::Mat::zeros(img.rows, img.cols, CV_64FC1); // grad变成一维
cv::resize(grad, caculateValue,cv::Size(1,(grad.rows * grad.cols)));
// caculateValue.convertTo(caculateValue, CV_64FC1);
cv::sort(caculateValue, caculateValue,CV_SORT_EVERY_COLUMN + CV_SORT_ASCENDING); // 升序
long long highIndex = img.rows * img.cols * hightThres;
double highValue = caculateValue.at<double>(highIndex, 0) ; // 最大阈值
// debug
// std::cout<< "highValue: "<<highValue<<" "<< caculateValue.cols << " "<<highIndex<< std::endl;
double lowValue = highValue * lowThres; // 最小阈值
// 3.非极大值抑制, 采用线性插值
for(int i = 1 ; i < img.rows-1; i++ ){
for( int j = 1; j<img.cols-1; j++){
// 八个方位
double N = grad.at<double>(i-1, j);
double NE = grad.at<double>(i-1, j+1);
double E = grad.at<double>(i, j+1);
double SE = grad.at<double>(i+1, j+1);
double S = grad.at<double>(i+1, j);
double SW = grad.at<double>(i-1, j-1);
double W = grad.at<double>(i, j-1);
double NW = grad.at<double>(i -1, j -1);
// 区域判断,线性插值处理
double tanThead; // tan角度
double Gp1; // 两个方向的梯度强度
double Gp2;
// 求角度,绝对值
tanThead = abs(tan(thead.at<double>(i,j)));
switch ((int)locateGrad.at<double>(i,j)) {
case 0:
Gp1 = (1- tanThead) * E + tanThead * NE;
Gp2 = (1- tanThead) * W + tanThead * SW;
break;
case 1:
Gp1 = (1- tanThead) * N + tanThead * NE;
Gp2 = (1- tanThead) * S + tanThead * SW;
break;
case 2:
Gp1 = (1- tanThead) * N + tanThead * NW;
Gp2 = (1- tanThead) * S + tanThead * SE;
break;
case 3:
Gp1 = (1- tanThead) * W + tanThead *NW;
Gp2 = (1- tanThead) * E + tanThead *SE;
break;
default:
break;
}
// NMS -非极大值抑制和双阈值检测
if(grad.at<double>(i, j) >= Gp1 && grad.at<double>(i, j) >= Gp2){
//双阈值检测
if(grad.at<double>(i, j) >= highValue){
grad.at<double>(i, j) = highValue;
result.at<double>(i, j) = 255;
}
else if(grad.at<double>(i, j) < lowValue){
grad.at<double>(i, j) = 0;
}
else{
grad.at<double>(i, j) = lowValue;
}
}
else{
grad.at<double>(i, j) = 0;
}
}
}
// NMS 和算阈值检测后的梯度图
cv::Mat tempGradNMS;
grad.convertTo(tempGradNMS, CV_8UC1);
imshow("gradNMS", tempGradNMS);
// 4.抑制孤立低阈值点 3*3. 找到高阈值就255
for(int i = 1 ; i < img.rows-1; i++ ){
for( int j = 1; j<img.cols-1; j++){
if(grad.at<double>(i, j) == lowValue){
// 3*3区域找强梯度
rect.x = j-1;
rect.y = i-1;
rect.width = 3;
rect.height = 3;
for(int i1 = 0; i1 < 3; i1++){
for(int j1 = 0; j1<3; j1++){
if(grad(rect).at<double>(i1,j1) == highValue){
result.at<double>(i, j) = 255;
std::cout<<result.at<double>(i, j);
break;
}
}
}
}
}
}
// 结果
result.convertTo(result, CV_8UC1);
imshow("result", result);
}
参考链接
[1]https://www.cnblogs.com/techyan1990/p/7291771.html
[2]https://wenku.baidu.com/view/607820c16137ee06eff9187b.html (阈值确定参考这个)