课程名称:计算机视觉 实验项目名称:直线检测和圆检测
一、实验目的和要求 对输入的一张彩色图像,检测其中的圆形与直线,并将检测结果显示在原图上。
检测算法的核心功能需要自己 写代码实现, 不能调用 OpenCV 或其他SDK里与圆形直线检测 相关的函数;如果要用到边缘检测,这个可以调用 OpenCV 函数。
在原图上显示最终的检测结果。
单独显示一些关键的中间结果
必须对指定的三张测试图像( coin 、seal 、highway )调试结果。此外,自己还可以自愿加一些测试图像。
二、实验原理 1)Hough 变换 $$$ y=mx+c \Rightarrow c=-xm+y $$$ 如上图,对于笛卡尔坐标系内的一条直线:$y=mx+c$
以$(x_0,y_0)$点为例,固定$x_0,y_0$为自变量,$(m,c)$为变量,则每个点$(x_0,y_0)$对应于空间$(m,c)$上的一条直线:$c=-xm+y$
通过变换坐标系,将直线和点相互转换,从而将求直线转化为求多条直线的交点。
但是上述转换存在直线垂直时,斜率是无穷大的情况,因此我们换用极坐标系。
直角坐标系中的点是极坐标系中的线,直角坐标系中的线是极坐标系中的点。
如下图所示,想要检测图像中的直线,可以使用Hough变换,转化为检测极坐标系中的点 $(\theta, \rho)$ 。
2)直线Hough变换 笛卡尔坐标系中的直线,由斜率和截距表示,而极坐标中用 $(\theta, \rho)$ 表示,并且存在下式关系: $$$ \rho = cos(\theta)\cdot x + sin(\theta)\cdot y $$$ 对于点 $(x_0,y_0)$,代入上式,在极坐标中就是一条线(很多对 $(\theta, \rho)$ 点): $$$ r = cos(\theta)\cdot x_0 + sin(\theta)\cdot y_0 $$$ $(\theta, \rho)$ 就是一对Hough空间的变量表示。一个点 $(x_0,y_0)$, 就是一个正弦曲线。
如下图,直角坐标系中的多个点,对应于 $\rho - \theta$ 空间的多条正弦曲线。
直角坐标系的三点共线,对应于 $\rho - \theta$ 空间的多线共点 。
因此,我们可以通过检测 $\rho - \theta$ 空间的交集点,来检测原始空间的线段。
接下来,就是要考虑 将 $\rho - \theta$ 离散化,形成离散化的Hough空间,用于统计交集点的个数。
3)累加器
将参数空间离散化,从而得到一个二维数组累加器,记录 $\rho - \theta$ 空间每对参数 $(\theta, \rho)$ 出现的次数,大于某个阈值的,就证明有线相交于该点,也就证明原笛卡尔坐标系中有直线。
4)圆的Hough变换 给定一个圆弧有三个参数,$a,b$用来确定圆心位置,$\rho$确定半径。 $$$ b=atan\theta-xtan\theta+y $$$
类似直线,将$(x_0,y_0)$固定,而将$(a,b,\rho)$作为变量: $$$ x=a+\rho cos \theta \Rightarrow a=x-\rho cos \theta \ y=b+\rho sin \theta b=y-\rho sin \theta $$$ 因此可以创建hough三维累加器,对于圆图像中的每一个点计算对应的$(a,b,\rho)$点对。
但由于是三维累加器,会大大增加计算量。因此我们采用霍夫梯度法,先找出圆心的坐标。
我们令$\rho$为边缘点处的梯度角,则上面的公式可以变换为: $$$ b = a tan\theta-x tan\theta+y $$$ 如下图,圆的边缘点切线的垂直方向,也就是梯度方向经过圆点。所以我们可以建立关于$(a,b)$的二位累加器,遍历图像的所有点,对每个像素点计算梯度(本实验中使用Sobel算子),对该直线上的所有像素点进行投票,最后选取超过阈值的像素点作为阈值,为了避免选取过多的圆心,把距离小于某一个阈值的圆心当做同一个圆心。
下一步,对于每一个投票选出的圆心,遍历整个图像,计算他们与该圆心的距离,然后将距离值排序,在某一个距离范围区间内,点超过一定数量的,就可以将该距离作为该圆心对应的一个半径。
全部遍历完成后,就可以得到$(a,b,\theta)$点对表示的圆检测结果。
三、实验内容和过程分析 1) 主函数及图像读入和预处理流程分析 本实验中,读入图像后我对图像做了一下预处理:
转化为灰度图
高斯滤波以及均值滤波,图像去噪,防止噪声干扰边缘检测
使用Canny算子计算图像边缘并展示
将边缘图像作为掩码拷贝源图像,从而展示彩色的边缘图像
预处理之后进入正式的直线和圆形检测,调用“HoughLine”以及“HoughCircle”模块中的函数
使用Hough变换检测直线并返回结果,在源图像上绘制结果直线
使用Hough梯度法检测原型并返回结果,在源图像上绘制结果圆
以下是简化版的主函数代码,其中包括图像预处理以及直线和圆检测函数的调用(篇幅限制,仅展示主要内容):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 vector<Vec3f> circles; vector<Line> lines; Mat src = imread ("1.jpg" , 1 ); if (!src.data){ cout << "图像不存在!" << endl; return -1 ; } imshow ("source" , src); waitKey (0 );Mat src_edge; cvtColor (src, src_gray, CV_BGR2GRAY); GaussianBlur (src_gray, src_gray, Size (9 , 9 ), 2 , 2 );imshow ("guass" , src_gray);waitKey (0 );blur (src_gray, src_edge, Size (3 , 3 ));imshow ("mean" , src_edge);waitKey (0 );Mat edges; int canny_threshold = 40 ;Canny (src_edge, edges, MAX (canny_threshold / 2 , 1 ), canny_threshold, 3 );imshow ("edges" , edges);waitKey (0 );Mat image = src.clone (); Mat dst; dst.create (image.size (), image.type ()); dst = Scalar::all (0 ); image.copyTo (dst, edges); imshow ("colorful_edge" , dst);waitKey (0 );houghcircles (src_gray, edges, circles, 10 , 40 , 30 , 0 , 0 );for (size_t i = 0 ; i < circles.size (); i++){ Point center (cvRound(circles[i][0 ]), cvRound(circles[i][1 ])) ; int radius = cvRound (circles[i][2 ]); circle (src, center, 3 , Scalar (0 , 255 , 0 ), -1 , 8 , 0 ); circle (src, center, radius, Scalar (255 , 0 , 0 ), 2 , 8 , 0 ); } houghlines (edges, lines, 110 );for (size_t i = 0 ; i < lines.size (); i++){ Point s = lines[i].start; Point e = lines[i].end; line (src, s, e, Scalar (0 , 0 , 255 )); } namedWindow ("Result" , CV_WINDOW_AUTOSIZE);imshow ("Result" , src);waitKey (0 );
2)Hough变换检测直线
HoughLine模块结构体及函数原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct Polar { int x; int y; int count; }; struct Line { Point start; Point end; }; bool polar_order (Polar a, Polar b) ; int get_position (Mat img, int ii, int jj, int flag) ; void houghlines (Mat img,vector<Line>& lines, int threshold, double rho = 1 , double theta = 1 ) ;
适当地量化参数空间(合适的精度即可)
根据图像大小及分辨率(精度)计算累加器大小
1 2 3 4 5 6 7 8 int w = img.cols;int h = img.rows;int add_w = 180 /theta;int add_h = 1.5 * (w + h) /rho ;int center_h = add_h / 2 ;
假定参数空间的每一个单元都是一个累加器,把累加器初始化为零
1 2 3 4 5 6 7 8 9 int ** add = (int **)malloc (add_h * sizeof (int *)); for (int i = 0 ; i < add_h; i++) add[i] = (int *)malloc ((add_w + 1 ) * sizeof (int )); for (int i = 0 ; i < add_h; i++) for (int j = 0 ; j < add_w; j++) add[i][j] = 0 ;
对图像空间的每一点,在其所满足的参数方程对应的累加器上加1
1 2 3 4 5 6 7 8 9 10 11 12 int threshold_pix = 200 ; for (int i = 0 ; i < h; i++) for (int j = 0 ; j < w; j++) if ((int )img.at <uchar>(i, j) > threshold_pix) for (int k = 0 ; k < 180 ; k=k+theta) { double angle = (double )k / 180 * PI; double dr = (double )j * cos (angle) + (double )i * sin (angle); int r = round (dr)/rho; int kk = k/theta; add[r + center_h][kk]++; }
累加器阵列的超过阈值的点对应直线的参数
在这里对原算法进行了改进,判断累加器是,需要该点同时满足值超过阈值和大于自身周围的点的累加器值,才将该点选中。
并且在选中点之后,对其做一个聚合操作,即将其周围一定区间内的累加器的值也看做是一条直线,同时累加给该点。
(原理相似处代码此处省略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 vector<Polar> v; for (int y = 1 ; y < add_h - 1 ; y++){ for (int x = 0 ; x < add_w; x++) { int flag = 0 ; if (x == 0 ) if (add[y][x] > threshold && add[y][x] > add[y][x + 1 ] && add[y][x] > add[y - 1 ][x] && add[y][x] > add[y + 1 ][x]) flag = 1 ; else if (x == add_w - 1 ) else if (add[y][x] > threshold && add[y][x] > add[y][x - 1 ] && add[y][x] > add[y][x + 1 ] && add[y][x] > add[y - 1 ][x] && add[y][x] > add[y + 1 ][x]) flag = 1 ; if (flag) { Polar po; po.x = y; po.y = x; po.count = add[y][x]; for (int p = y - 5 ; p <= y + 5 ; p++) for (int q = x - 5 ; q <= x + 5 ; q++) if (p >= 0 && p <= add_h - 1 && q >= 0 && q <= add_w - 1 ) po.count += add[p][q]; v.push_back (po); } } }
将结果记录到lines
对已经被选中的点降序排列后,反推计算出其在原图像中对应的直线的起始点坐标,并记录到lines中。(此处代码简化)
在这里分四种情况处理:
水平直线
垂直直线
$\theta$ 较小
$\theta$ 较大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 sort (v.begin (), v.end (), polar_order); vector<Polar>::iterator iter; for (iter = v.begin (); iter != v.end (); iter++) { if (iter->y == 0 ) { y1 = 0 ; y2 = h - 1 ; x1 = x2 = get_position (img, iter->x, iter->y, false ); Line temp; temp.start = Point (x1, y1); temp.end = Point (x2, y2); lines.push_back (temp); } else if (iter->y == 90 ){ else if (iter->y >= 45 && iter->y <= 135 ) { x1 = 0 ; y1 = (iter->x - center_h) / si; x2 = w - 1 ; y2 = ((iter->x - center_h) - (double )x2 * co) / si; Line temp; temp.start = Point (x1, y1); temp.end = Point (x2, y2); lines.push_back (temp); } else { } }
3)Hough梯度法检测圆形
HoughCircle模块结构体及函数原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #define PI 3.14159265358979 #define HOUGH_CIRCLE_RADIUS_MIN 10 #define HOUGH_CIRCLE_RADIUS_MIN_DIST 2 #define HOUGH_CIRCLE_INTEGRITY_DEGREE 0.6 #define HOUGH_CIRCLE_SAMEDIRECT_DEGREE 0.99 #define HOUGH_CIRCLE_GRADIENT_INTEGRITY_DEGREE 0.9 struct Centers2 { int x; int y; int count; }; bool center_order (Centers2 a, Centers2 b) ; struct Radius { double dist2; double inner_product; }; bool radius_order (Radius a, Radius b) ; float normalization (float x) ; void houghcircles (cv::Mat& src_gray, Mat edges, std::vector<cv::Vec3f>& circles, double min_dist, int param2, int minRadius = 0 , int maxRadius = 0 ) ;
量化关于a,b的参数空间到合适精度,创建并初始化累加器
1 2 3 4 5 6 7 8 9 10 11 12 13 int add_rows = (double )src_gray.rows / dp + 0.5 ;int add_cols = (double )src_gray.cols / dp + 0.5 ;int ** add = (int **)malloc (add_rows * sizeof (int *));for (int i = 0 ; i < add_rows; i++) add[i] = (int *)malloc ((add_cols) * sizeof (int )); for (int i = 0 ; i < add_rows; i++) for (int j = 0 ; j < add_cols; j++) add[i][j] = 0 ;
计算图像空间中边缘点的梯度幅度和角度,遍历边缘图像,若边缘点参数坐标满足则该参数坐标对应的累加器加1
使用Sobel算子计算边缘图像梯度,并根据分辨率遍历边缘图像计算累加器(篇幅限制,部分细节代码省略)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 cv::Sobel (src_gray, dx, CV_16SC1, 1 , 0 , 3 ); cv::Sobel (src_gray, dy, CV_16SC1, 0 , 1 , 3 ); Point pt; int accum_max = 0 ; for (int y = 0 ; y < rows; y++){ for (int x = 0 ; x < cols; x++) { if (!edges_row[x] || (dx_now == 0 && dy_now == 0 ))continue ; float length = sqrt (dx_now * dx_now + dy_now * dy_now); sx = cvRound ((dx_now / dp) / length * ONE ); sy = cvRound ((dy_now / dp) / length * ONE ); int x_now = cvRound ((x / dp) * ONE); int y_now = cvRound ((y / dp) * ONE); for (int times = 0 ; times < 2 ; times++) { x1 = x_now + minRadius * sx; y1 = y_now + minRadius * sy; for (r = minRadius; r <= maxRadius; x1 += sx, y1 += sy, r++) { int x2 = x1 >> SHIFT, y2 = y1 >> SHIFT; add[y2][x2]++; if (add[y2][x2] > accum_max) accum_max = add[y2][x2]; } sx = -sx; sy = -sy; } pt.x = x; pt.y = y; points.push_back (pt); } }
选出累加器的值大于一定阈值且局部最大的点,所在的坐标即为图像空间中的圆心之所在,并展示圆心选择结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 for (int y = 1 ; y < add_rows - 1 ; y++) for (int x = 1 ; x < add_cols - 1 ; x++) add[y][x] = normalization ((float )add[y][x] / accum_max) * 256 ; for (int y = 1 ; y < add_rows - 1 ; y++) for (int x = 1 ; x < add_cols - 1 ; x++) if (add[y][x] > add_threshold && add[y][x] > add[y][x - 1 ] && add[y][x] > add[y][x + 1 ] && add[y][x] > add[y - 1 ][x] && add[y][x] > add[y + 1 ][x]) { Centers2 temp; temp.x = x; temp.y = y; temp.count = add[y][x]; centers2.push_back (temp); } Mat center_img = Mat (add_rows, add_cols, CV_32SC1); for (int i = 0 ; i < rows; i++) int * ptmp = center_img.ptr <int >(i); for (int j = 0 ; j < cols; j++) center_img.at <int >(i, j) = add[i][j]; cv::normalize (center_img, center_img, 0 , 255 , cv::NORM_MINMAX); center_img.convertTo (center_img, CV_8UC1); imshow ("centers" , center_img);waitKey (0 );
得到圆心坐标之后,反求圆心对应的半径ρ
外层遍历得到的圆心,内层遍历所有参与计算的边缘图像中的点,计算他们与当前圆心的距离,并记录所有距离数据在名为Radius的vector中,对vector进行排序,之后遍历vector,对于在参数min_Radius范围内,圆周上的点数目大于一定阈值的半径,则作为该圆心对应的半径之一(考虑同心圆)。(代码部分省略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 sort (centers2.begin (), centers2.end (), center_order);min_dist *= min_dist; for (i = 0 ; i < centers2.size (); i++){ radius.clear (); int y = centers2[i].y; int x = centers2[i].x; float cx = (float )((x + 0.5f ) * dp), cy = (float )((y + 0.5f ) * dp); float start_dist, dist_sum; for (j = 0 ; j < circles.size (); j++){ cv::Vec3f center = circles[j]; if ((center[0 ] - cx) * (center[0 ] - cx) + (center[1 ] - cy) * (center[1 ] - cy) < min_dist) break ; } if (j < circles.size ())continue ; for (j = k = 0 ; j < points_count; j++){ cv::Point pt; pt = points[j]; float _dx = cx - pt.x; float _dy = cy - pt.y; float _r2 = _dx * _dx + _dy * _dy; short sx = dxdy_row[0 ]; short sy = dxdy_row[1 ]; if (minRadius2 <= _r2 && _r2 <= maxRadius2){ k++; Radius temp; temp.dist2 = _r2; radius.push_back (temp); } } int point_cnt1 = k, start_idx = point_cnt1 - 1 ; if (point_cnt1 == 0 ) continue ; for (int t = 0 ; t < point_cnt1; ++t) radius[t].dist2 = pow (radius[t].dist2, 0.5 ); sort (radius.begin (), radius.end (), radius_order); start_dist = radius[0 ].dist2; for (j = 0 ; j <point_cnt1; j++) { float dist2 = radius[j].dist2; float inner_product = radius[j].inner_product; if (dist2 > maxRadius) break ; if (dist2 - start_dist < HOUGH_CIRCLE_RADIUS_MIN_DIST * dr){ cur_r_count++; cur_r_dist_sum += dist2; } else { float r_mean = cur_r_dist_sum / cur_r_count; if (cur_r_count >= HOUGH_CIRCLE_INTEGRITY_DEGREE * 2 * PI * r_mean) { cv::Vec3f c; c[0 ] = cx; c[1 ] = cy; c[2 ] = (float )r_mean; circles.push_back (c); } cur_r_count = 1 ; cur_r_dist_sum = dist2; start_dist = dist2; } } }
四、实验结果展示 1)直线检测(以highway图像为例) 该图像由于噪点较多,影响检测,因此实际实验时将高斯滤波和均值滤波的参数调大。
源图像
滤波结果(此处仅展示高斯滤波结果,提交的压缩包中将含有高斯滤波和均值滤波)
边缘图像(此处仅展示黑白边缘图像,提交的压缩包中将含有黑白和彩色两种格式)
直线检测结果
2)圆形检测(以coins图像为例)
源图像
滤波结果(此处仅展示高斯滤波结果,提交的压缩包中将含有高斯滤波和均值滤波)
边缘图像(此处仅展示黑白边缘图像,提交的压缩包中将含有黑白和彩色两种格式)
圆心累加器结果
圆形检测结果
3)同时检测直线和圆(以seal图像为例) 1.源图像
滤波结果(此处仅展示高斯滤波结果,提交的压缩包中将含有高斯滤波和均值滤波)
边缘图像(此处仅展示黑白边缘图像,提交的压缩包中将含有黑白和彩色两种格式)
圆心累加器结果
直线和圆形检测结果
五、实验思考和感想
通过本次实验,我更加深入地学习和体会了hough变换检测图形的算法,在实践的过程中,我发现了很多在学习过程中没有注意到的细节,都需要在实践进行调整。
在实验初期,调整hough变换检测的阈值是一件非常困难的事情,不合适的阈值对检测结果的影响非常大,后期,我发现这个阈值由于是一个绝对值,而图像大小会导致不同的图像适合的阈值有非常大的差别,因此我将其中的highway图像大小进行了调整,使其与其他两张图像大小差别缩小,因此最后的实验结果中,不必人为调整阈值,就可以同时对三张图像进行检测。
在实验中,我还发现,在进行直线检测时,由于hough变换时转换计算有误差,可能会导致变换后的线(对应原来的共线的点)不准确相交于一个点,而是在一个很小的范围内波动,因此我对算法进行了改进,在检测到累加器中的局部极大值后,在一个小范围区间内,对累加器的值做一个聚合,就会很大程度上避免这种情况,也能使得检验结果更加清晰准确。
另外,由于highway图像较模糊,清晰度较低,因此两旁的数目对检测结果的影响非常大,这是因为树木范围中很多点都被识别成了边缘点,因此,对于这张图像,我增大了均值滤波的参数,成功降低了树木中的边缘点数目,也最终非常明显地改善了检测结果。
在识别圆形时,我发现算法对于同心圆的检测效果不是那么优秀,我对算法进行了一定的改进,调整了宏定义的 “HOUGH_CIRCLE_RADIUS_MIN_DIST” 参数,使得计算半径时的半径区间范围稍微放大一些,从而使得半径的容错率更高,改善了半径计算不准确导致的无法识别出圆,将其舍弃的情况。但是该参数调整反馈非常敏感,稍微调整一点都会很明显地影响到检测结果(可能识别出一些不合适的结果,如下图),因此最后很遗憾地是还是没能识别出胶带最外圈的圆形,而只识别出了两圈(见上文图片)。但是经过对原图像和边缘图像的分析,我们也可以看出,出现这种结果的原因可能是因为:胶带内圈的两个圆太密集,从而使得算法可能检测出小圆的左半圈和右半圈为一个整圆,才会出现下图的结果。因此最后还是将该参数调回了原来的值。