找圆方法的总结和比较(三种主要识别方法的比较和融合)
本篇博客是课程《
基于OpenCV的钢管计数项目实战》第7课的提纲。在前面已经详细分析3种主要算法的基础上,本节的重点在于如果将每一种算法找到的目标有效地融合起来,并且进一步横向分析研究算法间的关系,最后就整套算法创造过程中产生的思考进行联想和畅谈,希望多少能够给关注这个方向、有类似需求的创作者一些思考。
一、算法流程
首先对于自然图片,通过blod detection获得准确的半径以及一些准确的钢管;
而后基于准确的半径,分别调用HoughCircle以查漏补缺,对图片进行预处理后再调用Contours分析,寻找水泥管。
最后,以上获得的结果,需要进行融合筛选。
二、融合方法
数据结构:
使用
KeyPoint
和
vector
<KeyPoint>
,这种数据结构能够保存x,y和size,对于圆这种对象来说,非常对口。
融合依据:
在添加的过程中首先进行判断:
for (size_t i
=
0; i
< specialKeypoints.size(); i
++)
{
bool isNear
=
true;
for (size_t j
=
0; j
< keypoints.size(); j
++)
{
double dist
= norm(specialKeypoints[i].pt
- keypoints[j].pt);
isNear
= (dist
<
= radius
*
2.
5f);
if (isNear)
{
break;
}
}
if (isNear)
{
keypoints.push_back(specialKeypoints[i]);
}
}
在全部点叠加之后,再进行筛选:
std
:
:vector
< KeyPoint
> resultKeypoints;
for (size_t i
=
0; i
< keypoints.size(); i
++)
{
bool isNew
=
true;
for (size_t j
=
0; j
< resultKeypoints.size(); j
++)
{
double dist
= norm(keypoints[i].pt
- resultKeypoints[j].pt);
isNew
= (dist
>
= keypoints[i].size
/
2
&& dist
>
= resultKeypoints[j].size
/
2);
if (
!isNew)
{
break;
}
}
if (isNew
&& keypoints[i].size
> radius)
resultKeypoints.push_back( keypoints[i]);
}
当然你也可以尝试将所有的点全部融合在一起后,再进行全局筛选。逻辑上是没有问题的,实际操作上效果要差一些。
三、算法异同
在程序的实现过程中,没有过多考虑并行等因素
// 基于Blob方法进行圆的寻找
SimpleBlobDetector
:
:Params params;
params.filterByColor
=
false;
params.minThreshold
=
0;
params.maxThreshold
=
250;
vector
<KeyPoint
> keypoints;
cv
:
:Ptr
<cv
:
:SimpleBlobDetector
> detector
= cv
:
:SimpleBlobDetector
:
:create(params);
detector
-
>detect(srcNormal, keypoints);
//获得半径
std
:
:vector
<
double
> dists;
double radius;
for (size_t pointIdx
=
0; pointIdx
< keypoints.size(); pointIdx
++)
{
dists.push_back(keypoints[pointIdx].size);
}
std
:
:sort(dists.begin(), dists.end());
radius
= (dists[dists.size()
/
2])
/
2.;
//基于HoughCircle方法进行圆的寻找
vector
<KeyPoint
> tmpKeypoints
= findPipMethodHough(srcNormal, radius);
//基于轮廓方法,专门寻找“水泥管”
vector
<KeyPoint
> specialKeypoints
= findConcretePip(srcNormal, radius);
我们将三类算法的结构进行比较:
算法代码 | 注释讲解 |
SimpleBlobDetector::Params params;
params.filterByColor = false;
params.minThreshold = 0;
params.maxThreshold = 250;
vector<KeyPoint> keypoints;
cv::Ptr<cv::SimpleBlobDetector> detector = cv::SimpleBlobDetector::create(params);
detector->detect(srcNormal, keypoints);
//获得半径
std::vector<double> dists;
double radius;
for (size_t pointIdx = 0; pointIdx < keypoints.size(); pointIdx++)
{
dists.push_back(keypoints[pointIdx].size);
}
std::sort(dists.begin(), dists.end());
radius = (dists[dists.size() / 2]) / 2.; | 基于Blob方法进行圆的寻找,除了参数的定义有一些学问以外,基本上就是对自然图片进行基本处理,有个特点是然会radius |
Mat gray;
vector<cv::KeyPoint> vecCenters;
vector<Vec3f> vec3f_method_hough;
//处理彩色图片,进行Hough处理
if (src.channels() == 3 || src.channels() == 4)
cvtColor(src, gray, COLOR_BGR2GRAY);
else
gray = src.clone();
blur(gray, gray, cv::Size(3, 3));
HoughCircles(gray, vec3f_method_hough, HOUGH_GRADIENT, 2, p_radius*2, 100, 33, p_radius-2, p_radius + 2);
for (int i = 0; i < vec3f_method_hough.size(); i++)
{
Point center(cvRound(vec3f_method_hough[i][0]), cvRound(vec3f_method_hough[i][1]));
cv::KeyPoint kpt(center, (float)(p_radius * 2.0f));
vecCenters.push_back(kpt);
}
return vecCenters; | 基于Hough方法进行圆的寻找,除了参数中radius是确定的以外,基本上就是通用的找圆算法 |
float f_area = CV_PI * p_radius * p_radius;//数学公式
vector<cv::Point> vec_method_normal;//返回结果
Mat gray;
Mat tmp25;
Mat draw;
Mat srcClone = src.clone();
//局部阈值方法算法
cvtColor(src, gray, COLOR_BGR2GRAY);
blur(gray, gray, Size(3, 3)); //简单平滑
adaptiveThreshold(gray, tmp25, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY,(int) p_radius * 2 + 1, 0.0); //局部阈值
//形态学变换(膨胀)
Mat elementTest = getStructuringElement(MORPH_ELLIPSE, Size(5, 5));
morphologyEx(tmp25, tmp25, cv::MORPH_ERODE, elementTest);
//边界的钢管识别(边界填充)
rectangle(tmp25, cv::Rect(0, 0, tmp25.cols, tmp25.rows), Scalar(255, 255, 255), 1);
//轮廓分析
std::vector < std::vector<Point> > contours;
findContours(tmp25, contours, RETR_LIST, CHAIN_APPROX_NONE);
vector<cv::KeyPoint> vecCenters;
int minArea = f_area * 0.5f;
int maxArea = 5000;
float minCircularity = 0.47f;
float maxCircularity = std::numeric_limits<float>::max();
for (size_t contourIdx = 0; contourIdx < contours.size(); contourIdx++)
{
//筛选条件
Moments moms = moments(contours[contourIdx]);
//area
double area = moms.m00;
if (area < minArea || area >= maxArea)
continue;
//circularity
double perimeter = arcLength(contours[contourIdx], true);
double ratio = 4 * CV_PI * area / (perimeter * perimeter);
if (ratio < minCircularity || ratio >= maxCircularity)
continue;
Point2d center = Point2d(moms.m10 / moms.m00, moms.m01 / moms.m00);
cv::KeyPoint kpt(center, (float)(p_radius * 2.0f));
vecCenters.push_back(kpt);
}
drawKeypoints(srcClone, vecCenters, draw, Scalar(0, 255, 0), DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
return vecCenters; | 虽然找不到几个目标,但是本函数是最复杂的(最长)的,在实现的过程中包含了圆度和面积的分析。特别是算法预处理这块,明显是有针对性地优化若干目标项目。 |
四、算法衍生
这一次的实现,是比较系统且复杂的轮廓分析算法,从结果上来看,基本能够满足设计要求;从对于自己的提高来看,引导我深入地研究相关知识,增强了能力,主要是能够针对不同情况,有选择性地选择算法实验。
回顾我做过的一些案例,有很多是涉及到轮廓分析的,这里进行回顾和比对:
1、树叶测量
页面分析系统是基于OpenCV+MFC(RIBBON)的精度测量项目。
其关键的一个部分在于实现了3种标定方法,能够获得“像素-距离”的实际关系。
最终通过轮廓分析,获得每一片叶子的面积、周长等信息。
在这个例子中,轮廓分析的主要用途是做联通区域处理,而后区分的结果进行分析和进一步处理。从这个角度上来说,使用联通区域那两个函数更适合。
这个例子使用了Ribbon,这是难得的能够和OpenCV结合良好的框架。相关材料去除敏感信息后出了相关教程。
2、石材大板
使用了类似“树叶分析”的程序框架,但是重点完全不一样。“石材大板”是石料行业中,需要采用计算机的方法对进出库的石材进行有效面积计算的过程(后续应该还可以进一步引导切割)。由于和工业深度融合,该项目对精度、可靠性、可扩充性要求都比较高。
我依然是使用OpenCV+MFC(Ribbon)来完成本项目,并且顺利交付。现在看来,当时的很多算法是臃肿的,界面由于需要有实现很多“辅助线”,最终的实现也不是很巧妙。这里进行介绍,能够帮助大家了解在工业上面,(传统)图像处理项目的基本方法。比较而言,钢管识别的实现体系要现代许多,但是对应起来,只能计数码,不能度量。
3、中药项目
比较早的一个项目,基于OpenCV+MFC,对黑箱采集的图片进行精度测量。
其中也使用了标定的过程。当时这个项目是给合肥一个学校做的,用于标准的制定。现在看来,实现过程中从算法到界面都已经全面过时了。但是当时这个项目仍然是做成功的,因为我采用的视觉经典测量的方法是可以解释的。我相信肯定也有这样的商品,就是做成一个黑箱,而后来测量。
4、答题卡项目
答题卡项目不是完全的轮廓分析,应该说是基于二值区域的投影分析,这些都是不同尺度上的应用。我分别使用c++和mfc进行了实现。现在想来,答题卡这个项目,如果实现pybind++的调用,结合手机,变成“小猿答题”这样的app,似乎很有趣味。
从中我们也可以看出,轮廓(团块)分析的确是强有力的分析工具,在OpenCV中也提供了不同层次的实现,非常值得我们深入研究、灵活运用。
在我之前做过的项目中,应该说以精度策略和数量策略为主,这可能是市场对轮廓这块的具体需求。
五、设计杂谈
【想结合算法的设计实现,谈一些务虚的东西】
为了提高算法能力,达到预定目的,我对OpenCV本身进行了较深入研究,应该说有“新发现,新认识”。
5.1、算法库的涉及范围广泛,需要进一步挖掘
OpenCV到底有多少算法,那些算法进行了优质实现?这些内容,如果不去进行具体研究,是很难得出明确结论的。这一次在findblob、hough的具体研究中,我发现OpenCV从算法本身、实现方法、实现细节,都是有很多值得深入研究的。
在之前很长,我都满足于对算法进行初步试用、认识较为模糊。这一次,为了效率,比如完整的、深刻地认识算法原理,并且“举重若轻”地提出解决方案。
这种方式,在将来是“新常态”;我也需要在一次又一次的“淬炼”中成为真正的专家。
5.2、算法库影响广泛、代码规范,都是将来发展重要依托
在OpenCV PR的过程中,我接触到了“规范的代码”,这潜移默化地改变了我的编码习惯。现在很多方面,我的工作是模糊的,毕竟不是做专门的工作。但是正确的东西就是正确的东西。
必须增强自己在算法实现过程中的影响力、锻炼正确的方法,这些都是未来的重要依托。
5.3、Github的维护方式,是
进一步“刻意练习”的重要依托
“可以练习”强调的是focus feed-back fix ,这在Github上面有非常好的体现,一定要寻找真正的专家,并且就普遍关心的问题进行深入的工作,这样才能进入能力提高“正循环”。