首页 > 代码库 > 边缘提取与直线检测
边缘提取与直线检测
计算机中的边缘算法主要是依靠梯度差来计算,常见的有sobel算子,lapacian算子等,在实现方法上都大同小异,OpenCV中对这类函数都有封装,使用起来很方便:
1.Sobel算子的边缘检测
我们先找一张灰度图像,这里用一张照片,取在HSV色域的V通道:
sobel算子有两个方向:
-1 | -2 | -1 |
0 | 0 | 0 |
1 | 2 | 1 |
-1 | 0 | 1 |
-2 | 0 | 2 |
-1 | 0 | 1 |
分别用来检测水平方向与竖直方向上的边缘,
cv::Sobel(image, sobelX, CV_16S, 1, 0);//1,0代表水平方向cv::Sobel(image, sobelY, CV_16S, 0, 1);//0,1代表竖直方向
因为计算后的像素值区间在-510~510之间,所以这边要使用16位的类型来储存。
然后我们将两个方向上的值相加:
sobel = abs(sobelX) + abs(sobelY);
相加后的像素值区间在0~1020之间,无法显示,我们要将其归一化,即每个值都除以最大值,然后乘以255.
这里我们要将大的值(边缘)是用黑色表示,所以在归一化的时候要使用-255:
double sobmin, sobmax; cv::minMaxLoc(sobel, &sobmin, &sobmax); //这个方法可以返回最大值 cv::Mat sobelImage; sobel.convertTo(sobelImage, CV_8U, -255. / sobmax, 255);
最后一行的意义是将数据转化为8位,将每个像素点的值X做如下运算:
X=X*(-255/sobmax)+255;从而将值域转化为0~255之间:
结果如下:
这张图中,越黑的位置代表其边缘的强度越强,如果我们要忽略弱边缘,则需要在这张图上加一个阈值:
cv::threshold(sobelImage, sobelThresholded, 190, 255, cv::THRESH_BINARY)
上面这张图中,大于190的部分将被转换为白色,小于190的地方会被转换为黑色:
选取阈值后,会发现尽管一些不相关的边缘被抹去了,但是有些我们希望保留的边缘(杯子右侧)同样被忽略,这种情况下Canny在1986年提出了一种策略模式来优化边缘提取:
2.Canny算法的优化边缘提取
OpenCV中可以直接运用canny算法,canny算法实际上是将sobel算子应用两次,取不同于阈值,一个是低阈值,低阈值要包含像素全部的重要边缘,高阈值要尽量将全部的非重要边缘去除。
cv::Mat contours; cv::Canny(image, contours, 110,110);
这里可以教大家一个小技巧,在试阈值的时候,将两个阈值调到一样的值,然后看效果,比如先都调到110,这是低阈值:
低阈值包含了全部重要边缘,然后设置高阈值,我们以行李箱中的纹路全部消失为界,同时调到380:
下面执行110到380的阈值,比较下原图和最终的边缘提取:
3.霍夫变换的直线检测
霍夫变换是一个靠点的数量来判断空间中特定形状的存在的,其中最简单的形状就是直线,在实现霍夫变换检测直线之前,我们先复习一下直线在空间中的表示。
任意直线在空间中都可以表示为y=kx+b的形式,但是作为斜率的k在空间中的取值为0到正无穷。在直线接近垂直于y轴的时候,难以表示,为了更好的表示是空间中的直线,Hough变换中,通常用极坐标表示空间内的直线:
空间内任意一条直线都可以用原点到其的距离r与这条垂直线和x轴的夹角θ表示,记为(r,θ)。
这时直线上任意一点(x,y),可以表示为:
r=xcos(θ)+ysin(θ)
也就是说,对于一个(x,y)来说,我们可以在(r,θ)空间中画出很多条函数,表示通过这一点的所有直线,但是对于特定直线上的每个点,他们必定都通过(r,θ)这个点。
如果某一个(r,θ)出现的过多我们可以认定为w空间中很多点都在这条直线上,在图像内这是一条直线。
在程序中,我们一般用一个2为数组Hough[n][360]来表示空间内的所有直线,其中n为图像对角线的长度。
对于边缘检测中的所有像素点(x,y)进行一个360度的遍历,最终如果某一个n值出现多过一个阈值,则判定为一条直线。
OpenCV中使用Hough变换很方便,一般只需两步就可以得到一个结果直线的矩阵:
cv::Mat contours;cv::Canny(image, contours,110,380);vector<cv::Vec2f> lines;cv::HoughLines(contours, lines, 1, PI / 180, 60);
Hough中的五个参数分别是:边缘检测结果,输出结果矩阵,半径步长,角度步长,阈值(多少个点算直线);
然后我们需要把这个结果矩阵画到原来的图上:
这里我们需要特别注意一点,在画线的过程中我们是以直线方程与坐标轴相交的两个点来确定直线的,这时我们要用到cvPoint来定位这两个点,平常我们定位图像中坐标的时候是先行,在列,而cvPoint中是先列,再行。
因此对于 cv:Point pt1 (20,0)来说,pt1指的是图像的第20列,0行这个点,而对于指令image.at<cv::vec3b>(20,0),来说,指的则是20行第0列这个点。
vector<cv::Vec2f>::const_iterator it = lines.begin(); //初始化迭代器遍历所有直线 while (it != lines.end()){ float rho = (*it)[0]; //rho访问半径 float theta = (*it)[1]; //theta方访问角度 if (theta<PI / 4. || theta>3.*PI / 4.){ //近似垂直线 cv::Point pt1(rho / cos(theta), 0); //计算其与图像上方的交点 cv::Point pt2((rho - image1.rows*sin(theta)) / cos(theta), image1.rows);//下方交点 cv::line(image1, pt1, pt2, cv::Scalar(255), 1);//图像,2个点,颜色 } else{ cv::Point pt1(0, rho / sin(theta)); //左方交点 cv::Point pt2(image1.cols, (rho - image1.cols*cos(theta)) / sin(theta));//右方交点 cv::line(image1, pt1, pt2, cv::Scalar(255), 1); } ++it; }
结果如下:
边缘提取与直线检测