1. 程式人生 > >【OpenCV3.3】通過透視變換矯正變形影象

【OpenCV3.3】通過透視變換矯正變形影象

        在平面圖像處理中,因為鏡頭角度等原因,容易導致影象出現傾斜、變形等情況,為了方便後續處理我們常常需要進行影象矯正,其中主要技術原理是兩種變換型別--仿射變換(Affine Transformation)和透視變換(Perspective Transformation)。

        仿射變換是二維座標間的線性變換, 故而變換後的影象仍然具有原圖的一些性質,包括“平直性”以及“平行性”,常用於影象翻轉(Flip)、旋轉(Rotations)、平移(Translations)、縮放(Scale operations)等,具體到程式碼應用可以參見OpenCV官方介紹說句題外話,如果沒記錯的話,仿射變換在高中數學的選修部分就出現過,它可以解決一些初等幾何問題,比如部分橢圓題,使用仿射變換往往能得到優雅解法。


        但是仿射變換不能矯正一些變形,如矩形區域的部分發生變化最終變成梯形,這時候矯正就需要用到透視變換。透視變換(Perspective Transformation),又稱投影對映(Projective Mapping)、投射變換等,是三維空間上的非線性變換,可看作是仿射變換的更一般形式,簡單講即通過一個3x3的變換矩陣將原圖投影到一個新的視平面(Viewing Plane),在視覺上的直觀表現就是產生或消除了遠近感。落實到OpenCV,影象的透視變換由以下函式完成(該函式是針對影象的包裝,其本質呼叫cv::perspectiveTransform進行向量座標的變換):

void cv::warpPerspective (
		InputArray 	src, 
		OutputArray 	dst, 
		InputArray 	M, 
		Size 	dsize, 
		int 	flags = INTER_LINEAR, 
		int 	borderMode = BORDER_CONSTANT, 
		const Scalar &borderValue = Scalar(0))
        其中,src是輸入影象,dst是輸出影象,M是3x3變換矩陣,dsize是輸出影象的大小,flags指定畫素插補方法以及矩陣倒置標誌cv::WARP_INVERSE_MAP,borderMode指定邊沿畫素的推算模式,其中BORDER_CONSTANT指示邊沿畫素用borderValue替換,因為預設是0,所以我們變換後的影象邊界可能會出現黑邊,此時可以指定BORDER_REPLICATE對邊界畫素進行復制,即`aaaaaa|abcdefgh|hhhhhhh`(a-h代表畫素)。

        當WARP_INVERSE_MAP被指定時,warpPerspective使用輸入的矩陣M對影象src進行如下變換(圖片來自文件截圖):


        否則,方法先計算矩陣M的倒置矩陣T,然後將T應用到上述形式的變換。該變換不能在原地進行(須分配額外空間)。

        關於變換矩陣M,OpenCV提供了兩種方法計算,getPerspectiveTransformfindHomography,前者雖然有2個過載函式,但其實都是一樣的形式,通過原圖和變換後圖像的4個對應點(即對應的四邊形)計算出透視變換矩陣;後者則相對比較複雜,屬於calib3d模組的內容,概括而言即通過變換前、後兩個平面的點尋找出一個單應性變換矩陣H,滿足,使得反向投影誤差最小;這些在計算機視覺相關課程裡應有詳細介紹。

        下面來看一個具體例子:

static void testImageRectification(cv::Mat &image_original)
{
	CV_SHOW(image_original); // CV_SHOW是cv::imshow的一個自定義巨集,忽略即可
	cv::Mat &&image = image_original.clone();

	cv::Mat image_gray;
	cv::cvtColor(image, image_gray, cv::COLOR_BGR2GRAY);
	cv::threshold(image_gray, image_gray, g_threshVal, g_threshMax, cv::THRESH_BINARY);

	std::vector< std::vector<cv::Point> > contours_list; 
	{
		std::vector<cv::Vec4i> hierarchy;
		// Since opencv 3.2 source image is not modified by this function
		cv::findContours(image_gray, contours_list, hierarchy,
						 cv::RetrievalModes::RETR_EXTERNAL, cv::ContourApproximationModes::CHAIN_APPROX_NONE);
	}
	
	for (uint32_t index = 0; index < contours_list.size(); ++index) {
		cv::RotatedRect &&rect = cv::minAreaRect(contours_list[index]);
		if (rect.size.area() > 1000) {
			if (rect.angle != 0.) {
 				// 此處可通過cv::warpAffine進行旋轉矯正,本例不需要
			} //if

			cv::Mat &mask = image_gray;
			cv::drawContours(mask, contours_list, static_cast<int>(index), cv::Scalar(255), cv::FILLED);

			cv::Mat extracted(image_gray.rows, image_gray.cols, CV_8UC1, cv::Scalar(0));
			image.copyTo(extracted, mask);
			CV_SHOW(extracted);

			std::vector<cv::Point2f> poly;
			cv::approxPolyDP(contours_list[index], poly, 30, true); // 多邊形逼近,精度(即最小邊長)設為30是為了得到4個角點
			cv::Point2f pts_src[] = { // 此處順序調整是為了和後面配對,僅作為示例
				poly[1],
				poly[0],
				poly[3],
				poly[2]
			};
	
			cv::Rect &&r = rect.boundingRect(); // 注意座標可能超出影象範圍
			cv::Point2f pts_dst[] = { 
				cv::Point(r.x, r.y),
				cv::Point(r.x + r.width, r.y),
				cv::Point(r.x + r.width, r.y + r.height) ,
				cv::Point(r.x, r.y + r.height)
			};
			cv::Mat &&M = cv::getPerspectiveTransform(pts_dst, pts_src); // 我這裡交換了輸入,因為後面指定了cv::WARP_INVERSE_MAP,你可以試試不交換的效果是什麼

			cv::Mat warp;cv::warpPerspective(image, warp, M, image.size(), cv::INTER_LINEAR + cv::WARP_INVERSE_MAP, cv::BORDER_REPLICATE);
			CV_SHOW(warp);
		} //if
	}
}
        效果截圖(原圖變形不明顯,將就吧,人眼還是很強大的):