一,使用背景
对图像进行边缘检测,我们可以得到图像的边缘图,就是只有在图像中处于边缘位置上的点的像素值才有可能大于0,其余位置的像素值均为0。想通过边缘图像提取出边缘所在的直线,然后在原图像中画出来,我们就可以借助 hough 转换来实现。也可以说,在使用 hough 转换检测直线之前,应该获取图像的边缘图像。
二,原理
图像空间中的一条线可以使用
而对于图像上的一点
再考虑图像空间上的两个点
假设现在我们有一张图像,图像上只有一条直线
以上就是我对Hough转换的理解,其中m,b空间就是hough空间。而统计通过点的直线数目实际就是一个投票的过程。在编程的时候,对于m,b我们肯定要取离散的值,(连续的值计算机无法表示)。所以对于边缘图像上的一个点,我们可以遍历所有m的取值,算出b,取整,然后在hough空间中对每个m,b对进行投票。图像上点的投票如下图所示:
右图中一个小格子代表一组m,b对,小格子中有几条直线代表那组m,b对得到的票数。
在实际编程中我们不会使用
实际上,图像上的每一条直线都可以表示为
在 r,
总的来说,hough变换的过程就是将边缘图像上的每一个边缘点映射到
三,实现细节
在对图像空间上的点映射到hough空间的过程中,因为我们hough空间的
然后调整阈值使检测到的直线数目多一些,可以看到纸张的下面缘周围有很多直线被检测出来,这就是一条边缘对应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);
}
}
}
}
}