常见的边缘检测Mask
区分图像中目标(前景)与背景中的方法称之为图像分割,传统中图像分割的方法包括:1. 阈值处理 2. 边缘检测 3. 区域生长。其中阈值处理算法使用较为广泛,但面对复杂的图像时,使用阈值处理难以准确分割,此时就需要使用边缘检测算法或区域生长算法。
边缘是两个不同图像区域之间的边界点所形成的。在实际的应用中,图像的边缘往往被经以为图像局部强度变化最剧烈的部分。这就意味着这些部分区域的梯度很大,因此可以通过求梯度变化剧烈的部分间接的定位边缘位置。
与连续函数求梯度的方式不同,因为图像是以矩阵的方式进行存储,所以我们借助模板对原图像进行卷积运算,来达到相似的效果。对图像求梯度就转化成了利用模板对原图像进行卷积的过程。
Sobel算子
Sobel算子是边缘检测算法中应用最为广泛的一种算法,其优点在于方法简单,处理速度快,且得到的边缘光滑、连续。
Sobel算子模板:
对Sobel 方向的模板源码实现:
#include <iostream>
#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp" // 转灰度用
using namespace std;
using namespace cv;
int main(int argc,char** argv){
Mat src = imread("./imgSrc/2.jpg");
if(src.empty()){
cerr<<"Load image fail"<<endl;
return -1;
}
namedWindow("inputImage",WINDOW_AUTOSIZE);
imshow("inputImage",src);
Size imageSize = src.size(); // 读取图像的尺寸
Mat grayImage; // 灰度图像
cvtColor(src,grayImage,COLOR_BGR2GRAY);
Mat dst_my = Mat::zeros(imageSize,CV_8UC1); // 输出的结果图像
// 取图像每一行的头指针进行访问,为了不考虑边界,我直接从第二行第二列开始计算了
for (int row = 1; row < imageSize.height - 1; ++row) {
uchar *up_row = grayImage.ptr(row - 1);
uchar *current_row = grayImage.ptr(row);
uchar *down_row = grayImage.ptr(row + 1);
for (int col = 1; col < imageSize.width - 1; ++col) {
dst_my.at<uchar>(row, col) = saturate_cast<uchar>(
up_row[col - 1] * -1 + up_row[col + 1] * 1 + current_row[col - 1] * -2 + current_row[col + 1] * 2 +
down_row[col] * -1 + down_row[col] * 1); // 实现上面的公式,saturate_cast函数防止出现大于8U的值出现,将最大值限制到 255
}
}
namedWindow("outputImage",WINDOW_AUTOSIZE);
imshow("outputImage",dst_my);
waitKey();
cout<<"Hello OpenCV World"<<endl;
return 0;
}
结果:
当然,自己重复造轮子是没有什么意义的,可以使用OpenCV中的Api实现自己定义卷积模板:
//
// Created by lucas on 2020/8/29.
//
#include <iostream>
#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
using namespace std;
using namespace cv;
int main(int argc,char** argv){
Mat src = imread("./imgSrc/2.jpg");
if(src.empty()){
cerr<<"Load image fail"<<endl;
return -1;
}
namedWindow("inputImage",WINDOW_AUTOSIZE);
imshow("inputImage",src);
Size imageSize = src.size(); // 读取图像的尺寸
Mat grayImage; // 灰度图像
cvtColor(src,grayImage,COLOR_BGR2GRAY);
Mat dst_my = Mat::zeros(imageSize,CV_8UC1); // 输出的结果图像
Mat dst_filter2D = Mat::zeros(imageSize,CV_8UC1); // 输出的结果图像
//
for (int row = 1; row < imageSize.height - 1; ++row) {
uchar *up_row = grayImage.ptr(row - 1);
uchar *current_row = grayImage.ptr(row);
uchar *down_row = grayImage.ptr(row + 1);
for (int col = 1; col < imageSize.width - 1; ++col) {
dst_my.at<uchar>(row, col) = saturate_cast<uchar>((
up_row[col - 1] * -1 + up_row[col + 1] * 1 + current_row[col - 1] * -2 + current_row[col + 1] * 2 +
down_row[col] * -1 + down_row[col] * 1));
}
}
Mat kernel_Gx = (Mat_<int>(3,3)<<-1,0,1,-2,0,2,-1,0,1);
filter2D(grayImage,dst_filter2D,-1,kernel_Gx,Point(-1,-1),0,BORDER_DEFAULT);
namedWindow("outputImage_filter2d",WINDOW_AUTOSIZE);
imshow("outputImage_filter2d",dst_filter2D);
namedWindow("outputImage",WINDOW_AUTOSIZE);
imshow("outputImage",dst_my);
waitKey();
cout<<"Hello OpenCV World"<<endl;
return 0;
}
结果:
如果只对x方向做差分,可以使用另一个Api-getDerivKernels + sepFilter2D函数实现:
Mat kernel_Gx,kernel_Gy;
getDerivKernels(kernel_Gx,kernel_Gy,1,0,3,false,BORDER_DEFAULT);
sepFilter2D(grayImage,dst_filter2D,-1,kernel_Gx,kernel_Gy,Point(-1,-1),0,BORDER_CONSTANT);
结果:
注意outputImage_filter2d窗口与左边窗口交界处的白线,那是由于在sepFilter2D中我设置了边界为BORDER_CONSTANT.
有了API的加成,定义其他滤波模板就十分容易。
Scharr模板(严格来说是滤波器,不是算子),与Sobel相比只是没有ksize的大小。
Laplace
Robert
结果(调用的API):
#include <iostream>
#include <string>
#include <vector>
#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
//#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char **argv) {
Mat src = imread("imgSrc/2.jpg");
if (src.empty()) {
cerr << "Error in Src" << endl;
return -1;
}
namedWindow("inputImage", WINDOW_AUTOSIZE);
imshow("inputImage", src);
Size imageSize = src.size();
int imageChannel = src.channels();
Mat grayImage = Mat(imageSize, CV_8UC1);
cvtColor(src, grayImage, COLOR_BGR2GRAY);
Mat dst_my = Mat::zeros(imageSize, CV_8UC1);// 输出图像
Mat dst_sobel = Mat::zeros(imageSize, CV_8UC1);// 输出图像
// 对图像进行模糊处理,Sobel处理
for (int row = 1; row < imageSize.height - 1; ++row) {
uchar *up_row = grayImage.ptr(row - 1);
uchar *current_row = grayImage.ptr(row);
uchar *down_row = grayImage.ptr(row + 1);
for (int col = 1; col < imageSize.width - 1; ++col) {
dst_my.at<uchar>(row, col) = saturate_cast<uchar>(
up_row[col - 1] * -1 + up_row[col + 1] * 1 + current_row[col - 1] * -2 + current_row[col + 1] * 2 +
down_row[col] * -1 + down_row[col] * 1);
}
}
Sobel(grayImage,dst_sobel,-1,1,0,3,1,0,BORDER_DEFAULT); // 已经包含了高斯平滑和差分处理,因此对噪声不敏感,这里只对x方向求一介差分
namedWindow("源码实现",WINDOW_AUTOSIZE);
imshow("源码实现",dst_my);
namedWindow("Sobel_result",WINDOW_AUTOSIZE);
imshow("Sobel_result",dst_sobel);
Mat dst_scharr = Mat::zeros(imageSize, CV_8UC1);// 输出图像
Scharr(grayImage,dst_scharr,-1,1,0,1,0,BORDER_DEFAULT); // 使用Scharr算子计算一阶差分,等同于𝚂𝚘𝚋𝚎𝚕(𝚜𝚛𝚌, 𝚍𝚜𝚝, 𝚍𝚍𝚎𝚙𝚝𝚑, 𝚍𝚡, 𝚍𝚢, 𝙲𝚅_𝚂𝙲𝙷𝙰𝚁𝚁, 𝚜𝚌𝚊𝚕𝚎, 𝚍𝚎𝚕𝚝𝚊, 𝚋𝚘𝚛𝚍𝚎𝚛𝚃𝚢𝚙𝚎).
namedWindow("Scharr_result",WINDOW_AUTOSIZE);
imshow("Scharr_result",dst_scharr);
Mat dst_laplace = Mat::zeros(imageSize, CV_8UC1);// 输出图像
Laplacian(grayImage,dst_laplace,-1,1,1,0,BORDER_DEFAULT);
namedWindow("Laplace_result",WINDOW_AUTOSIZE);
imshow("Laplace_result",dst_laplace);
Mat dst_canny = Mat::zeros(imageSize, CV_8UC1);// 输出图像
Canny(grayImage,dst_canny,20,200,3,false);
namedWindow("Canny_result",WINDOW_AUTOSIZE);
imshow("Canny_result",dst_canny);
Mat dst_robert = Mat::zeros(imageSize, CV_8UC1);// 输出图像
Mat kernel_robert = (Mat_<int>(2,2)<<1,0,0,-1);
filter2D(grayImage,dst_robert,-1,kernel_robert,Point(-1,-1),0,BORDER_DEFAULT);
namedWindow("Robert_result",WINDOW_AUTOSIZE);
imshow("Robert_result",dst_robert);
Mat dst_My_struct = Mat::zeros(imageSize, CV_8UC1);// 输出图像
Mat MY_struct = (Mat_<int>(3,3)<< -1,0,1,-2,0,2,-1,0,1);
filter2D(grayImage,dst_My_struct,-1,MY_struct,Point(-1,-1),0,BORDER_DEFAULT);
namedWindow("My_struct",WINDOW_AUTOSIZE);
imshow("My_struct",dst_My_struct);
waitKey();
return 0;
}
Canny的算法就比较麻烦:包含了 非极大值抑制,滞后阈值处理算法×××××