前言
通过求塑料瓶盖的圆形程度可以检测出瓶盖周围是否存在凹凸等制造缺陷。有两个直观的作法,一是调用Houghcircles来找圆,这种方法的缺点是可能会找出一堆的圆,很难从这些圆中找到目标物体的外形圆;二是先灰度化图像,然后调用canny算法找物体边缘点,并对边缘点进行拟合就可以进行圆度检测了。 canny算法对噪点不敏感,所以检测准确度还不错,但是该算法比较耗时,大概数百毫秒,在瓶盖生产线上,要求算法实时性很高,所有算法执行时间一般不能超过100ms,甚至有的要求在50ms以内。
圆度检测
这里介绍一种工业界常用检测方法,即先用模板瓶盖的参数(圆心和半径)来确定待测瓶盖的大体位置,然后在模板圆上画出均匀分布的多个矩形窗口,再求出每个窗口里面真正的瓶盖外形边界点,最后通过这些边界点拟合出真正的圆,并基于它来计算圆度值。实践证明,该方法准确度和速度能达到一个很好的平衡。
其结果示意图如下所示。蓝色圆线为模板瓶盖外形,看得出它和实际瓶盖外形位置和大小有一些偏离,实际情况,这个偏离值应该更小。当然即使这么大的偏离也不影响我们最终检测结果。均匀分布在模板圆上的12个矩形窗口用红线表示,其带箭头的中心线和瓶盖边缘有一个最佳的边界点(一阶导数最大,二阶导数为0),用绿色小圆表示。使用最小二乘法来将这些小院点拟合成一个真实的圆用绿线表示。如果塑料瓶盖外形足够理想,那么拟合出来的绿色圆会经过或非常靠近这些绿色小圆中心点。
画窗口及其中心线
本博客重点介绍均匀分布的红色窗口及其中心线是如何画的,这其实和图形学知识相关。 至于如何在中心线上找边缘点及其圆形的拟合还有圆形度值得计算将在视频课程里面详细介绍。
先贴上代码:
#define COUNT_OF_WINDOWS 12
static void GetPointForRotRectVector(double dCenterX1, double dCenterY1, double dRadian, double dLength, double *dVectorX1, double *dVectorY1, double *dVectorX2, double *dVectorY2)
{
double dA1 = cos(dRadian);
double dB1 = sin(dRadian);
double dC1 = -1 * cos(dRadian) * dCenterX1 - sin(dRadian) * dCenterY1;
double dA2 = 1;
double dB2 = 1;
double dC2 = -1 * dLength * dLength;
double a = dCenterX1;
double b = dCenterY1;
if (SolveEquation22(dA1, dB1, dC1, dA2, dB2, dC2, a, b, dVectorX1, dVectorY1, dVectorX2, dVectorY2) == false)
{
*dVectorX1 = dCenterX1;
*dVectorY1 = dCenterY1;
*dVectorX2 = dCenterX1;
*dVectorY2 = dCenterY1;
return;
}
}
// 解二元二次方程
// A1X + B1Y + C1 = 0;
// A2(X - a) * (x -a) + B2(Y - b) * (Y - b) + C2 = 0
static bool SolveEquation22(double dA1, double dB1, double dC1, double dA2, double dB2, double dC2,
double a, double b, double *dResultX1, double *dResultY1, double *dResultX2, double *dResultY2)
{
if (SubSolveEquation22(dA1, dB1, dC1 + dA1 * a + dB1 * b, dA2, dB2, dC2, dResultX1, dResultY1, dResultX2, dResultY2) == false)
{
return false;
}
*dResultX1 += a;
*dResultY1 += b;
*dResultX2 += a;
*dResultY2 += b;
return true;
}
static bool SubSolveEquation22(double dA1, double dB1, double dC1, double dA2, double dB2, double dC2,
double *dResultX1, double *dResultY1, double *dResultX2, double *dResultY2)
{
if (dA1 * dA1 + dB1 * dB1 <= 0)
{
return false;
}
if (dA2 * dA2 + dB2 * dB2 <= 0)
{
return false;
}
if (abs(dA1) >= 0.000001)
{
if (SolveEquation12(dA2 * dB1 * dB1 / (dA1 * dA1) + dB2, 2 * dA2 * dB1 * dC1 / (dA1 * dA1), dA2 * dC1 * dC1 / (dA1 * dA1) + dC2, dResultY1, dResultY2) == false)
{
return false;
}
*dResultX1 = -1 * (dB1 * (*dResultY1) + dC1) / dA1;
*dResultX2 = -1 * (dB1 * (*dResultY2) + dC1) / dA1;
return true;
}
else if (abs(dB1) >= 0.000001)
{
if (SolveEquation12(dB2 * dA1 * dA1 / (dB1 * dB1) + dA2, 2 * dB2 * dA1 * dC1 / (dB1 * dB1), dB2 * dC1 * dC1 / (dB1 * dB1) + dC2, dResultX1, dResultX2) == false)
{
return false;
}
*dResultY1 = -1 * (dA1 * (*dResultX1) + dC1) / dB1;
*dResultY2 = -1 * (dA1 * (*dResultX2) + dC1) / dB1;
return true;
}
return false;
}
int draw_windows(const char *img_file, Mat &result_img)
{
Mat img, img_gray;
img = imread(img_file);
if (img.empty())
{
printf("reading image file fails \n");
return FILE_READ_FAIL;
}
cvtColor(img, img_gray, COLOR_BGR2GRAY);
circle(img, Point(TEMPLATE_CENTER_X, TEMPLATE_CENTER_Y), TEMPLATE_RADIUS, Scalar(255, 0, 0));
float fCurrentRadian = 0;
float fCurrentX = 0, fCurrentY = 0;
float fCurrentX1 = 0, fCurrentY1 = 0;
float fCurrentX2 = 0, fCurrentY2 = 0;
float fCurrentX3 = 0, fCurrentY3 = 0;
float fCurrentX4 = 0, fCurrentY4 = 0;
double dX1, dY1, dX2, dY2, dX3, dY3, dX4, dY4;
for (int i = 0; i < COUNT_OF_WINDOWS; i++)
{
fCurrentRadian = (nStartAngleOfWindow + i * (float)(nEndAngleOfWindow - nStartAngleOfWindow) / (float)COUNT_OF_WINDOWS) * PI / 180;
fCurrentX = TEMPLATE_CENTER_X + TEMPLATE_RADIUS * cos(fCurrentRadian);
fCurrentY = TEMPLATE_CENTER_Y + TEMPLATE_RADIUS * sin(fCurrentRadian);
// 画线条
fCurrentX1 = fCurrentX + nLengthOfWindow / 2 * cos(fCurrentRadian);
fCurrentY1 = fCurrentY + nLengthOfWindow / 2 * sin(fCurrentRadian);
fCurrentX2 = fCurrentX - nLengthOfWindow / 2 * cos(fCurrentRadian);
fCurrentY2 = fCurrentY - nLengthOfWindow / 2 * sin(fCurrentRadian);
GetPointForRotRectVector(fCurrentX1, fCurrentY1, fCurrentRadian, nHeightOfWindow / 2, &dX1, &dY1, &dX2, &dY2);
GetPointForRotRectVector(fCurrentX2, fCurrentY2, fCurrentRadian, nHeightOfWindow / 2, &dX3, &dY3, &dX4, &dY4);
line(img, Point2f(fCurrentX1, fCurrentY1), Point2f(fCurrentX2, fCurrentY2), Scalar(0, 0, 255));
line(img, Point2f(dX1, dY1), Point2f(dX2, dY2), Scalar(0, 0, 255));
line(img, Point2f(dX2, dY2), Point2f(dX4, dY4), Scalar(0, 0, 255));
line(img, Point2f(dX3, dY3), Point2f(dX4, dY4), Scalar(0, 0, 255));
line(img, Point2f(dX3, dY3), Point2f(dX1, dY1), Scalar(0, 0, 255));
//画箭头
fCurrentX3 = fCurrentX + (nLengthOfWindow / 2 - 10) * cos(fCurrentRadian);
fCurrentY3 = fCurrentY + (nLengthOfWindow / 2 - 10) * sin(fCurrentRadian);
fCurrentX4 = fCurrentX - (nLengthOfWindow / 2 - 10) * cos(fCurrentRadian);
fCurrentY4 = fCurrentY - (nLengthOfWindow / 2 - 10) * sin(fCurrentRadian);
// 离心
if (nDirectionOfWindow == 0)
{
GetPointForRotRectVector(fCurrentX3, fCurrentY3, fCurrentRadian, 5, &dX1, &dY1, &dX2, &dY2);
line(img, Point2f(dX1, dY1), Point2f(fCurrentX1, fCurrentY1), Scalar(0, 0, 255));
line(img, Point2f(dX2, dY2), Point2f(fCurrentX1, fCurrentY1), Scalar(0, 0, 255));
}
// 向心
else
{
GetPointForRotRectVector(fCurrentX4, fCurrentY4, fCurrentRadian, 5, &dX1, &dY1, &dX2, &dY2);
line(img, Point2f(dX1, dY1), Point2f(fCurrentX2, fCurrentY2), Scalar(0, 0, 255));
line(img, Point2f(dX2, dY2), Point2f(fCurrentX2, fCurrentY2), Scalar(0, 0, 255));
}
}
}
上面这段代码最难理解得部分是 下面该函数得实现,它用来求中心箭头线一端的两点,显然这两点的连线和中心线垂直。调用两次就可以获得4个点,这四个点就是矩形窗口的四个顶点。
GetPointForRotRectVector(fCurrentX1, fCurrentY1, fCurrentRadian, nHeightOfWindow / 2, &dX1, &dY1, &dX2, &dY2);
从该函数内部实现来看,其原理是先求和中心箭头线 垂直的直线方程,然后求该直线和端侧圆的两个交点。下面以一个矩形窗口求解为例示意如下:
需要注意的一点是,代码里面求直线和圆的交点前,先将它们平移到坐标系圆点来减少计算复杂度,最后将求得值统一加上偏移值,如下图第二个红框所示。图中第一个红框里的表达式值实际为0,圆方程圆心为原点,所以没必要用圆心作为函数参数了。
更合理的做法
上面先画窗口,再求窗口中心线和边界交点的做法比较繁琐,而且精确度不高。 更合理的做法就是直接均匀划分出更密集的中心线来获得更多的边缘点集,这样能保证足够的灵敏度,连小的凹凸都不会放过。 视频课程里面主要介绍这种做法。
下面是两个瓶盖的检测结果示意图。下面那个瓶盖右下方有些凹,如红框所示,所以其平均圆度值相对要小些。
本课代码特色
本课注重实践,结合代码来解决实际工作中的问题。一个最大的特色就是,其算法代码实现没有调用任何第三方库,包括opencv库,都是自己一行行码出来的。这里面包括画绕模板圆均匀分布的线段,高斯模糊去噪点,一阶求导获取边缘点,圆的最小二乘法拟合以及圆度检测。通过理论和源代码学习,可以提高图形、图像算法处理能力。而且还很方便移植这些代码到自己项目中。