[计算机视觉] A4纸边缘检测

来源:互联网 发布:sqlserver新建数据库 编辑:程序博客网 时间:2024/04/29 04:18

这次作业真的是再见再见……虽然最后写出的代码没多少行,但是很烧脑,需要用到很多高中的几何数学知识(差不多忘光了_(:зゝ∠)_)和一些算法。然后最后写完发现有些方法挺巧妙的,所以在这里分享一下。


先把作业要求放上来吧:

输入图分辨率:3120*4208

(输入图像的红色部分仅为马赛克处理哈,原输入图像不存在)


好,那接下来po一下做出的效果图


(边缘有点细_(:зゝ∠)_,得仔细看才能看出。用时也给出来了,四舍五入后 1张图片的处理到最后显示只用9秒。)


为什么提了时间呢?因为上面也提到了,输入图像的分辨率大得感人啊微笑不做点处理都不知道跑到什么时候才能跑出结果。好了,下面会先大概说一下整个处理的所有步骤,接着会有每个步骤的详细解释。


处理步骤:

1、先对输入图像进行灰度化处理,再按一定比例下采样(即对图像插值压缩)

2、利用上一次作业的Canny算法提取边缘(得到的边缘有噪点且不完整)

3、对ImageSpace做HoughSpace(极坐标)变换,并做voting投票(矩阵累加)

4、对累加矩阵取最大的一些值,得到边缘直线的斜率、截距,并画出边缘直线

5、求每两条直线之间交点,并累计该交点的直线交叉次数

6、取交叉次数最多的4个点,即为A4纸的四个顶点

7、把顶点坐标上采样并放回原图,最后4个顶点之间连线,形成边缘


步骤详细解释:

1、先对输入图像进行灰度化处理,再按一定比例下采样(即对图像插值压缩

      (1)灰度化处理:这个不用详细说了,都是数字图像的处理方法。灰度转化公式I = r * 0.299 + g * 0.587 + b * 0.114。

      (2)下采样:由于原图是3120*4208的分辨率,刚好都是8的倍数,所以我通过插值将宽高都变成原来的1/8,即390*526。插值方法网上很多教程啦,自己找下吧大笑

      @问题@:为什么可以进行下采样?会有精度丢失吗?

      @回答@:(这纯属我个人理解哈)首先,做下采样肯定是为了提高减少运行时间啦,上面也提到了处理分辨率如此高的一张图片最后只用了9 秒。而说到精度,精度丢失肯定会有的啦~~只是可以设想一下,如果原来边缘在图像的高度的1/4处,那我不管怎么放大缩小,那边缘还是在图像的高度的1/4处啊~~从上面的处理结果来看,其实效果我觉得还是可以的,虽然有一定误差,但是应该在可以接受的范围吧_(:зゝ∠)_


步骤1结果(这步骤都是简单的图像处理,所以就不放代码啦~~):

   


2、利用上一次作业的Canny算法提取边缘(得到的边缘有噪点且不完整):

      我上次的作业改的code0的代码啊。400多行,就不放上来了_(:зゝ∠)_  改一下那份代码就能用了。


步骤2结果:

  


3、对ImageSpace做HoughSpace(极坐标)变换,并做voting投票

      课件上有介绍直接的Hough坐标变化b = -x * m + y(不用极坐标),但是输入图像的很多直线的截距b都特别大(最大的接近1w),这样的累加矩阵就会很大很大!所以我用了极坐标控制矩阵大小。变换及几何意义如下:


      好了,有了这些几何知识就可以编码了。先声明一个累加矩阵:360 * ((x + y) * 2)。宽360是360度,高原来又平方又开方太复杂了,所以直接用更大(可接受范围)的矩阵,乘2倍,是把负的部分平移上来可以累加。

      然后编码思路:先在canny提取的边缘图像上,对于边缘点得到x、y。然后在Hough空间,对每一个θ,由x、y计算出ρ:

void HoughEdgeDetect::HoughTransAndDetectEdge() {cimg_forXY(SrcCannyImg, x, y) {if (SrcCannyImg(x, y, 0) == 255) {//cout << "(" << x << ", " << y << ")" << endl;accumulateTheHoughArray(x, y);}}//......}void HoughEdgeDetect::accumulateTheHoughArray(int x, int y) {for (int i = 0; i < 360; i++) {double theta = (double)i * Pi / 180.0;int rho = (int)round(sin(theta) * (double)y + cos(theta) * (double)x);if (rho < imgW + imgH) {houghArray[(rho + imgW + imgH) / RhoScale][i]++;//cout << "houghArray[" << (rho + imgW + imgH) / RhoScale << "][" << i << "] = "//<< houghArray[(rho + imgW + imgH) / RhoScale][i] << endl;}}}

步骤3结果:Hough空间的局部图像:

  


4、对累加矩阵取最大的一些值,得到边缘直线的斜率、截距,并画出边缘直线

      由刚得到的累加矩阵,原理上累计最多的4个点即为A4纸四条边的θ、ρ参数,但实际不然,会发现同一条边会有多组累计数都很高的参数(下面结果图可见),即前4个累计最多的点不能反映4条边。因此我在实验中一直调到取前12个点才能把所有图像的4条边都检测出来_(:зゝ∠)_:

      (1)这里需要从一堆数据中找到前topK个数,并获取其在数组里的坐标。这可比找一堆数据中的最大值复杂好多_(:зゝ∠)_。我是用了最小堆来实现:当前堆里topK个数的最小值放堆顶,当检测到比堆顶大的数,去掉堆顶的值,然后把刚检测到的数插入堆。

      (2)同时声明两个存储斜率k和截距b的数组。当在堆里插入累加值时,根据其θ、ρ参数可以求出k、b值。接着找到在堆里插入的位置,然后在数组里插入改k、b值。

//找线交叉最多的TopK个θ、ρ参数对void HoughEdgeDetect::findTheTopKParaPair() {for (int i = 0; i < (imgW + imgH) * 2 / RhoScale; i++) {for (int j = 0; j < 360; j++) {if (houghArray[i][j] != 0) {if (myHeap.size() < TopK) {  //堆里不够TopK个数,直接插入myHeap.insert(houghArray[i][j]);modifyTheParaArray(i, j);}else {  //堆里够了TopK个数,若检测到比堆顶大的数,先移除堆顶元素再插入IntHeap::iterator pIter = myHeap.begin();if (houghArray[i][j] > *pIter) {myHeap.erase(pIter);myHeap.insert(houghArray[i][j]);modifyTheParaArray(i, j);}}}}}}//根据θ、ρ参数获得b、m并插入数组void HoughEdgeDetect::modifyTheParaArray(int i, int j) {int index = 0;IntHeap::iterator tIter = myHeap.begin();//先获取在堆里插入的位置indexwhile (tIter != myHeap.end()) {if (*tIter == houghArray[i][j]) {break;}tIter++;index++;}for (int k = 0; k < index; k++) {k_TopList[k] = k_TopList[k + 1];b_TopList[k] = b_TopList[k + 1];}k_TopList[index] = -1.0 / tan((double)j * Pi / 180.0);b_TopList[index] = (double)(i * RhoScale - imgW - imgH) / sin((double)j * Pi / 180.0);}

画出边缘直线:

CImg<int> HoughEdgeDetect::getCannyGrayImageWithEdge() {CImg<int> answer = CImg<int>(SrcCannyImg._width, SrcCannyImg._height, 1, 3, 0);cimg_forXY(answer, x, y) {answer(x, y, 0) = SrcCannyImg(x, y, 0);answer(x, y, 1) = SrcCannyImg(x, y, 0);answer(x, y, 2) = SrcCannyImg(x, y, 0);}const double yellow[] = { 255, 255, 0 };const double green[] = { 0, 255, 255 };const double red[] = { 255, 0, 0 };const double purple[] = { 255, 0, 255 };for (int i = 0; i < TopK; i++) {double k = k_TopList[i];double b = b_TopList[i];//cout << "k = " << k << " , b = " << b << endl;//根据斜率、截距得到在图像区域内的两个点,然后画直线if (abs(k) < 1) {if (i >= TopK - 4)answer.draw_line(0, (int)round(b), imgW, (int)(round((double)imgW * k + b)), green);elseanswer.draw_line(0, (int)round(b), imgW, (int)(round((double)imgW * k + b)), purple);}else {   //abs(k) >= 1if (i >= TopK - 4)answer.draw_line((int)round(-b / k), 0, (int)round(((double)imgH - b) / k), imgH, green);elseanswer.draw_line((int)round(-b / k), 0, (int)round(((double)imgH - b) / k), imgH, purple);}}return answer;}

步骤4结果:

  

(可以看到边缘会有很多条直线经过,特意标注蓝色为累计数最多的4条直线,可以看到这4条直线中甚至有2-3条都过同一个边缘)


5、求每两条直线之间交点,并累计该交点的直线交叉次数

      可以从步骤4结果看到,基本边缘都出来了。但是!!! 右边的结果图很明显有一条无关的直线!回到原图看,那原来是桌子边缘!(这真的是坑!要咋整啊_(:зゝ∠)_)

      然而办法还是有的!我们可以仔细看下上面右边的图,4个边缘的直线一般都是有多条直线经过,而那条多余的线貌似只有一条。那也就是说:当有另外一条线与真实边缘线 or 多余的那条直线相交时,交点区域(即多个交点集中的区域)内的点的个数肯定是真实边缘线的比多余线的多!(貌似有点难理解_(:зゝ∠)_)那接下来就用代码证实!

    (1)找两条直线的交点:利用高中数学知识,可以根据两对k、b得到两直线的交点。同时我们需要检测交点是否在图像区域内。

    (2)有可能某个交点就在另一交点的附近(比如相差一个像素),此情况下我们把两个点看做同一个点,即需要合并,并累加两次。

(此部分代码与下一部分有合并,因此在下一部分给出)


步骤5结果:

  

 分析:代码里,点的大小与交点的直线交叉数成正比。所以可以很明显看出,右图中A交点明显比另外4个点小,也就是直线交叉数 比其他点少,即说明了我们上面的推论。


6、取交叉次数最多的4个点,即为A4纸的四个顶点

      有了步骤5,我们就好做了。步骤5已经得到所有的交点集。此时我们只需找到交叉数最多的4个点即可!

struct Vertex {int x;int y;int crossTimes = 0;Vertex(int posX, int posY): x(posX), y(posY) {}void setXY(int _x, int _y) { x = _x;y = _y;}void addCrossTimes() {crossTimes++;}};void HoughEdgeDetect::findTheNearest4Points() {//先对每个交点做相交次数累加for (int i = 0; i < TopK; i++) {double k1 = k_TopList[i];double b1 = b_TopList[i];for (int j = 0; j < TopK; j++) {if (i != j) {double k2 = k_TopList[j];double b2 = b_TopList[j];getValidCrossPointAndIncrement(k1, b1, k2, b2);}}}//找相交次数最多的4个点int max = 0;int maxUnder = -1;while (top4vertexSet.size() < 4) {max = 0;for (int i = 0; i < vertexSet.size(); i++) {if (vertexSet[i].crossTimes > max) {max = vertexSet[i].crossTimes;maxUnder = i;}}top4vertexSet.push_back(vertexSet[maxUnder]);vertexSet[maxUnder].crossTimes = -1;}}//根据两条直线的斜率、截距获取交点,检测是否在图像区域内//对交点做交叉次数做累加void HoughEdgeDetect::getValidCrossPointAndIncrement(double k1, double b1, double k2, double b2) {double xd = (b2 - b1) / (k1 - k2);int x = (int)round(xd);int y = (int)round(xd * k1 + b1);if (x >= 0 && x <= imgW && y >= 0 && y <= imgH) {  //在图像区域内int i = 0;for (i = 0; i < vertexSet.size(); i++) {int oldX = vertexSet[i].x;int oldY = vertexSet[i].y;//附近有特别靠近的点,可以合并if ((oldX - x) * (oldX - x) + (oldY - y) * (oldY - y) <= VertexGap) {vertexSet[i].addCrossTimes();break;}}if (i == vertexSet.size()) {  //如果该点附近没有距离特别近的点,自身作为一个新点Vertex newVertex(x, y);vertexSet.push_back(newVertex);}}}

对4个交点画圆(由于画点draw_point只能是一个像素,看不清,所以画圆):

//相交次数最多的4个点,描出来for (int i = 0; i < top4vertexSet.size(); i++) {answer.draw_circle(top4vertexSet[i].x, top4vertexSet[i].y, 5, yellow, 1.0f);}

步骤6结果:

  


7、把顶点坐标上采样放回原图,最后4个顶点之间连线,形成边缘

      (1)顶点坐标上采样,并放回原图:由于一开始是下采样了8倍,所以这时候将顶点横纵坐标乘8,并在原图画出

      (2)有了4个顶点,怎么连线?可能有同学会想4个点两两连线不就可以了么!但是!!! 对角线呢???对角线不能连。那怎么判断两个点处于对角线位置呢?看下图!


      是不是有idea了~~对了!若是对角线连线,另外两个点肯定一个在连线的上方,一个在下方!用y坐标减一下就能识别了!找到对角点,就分别跟另外两个点连接就好啦~~

CImg<int> HoughEdgeDetect::getFinallyProcessedImage(const CImg<int>& SrcImg) {CImg<int> answer = CImg<int>(SrcImg._width, SrcImg._height, 1, 3, 0);cimg_forXY(answer, x, y) {answer(x, y, 0) = SrcImg(x, y, 0, 0);answer(x, y, 1) = SrcImg(x, y, 0, 1);answer(x, y, 2) = SrcImg(x, y, 0, 2);}const double yellow[] = { 255, 255, 0 };for (int i = 0; i < top4vertexSet.size(); i++) {answer.draw_circle(top4vertexSet[i].x * DownSampledSize, top4vertexSet[i].y * DownSampledSize, 5 * DownSampledSize, yellow, 1.0f);}drawLinesBetweenVertex(answer);return answer;}//给4个顶点之间画直线void HoughEdgeDetect::drawLinesBetweenVertex(CImg<int>& img) {const double yellow[] = { 255, 255, 0 };int crossPoint = 0;for (int i = 1; i < 4; i++) {   //第0个点与第i个点连线double temp_k = (double)(top4vertexSet[i].y - top4vertexSet[0].y) / (double)(top4vertexSet[i].x - top4vertexSet[0].x);double temp_b = (double)top4vertexSet[0].y - temp_k * (double)top4vertexSet[0].x;int flag = 0;  //标志为正还是为负for (int j = 1; j < 4; j++) {if (j != i) {//第j个点的y坐标减线上坐标double diff = (double)top4vertexSet[j].y - (temp_k * (double)top4vertexSet[j].x + temp_b);if (flag == 0) {flag = diff > 0 ? 1 : -1;}else {if (flag == 1 && diff <= 0 || flag == -1 && diff > 0) {crossPoint = i;break;}}}}if (crossPoint != 0)break;}for (int i = 1; i < 4; i++) {if (i != crossPoint) {img.draw_line(top4vertexSet[i].x * DownSampledSize, top4vertexSet[i].y * DownSampledSize,top4vertexSet[0].x * DownSampledSize, top4vertexSet[0].y * DownSampledSize, yellow);img.draw_line(top4vertexSet[i].x * DownSampledSize, top4vertexSet[i].y * DownSampledSize,top4vertexSet[crossPoint].x * DownSampledSize, top4vertexSet[crossPoint].y * DownSampledSize, yellow);}}}

步骤7结果(最后结果):



(边缘有点细,小图显示不出来_(:зゝ∠)_)


好啦,搞定~~~



2 0