检测直线
边缘检测
首先对图片进行处理,使用上次的代码生成边缘图。(参考上一篇博客)
由于这次的目的是为了检测A4纸的边缘,所以不需要中间文字的小边缘,可以在计算sobel算子时增加亮度梯度阀值。
由于这次给的图片尺寸很大,这样计算太慢,所以我只采用了高斯模糊和sobel的步骤,并使用CImg的函数来进行灰度化和高斯模糊,并将sobel算子替换为更简单的Prewitt算子,并使用CImg内置的邻域矩阵CImg_3x3和循环cimg_for3x3来加快计算速度。
void Hough::Prewitt()
{
gradnum = CImg<double>(img.width(), img.height(), 1, 1, 0);
// 定义3*3领域矩阵I
CImg_3x3(I, double);
// 遍历计算梯度值
cimg_for3x3(gFiltered, x, y, 0, 0, I, double) {
const double ix = Inc - Ipc;
const double iy = Icp - Icn;
double grad = std::sqrt(ix*ix + iy * iy);
// 梯度大于阈值才赋值
if (grad > gradLimit) {
gradnum(x, y) = grad;
}
}
}
霍夫变换
声明一个霍夫空间,x轴为角度,值域0到PI,分为180份,纵轴为原图对角线的长度,这是由直角坐标系转极坐标系的关系得到的。注意这里theta并不需要0-2PI,因为0-PI就可以表示出直线的所有角度,用0-2PI的话反而会因为0和2PI表示同一条直线,但在霍夫空间的距离却很远,导致在相邻直线检测时无法很好区分。圆的话还是用2PI比较好。
houghImage = CImg(theta, max_length, 1, 1, 0);
对直角坐标系上的每个点,计算经过该点的直线在霍夫空间的坐标,对应像素值加一,这就是投票的过程。
void Hough::houghSpaceTransform()
{
houghImage = CImg<int>(theta, max_length, 1, 1, 0);
cimg_forXY(gradnum, x, y) {
int temp = gradnum(x, y);
if (temp != 0) {
for (int i = 0; i < theta; ++i) {
double r = x * cos(i*interval) + y * sin(i*interval);
if (r >= 0 && r < max_length) {
houghImage(i, r)++; //voting
}
}
}
}
}
运行后发现经过简化处理后的边缘与原来的边缘差别不大。
由于霍夫变换后曲线的交点并不一定真的交于一点,所以一般的做法是取一个区域内投票数最多的点。
void Hough::houghLinesDetect()
{
cimg_forXY(houghImage, x, y) {
if (houghImage(x, y) > min_votes) {
bool flag = false;
for (auto& c : peaks) {
if (distance(c.x - x, c.y - y) < min_distance) {
flag = true;
if (houghImage(x, y) > houghImage(c.x, c.y)) {
c = Point(x, y);
}
}
}
if (!flag) {
peaks.push_back(Point(x, y));
}
}
}
cout << peaks.size() << endl;
}
这里有两个参数
int min_votes = 200; //最小投票数
int min_distance = 200; //最小距离```
通过调整这两个参数,可以使得最后刚好检测出4条直线,如果检测出的直线十分靠近,可以考虑增加min_distance。
计算直角坐标
接下来就是根据得到的霍夫空间的坐标画出直线。这里要考虑两种情况,一是斜率不存在,二是斜率为0。所以最后写直线方程时我统一写成了ax+by+c=0的形式。
if (peaks[i].x > 0&&peaks[i].x<=360) { //斜率存在
if (lines[i].k != 0) {
result.draw_line(x0, y_min, x1, y_max, lines_color);
//cout << x0 << " " << y_min << " " << x1 << " " << y_max << endl;
}
else { //斜率为0
result.draw_line(x_min, y, x_max, y, lines_color);
}
}
else { //斜率不存在
result.draw_line(x, y_min, x, y_max, lines_color);
//cout << y_min << " " << x << endl;
}
然后求解直线交点,这里要注意由于浮点数精度问题,无法准确表现出PI/2,在计算三角函数值时会出错,需要单独处理。
double angle1 = peaks[j].x*interval;
double a1 = cos(angle1), b1 = sin(angle1), c1 = -peaks[j].y;
if (peaks[j].x == 90|| peaks[j].x == 270)
a1 = 0;
double D = a0 * b1 - a1*b0;
//cout << "D " << D <<" "<< a0 * b1 << " " << a1 * b0 << endl;
if (D != 0) {
int x = (b0*c1 - b1 * c0) / D;
int y = (a1*c0 - a0 * c1) / D;
//cout <<"P"<< (b0*c1 - b1 * c0) / D << endl;
if (x > 0 && x < width - 1 && y>0 && y < height - 1) {
points.push_back(Point(x, y));
}
}
效果
但是也有一些干扰比较大的图的识别效果不够好,就只好使用上次作业的边缘检测和边缘跟踪算法过滤掉干扰边缘。但是对于这样大小的图像时间可能需要几分钟,而使用简化的算法只需要数秒。
霍夫圆检测
原理
根据极坐标,圆上任意一点的坐标可以表示为如上形式。所以对于任意一个圆, 假设中心像素点p(x0, y0)像素点已知,圆半径已知,则旋转360°由极坐标方程可以得到每个点上的坐标。同样,如果只是知道图像上像素点, 圆半径,旋转360°则中心点处的坐标值必定最强。这正是霍夫变换检测圆的数学原理。
由于第二题的图片大小较小,并且干扰因素较多,所以可以直接用上次的代码进行边缘处理。
对于圆来说,霍夫空间的参数就是圆心的坐标,然后选择不同的半径进行投票,找到每次houghImage里面的最大投票数,这个投票数表示当前r的吻合程度,然后用投票数最大的r作为最好的r,票数超过阀值rLimit的就认为是一个圆的半径。
合理的选择r的区间(minR, maxR)可以有效减少遍历的次数,加快处理速度。
在确定了r之后,重新进行霍夫投票,将霍夫图像中投票数超过阀值的点对应圆心的坐标和值存入数组,这个值就代表了投票的结果,根据值进行排序,并判断检测出来的圆心坐标是否跟已检测的圆心坐标的距离,如果距离过小,默认是同个圆,最后输出结果。在这里,与圆形相关的像素就是圆心,用红色表示。
选择合适的参数可以加快运行速度,提高准确率,运行时间因图片大小不同在几分钟左右。
//圆
double minRadius = 200;
int minR = 150, maxR = 300;
int rLimit = 80; //筛选半径
int voteLimit = 40; //筛选圆心
效果
完整代码
https://github.com/CurryYuan/Computer-Vision/tree/master/Hough transform