应该是大半年前了,老师带领我做的一个项目主体部分已经完成了,但是投入运行的时候有一个很小的问题,就是需要在图像中知道一个圆形区域的边缘,由于这个圆形区域的半径是非递减随时间变化的而且圆心是固定的.当时解决的办法是找到变化的规律,然后将半径设置成随时间变化即可.不过此方法治标不治本,万一中途实际运行过程中发生了暂停类似时间,而程序还在计时,会有不同步的问题发生.后来希望由程序依托于图像自动找到变化的半径来解决这个问题,不过尽管最终识别的结果还算理想,但是考虑到系统已经投入测试运行了,修改的过程涉及到多次的测试过程,而且发现设置为随时间自动变化的效果还挺不错,就没把这段代码给嵌入进实际运行的系统中了.

半年过去了,当我回顾这段代码的时候,发现了以前写代码的一些问题,同时也希望再了解一下这个算法,下面就从要解决的问题和项目中的代码中窥探过去.

要识别的原图:

原图

 黄线为需要识别出来的圆

 

可能这样的图会比较好识别,但是如果是下面的这几副可能就没那么好识别了:

 

 

当时使用过大律法阈值判定,OpenCV的霍夫圆变换但效果都不是我想要的.

而且图中需要识别出来的也并非为完整的圆.

前面也说过了,这个是在测试现场提出来的一个需求,时间比较紧迫.可能我也没花太多时间去找更多的方法来适应这个问题.

好,下面是具体问题:

已知:

  • 圆心坐标,最小半径,最大半径.
  • (从图中也能看出我们要检测的圆形的半径是有最大值的,不会大过白色区域的区域半径.
  • 半径是确定的,因为摄像头的位置和下面需要检测的物体都是不会发生移动的.)

求解:

  • 在最小半径与最大半径之间找到不断变化的圆形半径.

 


 

我的思路:

寻找灰度值变化率大于某个阈值次数最多的那个半径.

  1. 按照角度递增选取圆上的点到圆心的连线,连线包含所经过的像素坐标及其灰度值.(效果图可以见图一)
  2. 计算每条连线的灰度变化值.
  3. 对每条连线的灰度变化值进行归一化处理.
  4. 选取大于某个阈值次数最多的那个半径. 

我认为不足之处在于最后的那个阈值的选取,这个阶段需要人工的提前通过测试样本选取出一个合理的值.

不过意识到这个问题后,我选取了满足条件次数最多的十条半径作为参考,因为实际过程中半径是非递减的而且不会发生突变,所以可以根据上一个获取的结果来从十个参考结果中选取一个较为合理的半径作为这次识别的最终结果.

(图一)

圆上的点到圆心的连线,白色是为了显示效果,实际为图像坐标对应的像素

(图二)

选取十个变化率最大的可参考半径

(图三)

选取变化率最大的那个半径

 

   嗯,看起来目前问题得到了很好的解决.实事也的确如此,我测试了一个大概有3k帧的图像,效果还是很不错的.

 

(图四)

几个预计比较难识别的图像


 

 

 

而且分析的时间满足肉眼对实时处理的要求(小于0.05s/帧,大于20FPS).

但是,当我打开了含有源代码的文件时,关于这个项目的介绍只有...

我很庆幸我写了时间,要不是工程文件名取得阔以:) 我真不知道这几个cpp是干嘛的.

但至少从这个项目中我已经开始了模块化的行为,使得我review省了点力.

 

1 //传递图像以便扫描 2  findInterCircle.scan(frame,scanValue); 3 //获取最佳半径 4 int raduis = findInterCircle.getBestRaduis()

 

复制代码
 1 void CCFindInterCircle::scan(Mat & _readImage,scanValue_s &_scanValue){  2  3 //图像数据预处理  4  preProcess(_readImage,_scanValue);  5  6 //开始数据处理  7  Process ();  8  9 //对计算结果的清理 10  basicClear(); 11 12 }
复制代码

 

主要的过程在Process()函数中.

这里有一个函数命名习惯不统一的问题.三个函数的首字母大小写应该相同,可能当时觉得这个函数很重要,不大写凸显不出与其他函数的不同.

哈哈.

 

复制代码
 1 void CCFindInterCircle::Process (){  2 size_t result = 0;  3  4 /*1*/  5 //生成与圆的顶点  6 result = calInterWithCircle();  7 //cout <<"生成了"<<result<<"个与圆的交点"<<endl;   8  9 /*2*/ 10 //获取连线的像素信息 11 result = calTotalLineCoorInfo(); 12 //cout <<"生成了"<<result<<"个连线段"<<endl;  13 14 /*3*/ 15 //对所有连线求取变化率 16 result = calTotalLineDeri(); 17 //cout <<"对"<<result<<"条连线段求取了变化率"<<endl;  18 19 /*4*/ 20 //求取可参考的半径 提供10个数据 21 result = calReferRaduis(); 22 //cout <<"计算出了"<<result<<"个可参考的半径"<<endl; 23 24 /*5*/ 25  filterRefeRaduis(); 26 //cout << "过滤筛选出最佳半径是:"<<currRadius<<endl; 27 }
复制代码

 

这里的功能区分能让阅读者更好的查看所示意的功能,说明当时我的思路还算清晰.

其实当时也是为了更好的排错.典型一个方便自己,幸福他人的举措.

 

1.生成与圆的交点:

我找increaseAngle的初始化还挺费劲,最后发现它初始化为5.

复制代码
 1 const double PI = 3.141926;  2  Point2i    interPoint;  3 float origAngle = 0.0;  4 while (origAngle < 360) {  5 //获得圆点和圆弧连接起来的交点  6 interPoint.x = outsideRadius * cos(origAngle *  PI/180);  7 interPoint.y = outsideRadius * sin(origAngle *  PI/180);  8  interPointVec.push_back(interPoint);  9 10 origAngle += increaseAngle; 11 }
复制代码

 

2.获取连线的像素信息

   这里的vector使用已经预示着变量命名难以区别的问题了.

复制代码
 1 size_t CCFindInterCircle::calTotalLineCoorInfo(){  2 vector<coorInfo_s> coorInfoVec;  3 for (vector<Point2i>::iterator it = interPointVec.begin(); it != interPointVec.end() ; ++it) {  4 // *it -> centerPoint 之间的像素信息  5 calInfoOfTwoPoints(*it,this->centerPoint,coorInfoVec);  6  totalLineInfoVec.push_back(coorInfoVec);  7  coorInfoVec.clear();  8  }  9 return totalLineInfoVec.size(); 10 }
复制代码

 

3.求所有连线的变化率并归一化

   问题同上

复制代码
 1 size_t CCFindInterCircle::calTotalLineDeri(){  2 vector<int> lineDeriVec;  3 for (vector<vector<coorInfo_s>>::iterator it = totalLineInfoVec.begin(); it != totalLineInfoVec.end(); ++it) {  4 // 计算变化值  5 calLineDeri(*it,lineDeriVec,outsideRadius-insideRadius);  6  totalLineDeriVec.push_back(lineDeriVec);  7  lineDeriVec.clear();  8  }  9 //归一化 10  normalLineDeri(); 11 return totalLineDeriVec.size(); 12 }
复制代码

 

4.求取可参考的半径 提供10个数据

 

复制代码
 1 size_t CCFindInterCircle::calReferRaduis () {  2  3 vector<int>count;  4 count.resize(((outsideRadius - insideRadius) / lineSection) + 1);  5  6 for (size_t i = 0;i < totalLineDeriNorVec.size() ; i ++) {  7 for (size_t j = 0; j < totalLineDeriNorVec.at(i).size(); j++) {  8 if(totalLineDeriNorVec.at(i).at(j) > lineSection_thresh)  9 count[j / lineSection] ++; 10  } 11  } 12 13 //用一个算法解决求第1-10最大数 14 size_t referenceBestRaduisCount = 10; 15  referenceBestRaduis.resize(referenceBestRaduisCount); 16 vector<size_t> maxOrder; 17 size_t maxNumCount = 0,index = 0; 18 int max = 255,diff = 255; 19 while (maxNumCount < referenceBestRaduisCount) { 20 for (size_t k = 0;k < count.size() ; k++) { 21 if((max - count[k]) < diff){ 22 index = k; 23 diff = max - count[index]; 24  } 25  } 26 max = count[index]; 27  maxOrder.push_back(index); 28 referenceBestRaduis[maxNumCount] = ((outsideRadius - (index+1) * lineSection)); 29 30 //尾处理 31 count[index] = 0; 32 diff    = 255; 33 index    = 0; 34 35 maxNumCount ++; 36  } 37 return referenceBestRaduis.size() ; 38 }
复制代码

 

看到半年前写的代码,心里的感觉是十分复杂的,一方面是自己的成长,另一方面又是自己的不足.

没错,由于缺少了帮助我理解的注释,我已经放弃阅读"4."这样的代码了.只知道他的功能 :(

因为我并不知道"尾处理"是在处理什么,而且"用一个算法解决..".并不能对我理解下面的算法起到多大的帮助.

所以对于这样的注释我真想找那个时候的我好好谈谈!

真的.

 

问题变多了,我想了解这个类中一些变量的具体含义.

复制代码
 1 private:  2  3 //私有成员变量  4  Mat processImage;  5  Mat drawImage;  6  7 //可设置变量  8  Point2i centerPoint;  9  size_t outsideRadius; 10  size_t insideRadius; 11 float increaseAngle; 12 int lineSection; 13 float lineSection_thresh; 14 int refeIncrThresh; 15 int lastRadius; 16 17 int currRadius; 18 bool isReset; 19 bool firstStart; 20 21 //计算结果变量 22 vector<Point2i> interPointVec; 23 vector<vector<coorInfo_s>>totalLineInfoVec; 24 vector<vector<int>> totalLineDeriVec; 25 vector<vector<float>> totalLineDeriNorVec; 26 vector<int>referenceBestRaduis
复制代码

 

我的刀呢?


当初开发的时候没想怎么合理的组织全部的数据,想着比较简单而且应该不用很多时间就能解决.

于是是算到哪儿就是哪儿,就有了许多的"临时"的计算结果,而且许多都是可以设计为一个数据结构就可以包含在一起,而不用另开一个vector来存储.

之后还要想清楚这几个vector之间的相互映射关系,这一点颇为痛苦.

这也是我今后在设计程序结构的时候需要考虑的.如何设计数据存储的结构使得可以迅速找到不同数据之间的关系,这里像素坐标和其灰度值使用了coorInfo_s结构存储,但是还可以和变化值联系在一起,类似的考虑不周到还有许多.

这里还有一个问题是变量的注释,应该明确指出该变量的含义,而不是...哈!但是现在这个问题我后来看过他人几个项目后就意识到了...


这里用到了一个Bresenham画直线算法,记一下吧.

(计算从_pointA 到 _pointB之间的像素点.将其像素坐标和像素值push_back进_coorInfoVec中)

复制代码
 1 void CCFindInterCircle::calInfoOfTwoPoints(Point2i & _pointA,Point2i & _pointB,vector<coorInfo_s>& _coorInfoVec){  2  3 int dx = _pointB.x - _pointA.x;  4 int dy = _pointB.y - _pointA.y;  5 int ux = ((dx > 0) << 1) - 1;//x的增量方向,取或-1  6 int uy = ((dy > 0) << 1) - 1;//y的增量方向,取或-1  7 int x = _pointA.x, y = _pointA.y;  8  9 int eps;//eps为累加误差 10 11 eps = 0;dx = abs(dx); dy = abs(dy); 12 13  coorInfo_s coorInfo; 14 15 if (dx > dy) 16  { 17 for (x = _pointA.x; x != _pointB.x+ux; x += ux) 18  { 19 coorInfo.setValue(x,y,this->processImage.at<uchar>(y,x)); 20  _coorInfoVec.push_back(coorInfo); 21 eps += dy; 22 if ((eps << 1) >= dx) 23  { 24 y += uy; eps -= dx; 25  } 26  } 27  } 28 else 29  { 30 for (y = _pointA.y; y != _pointB.y+uy; y += uy) 31  { 32 coorInfo.setValue(x,y,this->processImage.at<uchar>(y,x)); 33  _coorInfoVec.push_back(coorInfo); 34 eps += dx; 35 if ((eps << 1) >= dy) 36  { 37 x += ux; eps -= dy; 38  } 39  } 40  } 41 }
复制代码