实验6.1 霍夫变换
实验过程
基本遍历算法:
Hough Transform for Circles
For every edge pixel (x,y) : For each possible radius value r:
For each possible gradient direction θ:
// or use estimated gradient
a = x – r cos(θ)
b = y + r sin(θ) H[a,b,r] += 1
end
end
code:
首先要使用hough变换要对输入图片预处理:转化成灰度图、边界检测:
cvtColor(ori, copy_img, COLOR_RGB2GRAY);
Canny(copy_img, cannying, 20, 255, 3, false);
hough_count是三维数组存储圆心半径的投票;a/b对应hough空间的圆心,hough和源空间一致对应的就是源空间的圆心;
for (int ro = 0; ro < rows; ro++) {
//cout << "r:" << ro << "\n";
for (int co = 0; co < cols; co++) {
if (cannying.at<uchar>(ro, co)!=0) {//ptr error
//cout << "ro:" << ro << " co:" << co << "cannying.ptr<uchar>(ro, co):" << int(cannying.ptr<uchar>(ro, co) )<< "\n";
edgenum++;
for (int r = min_r; r <= max_r; r++) {
for (float theta = 0; theta <2*pi; theta += 0.1) {//2pi 范围内投票更准确
int a = co - r * cos(theta), b = ro + r * sin(theta);
//cout << "a:" << a << " b:" << b << " r:" << r << "\n";
if (a > 0 && a < cols && b>0 && b < rows) {
hough_count[a][b][r]++;
}
}
}
}
}
}
错误使用指针,直接用来指针ptr判断是否为0,野指针不代表指针内容,应该是*ptr
这里是 θ \theta θ 选择0–2pi更准确;
然后通过得分结果选择阈值显示圆:
createTrackbar("alpha", newwin, &alpha, 80, thresh_draw);
for (int ro = 0; ro < rows; ro++) {
for (int co = 0; co < cols; co++) {
for (int r = 0; r <= max_r; r++) {
if (hough_count[co][ro][r] >= alpha) {
//cout << hough_count[co][ro][r] << "\n";
circlenum++;
Point center(co, ro);
circle(new_img, center, 3, Scalar(0, 255, 0), -1, 8, 0);
circle(new_img, Point(co, ro), r, Scalar(0, 0, 255),3,8,0);//Point (x,y)
}
}
}
}
Point (x,y) ,x与column对应,y与row对应;
结果:显示结果不错;
然后使用标准的HoughCircles绘制,查阅其中的参数使用:
HoughCircles( InputArray image, OutputArray circles,int method, double dp, double minDist,(认为是同一圆心的距离)double param1 = 100,(canny相关) double param2 = 100,(圆心投票相关)int minRadius = 0, int maxRadius = 0 );
dp:表示将原图按照1:2,或者1:1进行操作,直接使用1;
minDist:表示在这个距离内算作同一个圆心;
其他参数为具体过程投票的阈值;
minRidus、maxRadius表示半径表示的阈值。
这种方法通过调节参数能取得不错的效果,改变其中的minR、maxR运行时间长,遍历运算量大;
梯度引入
考虑使用梯度可以直接确定一个大致的圆心所在的方向,使用sobel算子通过梯度确定圆心的方向。
do_hough_gradient();
//sobel 计算梯度
Sobel(cannying, img_dx, CV_32F, 1, 0);
Sobel(cannying, img_dy, CV_32F, 0, 1);
//创建三个调节条
createTrackbar("min_r", gradientwin, &min_r, 200, sobel_gradient);
createTrackbar("max_r", gradientwin, &max_r, 400, sobel_gradient);
createTrackbar("alpha", gradientwin, &alpha, 80, thresh_draw_gradient);
opencv的sobel算子:方向和图像的xy方向一致,对于梯度直接计算:每次计算清除原本记录矩阵的内容,然后遍历通过x、y两个方向dx,dy确定梯度相对于x正方向的偏向角然后遍历半径范围记录:
void sobel_gradient(int, void*) {
memset(hough_gradient, 0, sizeof(hough_gradient));//清零
for (int ro = 0; ro < rows; ro++) {
for (int co = 0; co < cols; co++) {
if (cannying.at<uchar>(ro, co) != 0) {
float cur_dx = img_dx.at<float>(ro, co), cur_dy = img_dy.at<float>(ro, co);
double theta0 =cur_dx!=0? atan(cur_dy / cur_dx):pi/2;//cur_dx-->theta=nan error
// cout << theta0 << "gradient angle \n";
for (float theta = theta0 - 0.5; theta < theta0 + 0.5; theta += 0.1) {
for (int r = min_r; r <= max_r; r++) {
int a = co + r * cos(theta), b = ro + r * sin(theta);
if (a > 0 && a < cols && b>0 && b < rows) {
hough_gradient[a][b][r]++;
}
}
}
}
}
}
thresh_draw_gradient(alpha,0);//阈值相同处理
数组起始清零,alpha的开始不能设置太小否则画的圆太多也会在起始有一定卡顿,得到的图效果不好
一开始得到这种图,以为是坐标变换不对,实际是没有对dx=0特殊处理出现的角度大部分是nan;
更新错误:上面错误是由于sobel滤波之后存储浮点数,这里存储的是cv_16s是整数,错误;修改后效果不错,在梯度周围较小范围内投票效果较好。
Tips:
在进行霍夫变换之前做滤波:但是通过调阈值基本可以解决,没看出有太大区别,只是在canny之后的边界中去除了很多噪音。
gassian后:
这张图右边的硬币总是检测不到,但是轮廓是比较清楚的,我就想是坐标变换后rows和cols某个地方边界判断错误,果真是a,b边界写反了导致那里没检测到,调整后:
没用滤波实验效果会好点,考虑滤波消除掉某些特征。
基本暴力更多测试例子:
对于半径相差较大的准确率不会很高,在自带的函数里面存在同样的问题,考虑算法本身的minDist参数,minR,maxR参数限制了R差别较大的问题;上图分辨率较高,修改大小:
resize(ori, ori, Size(500, 500), 0, 0, INTER_LINEAR);
虽然有上面的结论,但是这张图效果却不错,考虑自己实现没有使用minDist参数。
使用含有梯度的:
其中theta的值不是很准确表示梯度方向,要对theta取一定范围的值来解,复杂度还是比原来降低了,调节bar也流畅了,但是准确率和速度成反比关系,取值越多,也趋向于枚举做法,越精准,但速度也变慢。
实验总结
- 枚举求解
- 实验细节实现,多cout输出
- sobel算子梯度进一步使用
- opencv本是实现更巧妙
霍夫梯度法的原理
- 估计圆心
1.把原图做一次 Canny 边缘检测,得到边缘检测的二值图。
2.对原始图像执行一次 Sobel 算子,计算出所有像素的邻域梯度值。
3.初始化圆心空间 N(a,b),令所有的 N(a,b)=0。
4.遍历 Canny 边缘二值图中的所有非零像素点,沿着梯度方向 ( 切线的垂直方向 )画线,将线段经过的所有累加器中的点 (a,b) 的 N(a,b)+=1。
5.统计排序 N(a,b),得到可能的圆心(N(a,b) 越大,越有可能是圆心)
- 估计半径(针对某一个圆心 (a,b))
1.计算 Canny 图中所有非 0 点距离圆心的距离。
2.距离从小到大排序,根据阈值,选取合适的可能半径(比如 3 和 3.5 都被划为半径值 3 中)。
3.初始化半径空间 r, N®=0。
4.遍历 Canny 图中的非 0 点,N( 距离 )+=1。
5.统计得到可能的半径值(N® 越大,说明这个距离值出现次数越多,越有可能是半径值)。
- 估计半径(针对某一个圆心 (a,b))
1.计算 Canny 图中所有非 0 点距离圆心的距离。
2.距离从小到大排序,根据阈值,选取合适的可能半径(比如 3 和 3.5 都被划为半径值 3 中)。
3.初始化半径空间 r, N®=0。
4.遍历 Canny 图中的非 0 点,N( 距离 )+=1。
5.统计得到可能的半径值(N® 越大,说明这个距离值出现次数越多,越有可能是半径值)。