使用Hough转换检测直线

一,使用背景

对图像进行边缘检测,我们可以得到图像的边缘图,就是只有在图像中处于边缘位置上的点的像素值才有可能大于0,其余位置的像素值均为0。想通过边缘图像提取出边缘所在的直线,然后在原图像中画出来,我们就可以借助 hough 转换来实现。也可以说,在使用 hough 转换检测直线之前,应该获取图像的边缘图像。


二,原理

图像空间中的一条线可以使用 y=m0x+b0 来表示。此时如果有一个m,b二维空间,那么图像上的一条直线就对应m,b空间上的一个点。映射关系如下图所示:

这里写图片描述

而对于图像上的一点 (x0,y0) ,我们可以设经过它的所有直线(垂直于x轴的直线除外)为 y0=mx0+b ,也就是说图像空间上的一个点,对应m,b空间的一条直线。映射关系如下图所示:

这里写图片描述

再考虑图像空间上的两个点 (x0,y0) (x1,y1) ,那么它们映射到m,b空间上就是两条直线,当 x0!=x1 的时候,映射到m,b空间上的两条线就肯定会有一个交点,这个交点就是图像空间中由两个点决定的一条线映射过来的。映射关系如下图所示:

这里写图片描述

假设现在我们有一张图像,图像上只有一条直线 y=m0x+b0 以及直线外一点 (x1,y1) 。对于图像直线上的所有点,映射到m,b空间上,就是许多直线,然后这些直线都有一个公共的交点 (m0,b0) 。此时再考虑图像直线之外的那个点,映射到m,b空间上就是一条直线 l0 。毫无疑问, l0 和m,b空间中的其他直线也会有交点,代表着原图像中 (x1,y1) 与直线上点的连线。但是在m,b空间中,通过 (m0,b0) 的直线数目肯定会比通过其他点的直线数目多。至此我们可以看到在图像空间中的一条直线,映射到m,b空间上的一个点,在m,b空间中通过这个点的直线的数目肯定会比通过其他点的直线数目多。这就给我们一个启示,我们可以将边缘图像上的所有像素值不为0的点映射到m,b空间上,然后再统计m,b空间中通过直线数目最多的一些点,那么这些点就是原图像中存在的直线。
以上就是我对Hough转换的理解,其中m,b空间就是hough空间。而统计通过点的直线数目实际就是一个投票的过程。在编程的时候,对于m,b我们肯定要取离散的值,(连续的值计算机无法表示)。所以对于边缘图像上的一个点,我们可以遍历所有m的取值,算出b,取整,然后在hough空间中对每个m,b对进行投票。图像上点的投票如下图所示:

这里写图片描述

右图中一个小格子代表一组m,b对,小格子中有几条直线代表那组m,b对得到的票数。

在实际编程中我们不会使用 y=mx+b 取表示一条直线,一个原因是这种表示方法无法表示垂线。还有一个原因就是我们往往无法知道m,b的取值范围。
实际上,图像上的每一条直线都可以表示为 r=xcosθ+ysinθ ,其中 r 表示图像中坐标原点到直线的距离, θ 则是过原点到直线的垂线与x轴的夹角。具体的几何关系如下所示:

这里写图片描述

在 r, θ 空间中,首先所有直线都可以得到表示。其次,r, θ 的值的范围我们也可以很容易算出。 θ 我们一般取 0 180 (这样 r 的值可以是负的,但绝对值还是表示原点到直线的距离)。而由于 r 表示原点到直线的距离,图像的大小是一定的,所以 r 最大不可能超过图像对角线的长度。
总的来说,hough变换的过程就是将边缘图像上的每一个边缘点映射到 r , θ 的hough空间上。然后hough空间上有一个 2*2 的累加器,横轴是 θ ,纵轴是 r 。映射过程使用公式 r=xcosθ+ysinθ 对所有 θ 进行遍历,然后在累加器上某个位置加一。这样将边缘图像上的所有点映射到hough空间之后,找出累加器上的一些局部最大点,这些点就是我们检测到的直线。当然,我们也可以通过设置阈值来限制检测出来直线的数目。得到hough空间中局部最大值点之后,我们还需要将点转换为图像空间的线。具体的转换可以根据一下的公式来进行。

x=rysinθcosθ

y=rxcosθsinθ


三,实现细节

在对图像空间上的点映射到hough空间的过程中,因为我们hough空间的 r θ 取的都是整数,这就会导致图像空间中的一点线在hough空间中有多个点对应着,而这会导致在后空间中对图像空间一条直线对应的 r θ 的票数分散,最终可能出现图像中某些长的直线没被检测出来,而短的直线反而被检测出来了。例如下图,本来是为了检测纸张的四条边缘,结果纸张的下边缘没被检测出来,而另外一张值的一条短边缘反而被检测到了。

这里写图片描述

然后调整阈值使检测到的直线数目多一些,可以看到纸张的下面缘周围有很多直线被检测出来,这就是一条边缘对应hough空间多个点的现象,导致了票数的分散。而再看另外一张纸的短边缘,边缘附近只有比较少的直线,可以知道该边缘的票数比较集中,所以最终它被检测出来了。

这里写图片描述

解决的办法可以是在找到累加器中的局部最大值之后,对每一个最大值做一个聚类。就是遍历每一个局部最大值点,检测一定范围内有没有另外的局部最大值点,如果有,则将另一个点的票数加到当前的点上,然后另一个点的票数置为0。通过这样的处理,就可以一下的结果,也就是检测出纸张的四条边缘。

这里写图片描述


四,C++代码实现

代码的使用到的第三方库是一个C++的库CImg。主要功能是检测A4纸的四条边缘。
将hough变换封装成一个类。类的.h文件如下:

#ifndef _HOUGH_
#define _HOUGH_

#include "canny.cpp"
#include <vector>
#include <algorithm> 
#include <iostream>

using namespace std;

#define PI 3.14159265359
#define threshold 100

struct Position {
    int rowindex;
    int colindex;
    int votenum;
};

struct Point {
    int x;
    int y;
    Point(int _x, int _y) {
        x = _x;
        y = _y;
    }
    Point() {
        x = -1;
        y = -1;
    }
};

class Hough {
public:
    CImg<unsigned char> edge;  //待处理的边缘图像
    CImg<unsigned char> img;  //原图像
    int edge_w;
    int edge_h;
    int acc_w;  //累加器的宽度
    int acc_h;  //累加器的高度
    int center_h;  //由于r有正有负,所以应将计算的r值加上这个值才能代表累加器上的某个位置
    vector<Point> vertex;  //用来存储检测到纸张的四个顶点
    int **acc;  //累加器,二维数组

    Hough(const char *filename);
    ~Hough();
    void detectline();  //检测直线的接口函数,调用以下的函数
    vector<Position> vote();  //对图像上边缘点的映射,然后统计在hough空间上的票数
    vector<Position> gethighestvote(vector<Position> v);  //在hough空间的最值点中筛选出代表纸张边缘的四个点
    void drawline(vector<Position> v);  //在原图像上画出四条边缘
    void drawpoint(vector<Position> v);  //画出四条边缘的四个交点

};

#endif

以下是主要函数的代码。
检测直线的接口函数。

void Hough::detectline() {
    edge.display();
    vector<Position> v = vote();
    v = gethighestvote(v);
    drawline(v);
    drawpoint(v);
    img.display();
}

进行hough转换并投票的函数。

vector<Position> Hough::vote() {
    cimg_forXY(edge, x, y) {  //对每个边缘点在另一个坐标系上进行投票
        if (edge(x, y) > 0) {  //该点是边缘点。
            for (int i = 0; i < 180; i++) {
                double angle = (double)i / 180 * PI;  //角度制转换为弧度值
                double dr = (double)x * cos(angle) + (double)y * sin(angle);
                int r = round(dr);
                acc[r + center_h][i]++;  //r可为负值,加上矩阵中心
            }
        }
    }

    vector<Position> v;

    for (int i = 0; i < acc_h; i++) {  //找出投票数局部最大的r, theta组合。
        for (int j = 0; j < acc_w; j++) {
            if (acc[i][j] > threshold) {
                int flag = 1;
                if (i > 0) {
                    if (j > 0) {
                        if (acc[i][j] < acc[i-1][j-1]) flag = 0;
                        if (acc[i][j] < acc[i][j-1]) flag = 0;
                    }
                    if (j < acc_w - 1) {
                        if (acc[i][j] < acc[i-1][j+1]) flag = 0;
                        if (acc[i][j] < acc[i][j+1]) flag = 0;
                    }
                    if (acc[i][j] < acc[i-1][j]) flag = 0;
                }
                if (i < acc_h - 1) {
                    if (j > 0) {
                        if (acc[i][j] < acc[i+1][j-1]) flag = 0;
                    }
                    if (j < acc_w - 1) {
                        if (acc[i][j] < acc[i+1][j+1]) flag = 0;
                    }
                    if (acc[i][j] < acc[i+1][j]) flag = 0;
                }

                if (flag == 1) {
                    Position po;
                    po.rowindex = i;    //r
                    po.colindex = j;    //theta
                    po.votenum = acc[i][j];
                    v.push_back(po);  将局部最大值的点存进一个vector中
                }
            }
        }
    }

    return v;
}

对极大值点进行阈值处理和聚类处理,最终在vector中只保留纸张四条边缘对应的hough空间的点。

vector<Position> Hough::gethighestvote(vector<Position> v) {
    sort(v.begin(), v.end(), cmp);        //按照投票数对直线参数进行排序
    vector<Position>::iterator iter;
    vector<Position>::iterator iter1;
    for (iter = v.begin(); iter != v.end(); iter++) {
        for (iter1 = iter+1; iter1 != v.end(); iter1++) {
            if (abs(iter->rowindex - iter1->rowindex) < 60 && iter->votenum != 0 && iter1->votenum != 0) {  //对一些r值相近的点进行聚类
                iter->votenum = iter->votenum + iter1->votenum;
                iter1->votenum = 0;
            }
        }
    }

    sort(v.begin(), v.end(), cmp);
    while(v.size() > 4) {  //只保留纸张的四条边缘对应的点
        v.pop_back();
    }
    return v;
}

在原图像画纸张边缘。

void Hough::drawline(vector<Position> v) {
    vector<Position>::iterator iter;
    vector<Position>::iterator iter1;
    for (iter = v.begin(); iter != v.end(); iter++) {        //将投票数最多的直线画在原图像上
        int x1, y1, x2, y2;
        x1 = y1 = x2 = y2 = 0;
        double angle = (double)(iter->colindex) / 180 * PI;
        double si = sin(angle);
        double co = cos(angle);
        if (iter->colindex >= 45 && iter->colindex <= 135) {        //在这个范围内sin值比较大,使用sin做分母误差较小
            x1 = 0;
            y1 = (iter->rowindex - center_h) / si;        //加上之前减去的值才是真正的r
            x2 = edge_w - 1;
            y2 = ((iter->rowindex - center_h) - (double)x2 * co) / si;
        } else {
            y1 = 0;
            x1 = (iter->rowindex - center_h) / co;
            y2 = edge_h - 1;
            x2 = ((iter->rowindex - center_h) - (double)y2 * si) / co;
        }
        const unsigned char color[] = {255, 0, 0};
        img.draw_line(x1, y1, x2, y2, color);
    }
}

求出纸张边缘交点,并在原图像中标出。

void Hough::drawpoint(vector<Position> v) {
    vector<Position>::iterator iter;
    vector<Position>::iterator iter1;
    for (iter = v.begin(); iter != v.end(); iter++) {        //求出直线间的交点
        double angle0 = (double)(iter->colindex) / 180 * PI;
        double si0 = sin(angle0);
        double co0 = cos(angle0);
        int r0 = iter->rowindex - center_h;
        for (iter1 = v.begin(); iter1 != v.end(); iter1++) {
            if (iter == iter1) continue;
            double angle1 = (double)(iter1->colindex) / 180 * PI;
            double si1 = sin(angle1);
            double co1 = cos(angle1);
            int r1 = iter1->rowindex - center_h;

            int deta = iter->colindex - iter1->colindex;
            if (abs(deta) < 30 || abs(deta) > 150) continue;        //两直线夹角太小不是我们要求的纸的顶点
            double detaangle = (double)deta / 180 * PI;
            int x = (double(r1)*si0 - double(r0)*si1) / sin(detaangle);
            int y;
            if (iter->colindex >= 30 && iter->colindex <= 150) {
                y = ((double)r0 - double(x)*co0) / si0;
            } else {
                y = ((double)r1 - (double)(x)*co1) / si1; 
            }
            if (x < edge_w && x >= 0 && y < edge_h && y > 0) {
                int flag = 0;
                int deta = 5;
                for (int i = x - deta; i <= x + deta; i++)
                    for (int j = y - deta; j <= y + deta; j++)
                        if (img(i, j ,0) == 0 && img(i, j, 1) == 0 && img(i, j, 2) == 255) {
                            flag = 1;
                            break;
                        }
                    if (flag == 1) break;
                if (flag == 0) {
                    const unsigned char color[] = {0, 0, 255};
                    img.draw_point(x, y, color);
                    Point p(x, y);
                    vertex.push_back(p);
                }
            }
        }
    }
}

参考博客:Hough Transformation - C++ Implementation
代码地址:hough转换

猜你喜欢

转载自blog.csdn.net/huinsysu/article/details/68336845
今日推荐