、功能简介及其运行
(一)、功能:该程序实现了canny算子求图片的边缘,其中主要包括七大部分:
1、对传入的彩色图片二值化
2、对二值化后的图片进行高斯滤波
3、使用sobel算子对滤波之后的图形分别求x,y方向上的梯度
4、计算出梯度幅值
5、对梯度图像做非极大抑制
6、对非极大抑制后的图像做双阈值处理
7、对双阈值处理后的图像进行连接操作(处理阈值中间的点)
(二)、运行:注意要在release文件夹下运行exe
运行程序后终端会显示所用到的sobel算子矩阵和高斯滤波矩阵,同时会弹出多张图片,他包括:
1、原图片:Lena.jpg
2、二值化后的图片:gray.jpg
3、高斯滤波后的图片:guss,jpg
4、x,y方向梯度的图片:gradit_x.jpg gradint_y.jpg
5、梯度图片:gradint.jpg
6、非极大抑制后的图片:afterNms.jpg
7、双阈值后的图片:afterDT.jpg
8、连接操作后的图片(即最终的canny算法图片)canny.jpg
9、使用opencv自带函数得到的canny算子图片(用于对比)
弹出的同时,会将这些图片保存在当前路径的文件下,命名为上面的命名。
二、开发环境
操作系统:windows
Opencv版本:2.4
编译器:vs2015
语言:c++
请助教运行release下的exe。
三、程序思路
大体思路其实与上面七大部分相同,这里做一个概述,并说明大体实现,具体通过这个简单流程图来说明(流程图viso文件附在压缩包内))
一、具体实现
(1)图像灰度化:
图像灰度画函数:Mat getGray(Mat &image)
作用:返回一张彩色图片的灰度图片
实现方式:才用标准的转化公式:Gray = R*0.299 + G*0.587 + B*0.114
循环的取出每一个像素,对每一个彩色像素做该运算就可得到灰度图片。
(2)获取高斯核
图像灰度画函数:double **getGussKernel(int size, double sigma)
作用:返回一个二维double类型指针,里面是对应size和sigma的高斯卷积核
实现方式:通过查阅资料和博客我们知道高斯卷积核公式:
由这个公式,我们可以容易写出一下核心代码:
最后记得要做归一化,之后返回指针g即可。
(3)卷积函数的两种实现
现在我们得到了一个高斯核,要做卷积了,由于我们知道在求梯度图片的时候同样要用到卷积,所以我们写一个卷积函数。
卷积函数:Mat Convolution1(Mat &image, double **kernel,int size)
Mat Convolution2(Mat &image, double **kernel,int size)
参数:带卷积图像 卷积核 卷积核大小
这两种方法大体上相同,就是在对边缘的处理上一个视边缘的无效邻域视为边缘本身;一个视为0,根据实验效果来看,两者没有太大差别(边缘本身就看不出来)
作用:将图像与卷积核做卷积处理并返回卷积后的图像(不改变原图)
具体实现:采用循环的方式,对每一个像素都对他的八邻域做分别做乘积后相加。需要注意的是不同的边缘控制代码有一些不同。
(需要注意的是,为了方便计算,我们事先把卷积核转化为1维,这样省了不少事,这个主要感谢http://blog.csdn.net/dcrmg/article/details/52344902这篇博客上的灵感,不然处理起来有点混乱。)
最后返回Mat 图像就是卷积之后的图像。
有了这个工具,我们只需要使用这个工具与步骤而获得的高斯核卷积就可以得到高斯滤波后的图像。
(4)获得梯度图像
这里分三步:
(1)获得x方向的梯度图像
方法:使用sobel算子:
sobel_x[3][3] = { { -1,-2,-1 } ,{ 0,0,0 } ,{ 1,2,1 } };
与我们之前写好的卷积函数做卷积即可。
(2)获得y方向上的梯度图像
方法:使用sobel算子:
sobel_y[3][3] = { { -1,0,1 } ,{ -2,0,2 } ,{ -1,0,1 } };
与我们之前写好的卷积函数做卷积即可。
(3)获得综合梯度的幅值
Sobel函数:
Mat Sobel(Mat &image_x,Mat &image_y)
参数:x方向梯度图像 y方向梯度图像
作用:返回梯度图像
具体实现:根据公式
可以知道新图片的每一像素点都等于x,y梯度方向上的欧氏距离,由此思路可以清晰的写出代码:
最后返回Mat图像即可
(5)非极大抑制
非极大值抑制就是要消除梯度方向上非极大值的点,避免边缘非常“厚”的问题,是canny算法的精髓之一。
Nms函数:
Mat Nms(Mat &image, Mat &image_x, Mat &image_y)
参数:梯度图像 x方向梯度图像 y方向梯度图像
作用:通过计算梯度方向和插值对图像进行非极大抑制
具体实现:我们知道非极大抑制的难点就在于找和当前点梯度方向相同的相邻点,由于很有可能该点处于两个像素的中间,即实际该点没有像素值,但是梯度方向指向该值,所以我们在这里使用插值的方法。通俗的解释就是
假设这里我们以c点为中心,那么c点的梯度方向的邻域点可能落在上图中四个小正方体的外接边上的八个边上,而我们只知道八个顶点的信息,为了取得亚像素的信息,我们采用线性插值的方法,插值点有四种位置,我们需要根据梯度方向的不同来确定具体的位置。根据线性插值的一般公式:
M(dtmp1)=w*M(g2)+(1-w)*M(g1)
其中w=|x方向梯度|/|y方向梯度|(靠x方向时)
或w=|y方向梯度|/|x方向梯度|(靠y方向时)
基于这种考虑,我们通过判断x与y的绝对值大小来判断c2与c4的位置
通过比较x与y是否同号来确定c1,c3的位置。
其他方向同理
在找到其梯度方向上的插值点之后,剩下的就是要与这两个插值点进行比较,若当前点比他们都大,说明当前点为极大值点,保留。否则就置成0。代码实现较简单使用简单的判断即可。(这里没有使用梯度角,而是直接用了x,y方向上的梯度图像来比较主要是为了理解方便,而且梯度角非常容易出错)
(6)双阈值
到上一步,我们得到了大致的边缘图片,但是这幅图片边缘处太密,我们想通过双阈值的方法来处理图片,突出灰度值较大的边缘部分,将灰度值较小的部分置为0.
双阈值函数:
Mat DoubleThreshold(Mat &image, double high_threshold, double low_threshold)
参数: Nms后的图像 高阈值 低阈值
作用:输出对应双阈值之后的图像(没有对中间阈值部分进行连接)
连接函数:
Mat Link1(Mat &image)
作用:对执行完DoubleThreshold函数的图像的中间部分做中间的连接处理,返回最终的边缘函数。
具体实现:实现分两步。
首先第一步很简单,就是遍历图像的每一个像素,并对他的值进行判断,若该值小于低阈值就为0;若高于高阈值就置为255。对于中间的点先不理他,在后面做连接的时候再处理。
第二步 连接:在这里实现了两种连接。后面在实验结果分析会比较哪种比较好。第一种连接方式的原则很简单:
遍历一个经过DoubleThreshold后的图像,对于每一个点再去遍历他的八连通邻域,若其八连通邻域全是0,说明这是一个孤立的边缘点,可以将其去掉;若其八邻域内有非0的点,不管是255还是处于中间的点,则说明这个点可以作为边缘连接点,就将其置为255。这样循环遍历,然后返回。
核心程序截图如下:
第二种才用递归的方式,称其为Link2。
递归的主要特点使每次从当前刚刚变为边缘的图像开始深度优先搜索,这样有不会出现“误伤”的情况。具体实现分为递归函数和主控函数,核心递归函数如图所示:
最终canny为
opencv自带的为
最后附上源代码:
//#include <stdafx.h> #include "cv.h" #include "cxcore.h" #include "highgui.h" #include<cstring> #include<math.h> #include<iostream> #define PI 3.1415926 using namespace cv; using namespace std; char path[20]; double sobel_y[3][3] = { { -1,0,1 } ,{ -2,0,2 } ,{ -1,0,1 } };//sobel算子 double sobel_x[3][3] = { { -1,-2,-1 } ,{ 0,0,0 } ,{ 1,2,1 } }; int xNum[8] = { 1,1,0,-1,-1,-1,0,1 }; int yNum[8] = { 0,1,1,1,0,-1,-1,-1 };//方向 /*返回该函数的灰度值*/ Mat getGray(Mat &image) { if (!image.data || image.channels() != 3) { printf("图片属性错误"); return image; } Mat gray_image = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 //Mat gray_image(2, image.size, CV_8UC1, Scalar::all(0)); int image_step = image.step; for (int i = 0; i < gray_image.rows; i++) { for (int j = 0; j < gray_image.cols; j++) { int base = image_step*i;//基地址 gray_image.data[i*gray_image.step + j] = 0.114*image.data[base + j * 3 + 0] + 0.587*image.data[base + j * 3 + 1] + 0.299*image.data[base + j * 3 + 2]; } } return gray_image; } /*获取尺寸为size 标准差为sigma的高斯核*/ double **getGussKernel(int size, double sigma) { double temp = 1 / (2 * PI*sigma*sigma); int x0 = size / 2; int y0 = size / 2; double sum = 0;//归一化做准备 double **g = new double *[size]; for (int i =0; i < size; i++) { g[i] = new double[size]; } for (int x = 0; x < size; x++) { for (int y = 0; y < size; y++) { g[x][y] = temp*exp(-((x - x0)*(x - x0) + (y - y0)*(y - y0)) / (2 * sigma*sigma)); sum += g[x][y]; } } for (int x = 0; x < size; x++) { for (int y = 0; y < size; y++) { g[x][y] = g[x][y] / sum; cout << g[x][y] << " "; } cout << endl << endl; } return g; } /*对图片做kernel卷积 边缘处理方案一*/ Mat Convolution1(Mat &image, double **kernel,int size) { Mat after_convolution = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 int count = 0; double kernel_temp[100]; for (int i = 0; i<size*size; i++) { kernel_temp[i] = 0; //赋初值,空间分配 } for (int i = 0; i < size; i++)//将核转化为一维,方便卷积*** { for (int j = 0; j < size; j++) { kernel_temp[count] = kernel [i][j]; count++; } } /*边缘处理才用边缘的点代替值*/ for (int i = 0; i < image.rows; i++) { for (int j = 0; j < image.cols; j++) { int k = 0;//卷积核的索引 for (int x = -size / 2; x <=size / 2; x++) { for (int y = -size / 2; y <= size / 2; y++) { int r = i + x; int c = j + y; r = r < 0 ? 0 : r;//处理顶部边缘 r = r >= image.rows ? image.rows - 1 : r;//处理底部边缘 c = c < 0 ? 0 : c; c = c >= image.cols ? image.cols - 1 : c; after_convolution.at<uchar>(i, j) += kernel_temp[k] * image.at<uchar>(r, c); k++; } } } } return after_convolution; } Mat Convolution2(Mat &image, double **kernel, int size) { Mat after_convolution = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 int count = 0; double kernel_temp[100]; for (int i = 0; i<size*size; i++) { kernel_temp[i] = 0; //赋初值,空间分配 } for (int i = 0; i < size; i++)//将核转化为一维,方便卷积*** { for (int j = 0; j < size; j++) { kernel_temp[count] = kernel[i][j]; count++; } } /*边缘处理用0代替*/ for (int i = 0; i < image.rows; i++) { for (int j = 0; j < image.cols; j++) { int k = 0;//卷积核的索引 for (int x = -size / 2; x <= size / 2; x++) { for (int y = -size / 2; y <= size / 2; y++) { int r = i + x; int c = j + y; r = r < 0 ? -1 : r;//处理顶部边缘 r = r >= image.rows ? - 1 : r;//处理底部边缘 c = c < 0 ? -1: c; c = c >= image.cols ? -1 : c; if (r == -1 || c == -1) after_convolution.at<uchar>(i, j) += kernel_temp[k] * 0; else after_convolution.at<uchar>(i, j) += kernel_temp[k] * image.at<uchar>(r, c); k++; } } } } return after_convolution; } //卷积公式加绝对值 Mat Convolution3(Mat &image, double (*kernel)[3], int size) { double sum=0; Mat after_convolution = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 int count = 0; double kernel_temp[100]; for (int i = 0; i<size*size; i++) { kernel_temp[i] = 0; //赋初值,空间分配 } for (int i = 0; i < size; i++)//将核转化为一维,方便卷积*** { for (int j = 0; j < size; j++) { kernel_temp[count] = kernel[i][j]; count++; printf("%f ", kernel[i][j]); } printf(" "); } /*边缘处理才用边缘的点代替值*/ for (int i = 0; i < image.rows; i++) { for (int j = 0; j < image.cols; j++) { sum = after_convolution.at<uchar>(i, j); int k = 0;//卷积核的索引 for (int x = -size / 2; x <= size / 2; x++) { for (int y = -size / 2; y <= size / 2; y++) { int r = i + x; int c = j + y; r = r < 0 ? 0 : r;//处理顶部边缘 r = r >= image.rows ? image.rows - 1 : r;//处理底部边缘 c = c < 0 ? 0 : c; c = c >= image.cols ? image.cols - 1 : c; sum += (kernel_temp[k] * image.at<uchar>(r, c)); k++; } } after_convolution.at<uchar>(i, j) = abs(sum); } } convertScaleAbs(after_convolution, after_convolution); return after_convolution; } Mat Sobel(Mat &image_x,Mat &image_y, double *gradint_angle) { Mat after_sobel = Mat::zeros(image_x.size(), CV_32FC1);//创建一幅空图片注意图片类型 int k = 0; for (int i = 0; i < after_sobel.rows; i++) { for (int j = 0; j < after_sobel.cols; j++) { double x = image_x.at<uchar>(i, j); double y = image_y.at<uchar>(i, j); after_sobel.at<float>(i, j) = sqrt(x*x + y*y); } } convertScaleAbs(after_sobel, after_sobel);//转为8位无符号整形 return after_sobel; } Mat Nms1(Mat &image, Mat &image_x, Mat &image_y) { Mat after_nms = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 after_nms = image.clone(); double center, temp1, temp2; int c1, c2, c3, c4; double x, y;//图片在x,y方向的梯度 double k; for (int i = 1; i < image.rows-1; i++) { for (int j = 1; j < image.cols-1; j++) { center = image.at<uchar>(i, j); x = image_x.at<uchar>(i, j); y = image_y.at<uchar>(i, j); if (abs(y) > abs(x))//确定c2c4位置 考y轴 { c2 = image.at<uchar>(i - 1, j);//上 c4 = image.at<uchar>(i - 1, j); if (y == 0) k = 0; else { k = fabs(x) /fabs(y);//比例 } if (x*y < 0)//两方向不相同,13象限 { c1 = image.at<uchar>(i - 1, j + 1); c3 = image.at<uchar>(i + 1, j - 1); } else//否则24象限 { c3 = image.at<uchar>(i + 1, j + 1); c1 = image.at<uchar>(i - 1, j - 1); } } else { c2 = image.at<uchar>(i , j+1);//上 c4 = image.at<uchar>(i - 1, j-1); if (x == 0) k = 0; else { k = fabs(y) / fabs(x);//比例 } if (x*y < 0)//两方向不相同,13象限 { c1 = image.at<uchar>(i - 1, j - 1); c3 = image.at<uchar>(i + 1, j + 1); } else//否则1,3象限 { c1 = image.at<uchar>(i + 1, j - 1); c3 = image.at<uchar>(i - 1, j + 1); } } temp1 = k*c1 + (1 - k)*c2; temp2 = k*c3 + (1 - k)*c4; if (center >= temp1&¢er >= temp2) { //是极大值 ; } else { //不是最大值置0 after_nms.at<uchar>(i, j) = 0; } } } return after_nms; } Mat Nms2(Mat &image, Mat &image_x, Mat &image_y) { Mat after_nms = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 after_nms = image.clone(); double center, temp1, temp2; int c1, c2, c3, c4; double x, y;//图片在x,y方向的梯度 double k; for (int i = 1; i < image.rows - 1; i++) { for (int j = 1; j < image.cols - 1; j++) { center = image.at<uchar>(i, j); x = image_x.at<uchar>(i, j); y = image_y.at<uchar>(i, j); if (abs(y) > abs(x))//确定c2c4位置 考y轴 { c2 = image.at<uchar>(i, j + 1);//上 c4 = image.at<uchar>(i, j - 1); if (y == 0) k = 0; else { k = fabs(x) / fabs(y);//比例 } if (x*y < 0)//两方向不相同,13象限 { c1 = image.at<uchar>(i - 1, j + 1); c3 = image.at<uchar>(i + 1, j - 1); } else//否则24象限 { c3 = image.at<uchar>(i + 1, j + 1); c1 = image.at<uchar>(i - 1, j - 1); } } else { c2 = image.at<uchar>(i - 1, j);//上 c4 = image.at<uchar>(i + 1, j); if (x == 0) k = 0; else { k = fabs(y) / fabs(x);//比例 } if (x*y < 0)//两方向不相同,13象限 { c1 = image.at<uchar>(i - 1, j - 1); c3 = image.at<uchar>(i + 1, j + 1); } else//否则1,3象限 { c1 = image.at<uchar>(i + 1, j - 1); c3 = image.at<uchar>(i - 1, j + 1); } } temp1 = k*c1 + (1 - k)*c2; temp2 = k*c3 + (1 - k)*c4; if (center >= temp1&¢er >= temp2) { //是极大值 ; } else { //不是最大值置0 after_nms.at<uchar>(i, j) = 0; } } } return after_nms; } Mat Nms3(Mat &image, Mat &image_x, Mat &image_y) { Mat after_nms = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 after_nms = image.clone(); double center, temp1, temp2; int c1, c2, c3, c4; double x, y;//图片在x,y方向的梯度 double k; for (int i = 1; i < image.rows - 1; i++) { for (int j = 1; j < image.cols - 1; j++) { center = image.at<uchar>(i, j); x = image_x.at<uchar>(i, j); y = image_y.at<uchar>(i, j); if (abs(y) < abs(x))//确定c2c4位置 考y轴 { c2 = image.at<uchar>(i, j + 1);//上 c4 = image.at<uchar>(i, j - 1); if (y == 0) k = 0; else { k = fabs(y) / fabs(x);//比例 } if (x*y < 0)//两方向不相同,13象限 { c1 = image.at<uchar>(i - 1, j + 1); c3 = image.at<uchar>(i + 1, j - 1); } else//否则24象限 { c3 = image.at<uchar>(i - 1, j - 1); c1 = image.at<uchar>(i + 1, j + 1); } } else { c2 = image.at<uchar>(i - 1, j);//上 c4 = image.at<uchar>(i + 1, j); if (x == 0) k = 0; else { k = fabs(x) / fabs(y);//比例 } if (x*y < 0)//两方向不相同,13象限 { c1 = image.at<uchar>(i - 1, j - 1); c3 = image.at<uchar>(i + 1, j + 1); } else//否则1,3象限 { c1 = image.at<uchar>(i - 1, j + 1); c3 = image.at<uchar>(i + 1, j - 1); } } temp1 = k*c1 + (1 - k)*c2; temp2 = k*c3 + (1 - k)*c4; if (center >= temp1&¢er >= temp2) { //是极大值 ; } else { //不是最大值置0 after_nms.at<uchar>(i, j) = 0; } } } return after_nms; } Mat DoubleThreshold(Mat &image, double high_threshold, double low_threshold) { Mat after_threshold = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 after_threshold = image.clone(); for (int i = 0; i < image.rows; i++) { for (int j = 0; j < image.cols; j++) { double temp = image.at<uchar>(i, j); if (temp < low_threshold) { after_threshold.at<uchar>(i, j) = 0; } if (temp > high_threshold) { after_threshold.at<uchar>(i, j) = 255; } } } return after_threshold; } Mat Link1(Mat &image) { bool flag = false; int x = 0; int y = 0; Mat after_link = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 after_link = image.clone(); for (int i = 1; i < image.rows-1; i++) { for (int j = 1; j < image.cols-1; j++) { flag = false; double temp = image.at<uchar>(i, j); if (temp > 0 && temp < 255)//处于阈值中间的 { for (int k = 0; k < 8; k++)//循环判断 { x = i + xNum[k]; y = j + yNum[k]; if (after_link.at<uchar>(x, y) != 0) { flag = true; break; } } if (flag)//周围有不为0的点 { after_link.at<uchar>(i, j) = 255; } else { after_link.at<uchar>(i, j) = 0; } } } } return after_link; } void Link2(Mat &image,int i,int j) { //printf("i=%d j=%d ", i, j); bool flag = false; int x = 0; int y = 0; double temp = image.at<uchar>(i, j); if (temp > 0 && temp < 254)//处于阈值中间的 { for (int k = 0; k < 8; k++)//循环判断 { x = i + xNum[k]; y = j + yNum[k]; if (image.at<uchar>(x, y) != 0) { flag = true; break; } } if (flag)//周围有不为0的点 { image.at<uchar>(i, j) = 255; Link2(image, x, y); } else { image.at<uchar>(i, j) = 0; } } return; } Mat Link3(Mat &image) { Mat after_link = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 after_link = image.clone(); int k = 0; for (int i = 1; i < after_link.rows-1; i++) { for (int j = 1; j<after_link.cols-1; j++) { double temp = image.at<uchar>(i, j); if (temp > 0 && temp < 255) { //printf("%d ", k); Link2(after_link, i, j); k++; } } } return after_link; } int main() { Mat image = imread("G:\image\Lena.jpg"); Mat test_image = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 Mat test_image2 = Mat::zeros(image.size(), CV_8UC1);//创建一幅空图片 if (image.empty()) { printf("图片加载出错"); return 0; } double *point; Mat gray_image = getGray(image); imwrite("..\gray.jpg", gray_image); double *gradint_angle=nullptr; double **guss = getGussKernel(5, 5); Mat after_guss = Convolution2(gray_image, guss, 5); imwrite("..\guss.jpg", after_guss); Mat x = Convolution3(after_guss, sobel_x, 3); imwrite("..\gradint_x.jpg", x); Mat y = Convolution3(after_guss, sobel_y,3); imwrite("..\gradint_y.jpg", y); Mat z = Sobel(x, y, gradint_angle); imwrite("..\gradint.jpg", z); Mat nms = Nms2(z, x, y); imwrite("..\afterNms.jpg", nms); Mat t = DoubleThreshold(nms, 120, 60); imwrite("..\afterDT.jpg", t); Mat canny = Link1(t); imwrite("..\canny.jpg", canny); Mat canny2 = Link3(t); GaussianBlur(gray_image, test_image,Size(5,5),10); imshow("原图片为", image); imshow("灰度图片为", gray_image); imshow("使用自己写的高斯滤波后的图片为", after_guss); imshow("x方向的梯度", x); imshow("y方向的梯度", y); imshow("合并的梯度", z); imshow("非极大抑制后的结果为", nms); imshow("双阈值结果为", t); imshow("连接后的结果为", canny); imshow("2连接后的结果为", canny2); imshow("cv高斯滤波", test_image); ///*opencv 自带梯度算法*/ //Sobel(after_guss, test_image, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT); //convertScaleAbs(test_image, test_image);//使用线性变换转换输入数组元素成8位无符号整型 //imshow("oX方向Sobel", test_image); //Sobel(after_guss, test_image2, CV_16S, 1, 1, 3, 1, 1, BORDER_DEFAULT); //convertScaleAbs(test_image2, test_image2);//使用线性变换转换输入数组元素成8位无符号整型 //imshow("oY方向Sobel", test_image2); //imshow("使用opencv高斯滤波后的图片为", test_image); //addWeighted(test_image, 0.5, test_image2, 0.5, 0, test_image); //imshow("整体方向Sobel", test_image); ///****************************************/ Canny(image, test_image, 150, 50,3); imshow("cvcanny", test_image); waitKey(); return 0; }