Opencv4圖像分割和識別第八課(實戰7)塑料瓶蓋的圓度檢測

前言

通過求塑料瓶蓋的圓形程度可以檢測出瓶蓋周圍是否存在凹凸等製造缺陷。有兩個直觀的作法,一是調用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庫,都是自己一行行碼出來的。這裏麪包括畫繞模板圓均勻分佈的線段,高斯模糊去噪點,一階求導獲取邊緣點,圓的最小二乘法擬合以及圓度檢測。通過理論和源代碼學習,可以提高圖形、圖像算法處理能力。而且還很方便移植這些代碼到自己項目中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章