首頁 > 軟體

C++ OpenCV實現檔案矯正功能

2022-03-15 19:00:11

需求

將一個斜著拍攝的檔案矯正成正的,如圖所示:

思路

1.讀取原始影象,若影象太大可以先進行縮放處理,並獲取原始影象的寬和高

2.對影象進行預處理得到邊緣,依次進行灰度處理、高斯模糊、邊緣檢測、膨脹、腐蝕。

3.找到最大的輪廓,並提取角點

  • 進行降噪處理:檢測輪廓面積,只保留大於閾值面積的輪廓
  • 計算每個輪廓的周長,使用DP演演算法計算出輪廓點的個數,規則為周長*0.02
  • 找到影象中面積最大的,且角點為4的輪廓

4.將找到的四個角點排列成一個固定的順序,排列後的順序為:左上角-右上角-左下角-右下角

  • 將每個點的xy座標值相加(x+y),左上角的點的座標和應該是最小的,右下角的點的座標和應該是最大的
  • 將每個點的xy座標值相減(x-y),左下角的點的座標差應該是最小的,右上角的點的座標差應該是最大的
  • 重新排列四個角點

5.進行透視變換

  • 根據變換前及變換後的四個角點,建立變換矩陣
  • 根據變換矩陣對影象進行透視變換

6.若透視變換後有一些毛邊,按需要進行裁剪,裁剪後重新調整比例

  • 建立一個矩形用來裁剪,並設定四周裁剪5畫素
  • 裁剪後重新調整影象寬高

7.顯示變換後影象

程式碼

程式碼中均有詳細註釋,請仔細閱讀

#include <iostream>
#include<opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

using namespace cv;
using namespace std;

// 一些定義
Mat image_origin,     // 原始影象
	image_gray,       // 灰度處理後的影象
	image_blur,       // 高斯模糊處理後的影象
	image_canny,      // 邊緣檢測後的影象
	image_dilate,     // 膨脹後的影象
	image_erode,      // 腐蝕後的影象
	image_preprocess, // 預處理後的影象
	image_trans,      // 透視變換後的影象
	image_crop;	      // 裁剪後的影象

vector<Point> origin_points,  // 重新排列前的角點
			  reorder_points; // 重新排列後的角點
			  
			  
int origin_width = 0, origin_height = 0;

/*
 * 函數功能:預處理,依次進行灰度處理、高斯模糊、邊緣檢測、膨脹、腐蝕。
 * 輸入:影象,是否顯示(0-不顯示 1-顯示每一步處理後的影象 2-只顯示最終影象)
 * */
Mat PreProcess(const Mat& image, int display)
{
	// 灰度處理
	cvtColor(image, image_gray, COLOR_BGR2GRAY);

	// 高斯模糊
	GaussianBlur(image_gray, image_blur, Size(3, 3), 3, 0);

	// 邊緣檢測(邊緣檢測前對影象進行一次高斯模糊)
	Canny(image_blur, image_canny, 50, 150);

	// 膨脹和腐蝕(有時進行邊緣檢測的時候,沒有被完全填充,或者無法正確檢測,可以用膨脹和腐蝕)
	// 建立一個用於膨脹和腐蝕的核心,後面的數位越大膨脹的越多(數位要用奇數)
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
	// 膨脹
	dilate(image_canny, image_dilate, kernel);
	// 腐蝕
	//erode(image_dilate, image_erode, kernel);

	// 顯示預處理效果
	if(display == 1)
	{
		imshow("灰度處理後的影象", image_gray);
		imshow("高斯模糊後的影象", image_blur);
		imshow("邊緣檢測後的影象", image_canny);
		imshow("膨脹後的影象", image_dilate);
//		imshow("腐蝕後的影象", image_erode);
	}
	else if(display == 2)
	{
		imshow("預處理後的影象", image_dilate);
	}

	return image_dilate;
}

/*
 * 函數功能:找到面積最大的輪廓
 * 輸入:源影象
 * 輸出:最大輪廓的四個角點陣列
 * */
vector<Point> GetMaxContour(const Mat& img_input)
{
	/*
	 * contours是一個雙重向量,向量內每個元素儲存了一組由連續的Point點構成的點的集合的向量,每一組Point點集就是一個輪廓。有多少輪廓,向量contours就有多少元素。
	 * 相當於建立了這樣一個向量{{Point(),Point()},{},{}}
	 * */
	vector<vector<Point>> contours;
	/*
	 * hierarchy向量內每個元素儲存了一個包含4個int整型的陣列。向量hiararchy內的元素和輪廓向量contours內的元素是一一對應的,向量的容量相同。
	 * hierarchy向量內每一個元素的4個int型變數——hierarchy[i][0] ~ hierarchy[i][3],分別表示第i個輪廓的後一個輪廓、前一個輪廓、父輪廓、內嵌輪廓的索引編號。
	 * 如果當前輪廓沒有對應的後一個輪廓、前一個輪廓、父輪廓或內嵌輪廓的話,則hierarchy[i][0] ~ hierarchy[i][3]的相應位被設定為預設值-1。
	 * */
	vector<Vec4i> hierarchy;

	/*
	 * findContours找到輪廓
	 * 第一個引數:單通道影象矩陣,可以是灰度圖,但更常用的是二值影象,一般是經過Canny、拉普拉斯等邊緣檢測運算元處理過的二值影象;
	 * 第二個引數:contours (前文介紹過)
	 * 第三個引數:hierarchy(前文介紹過)
	 * 第四個引數:輪廓的檢索模式
	 * 		取值一:CV_RETR_EXTERNAL 只檢測最外圍輪廓,包含在外圍輪廓內的內圍輪廓被忽略
	 * 		取值二:CV_RETR_LIST     檢測所有的輪廓,包括內圍、外圍輪廓,但是檢測到的輪廓不建立等級關係,彼此之間獨立,沒有等級關係,這就意味著這個檢索模式下不存在父輪廓或內嵌輪廓,所以hierarchy向量內所有元素的第3、第4個分量都會被置為-1,具體下文會講到
	 * 		取值三:CV_RETR_CCOMP    檢測所有的輪廓,但所有輪廓只建立兩個等級關係,外圍為頂層,若外圍內的內圍輪廓還包含了其他的輪廓資訊,則內圍內的所有輪廓均歸屬於頂層
	 * 		取值四:CV_RETR_TREE     檢測所有輪廓,所有輪廓建立一個等級樹結構。外層輪廓包含內層輪廓,內層輪廓還可以繼續包含內嵌輪廓。
	 * 第五個引數:輪廓的近似方法
	 * 		取值一:CV_CHAIN_APPROX_NONE   儲存物體邊界上所有連續的輪廓點到contours向量內
	 * 		取值二:CV_CHAIN_APPROX_SIMPLE 僅儲存輪廓的拐點資訊,把所有輪廓拐點處的點儲存入contours向量內,拐點與拐點之間直線段上的資訊點不予保留
	 * 		取值三和四:CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似演演算法
	 * 第六個引數:Point偏移量,所有的輪廓資訊相對於原始影象對應點的偏移量,相當於在每一個檢測出的輪廓點上加上該偏移量,且Point可以是負值。不填為預設不偏移Point()
	 * */
	findContours(img_input, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	/*
	 * drawContours繪出輪廓
	 * 第一個引數:指明在哪幅影象上繪製輪廓。image為三通道才能顯示輪廓
	 * 第二個引數:contours
	 * 第三個引數:指定繪製哪條輪廓,如果是-1,則繪製其中的所有輪廓
	 * 第四個引數:輪廓線顏色
	 * 第五個引數:輪廓線的寬度,如果是-1(FILLED),則為填充
	 * */
//	// 不全輸出,在下文只輸出角點
//	drawContours(image, contours, -1, Scalar(255, 0, 255), 2);

	// 定義輪廓,大小與contours相同,但內層向量中只有角點(例如三角形就是3,四邊形就是4,圓形可能七八個)
	vector<vector<Point>> corners_contours(contours.size());

	// 定義邊界框,大小與contours相同
	vector<Rect> bounding_box(contours.size());

	vector<Point> biggest_contours;
	double max_area = 0;

	for (int i = 0; i < contours.size(); i++)
	{
		// 檢測輪廓面積
		double contour_area = contourArea(contours[i]);
//		cout << area << endl;

		// 假設影象中有噪聲,需要將其過濾,只保留面積大於1000的輪廓
		if (contour_area > 1000)
		{
			// 計算每個輪廓的周長
			double contour_perimeter = arcLength(contours[i], true);

			// 使用DP演演算法計算出輪廓點的個數,規則為周長*0.02
			approxPolyDP(contours[i], corners_contours[i], 0.02 * contour_perimeter, true);

			// 找到影象中面積最大的,且角點為4的輪廓
			if (contour_area > max_area && corners_contours[i].size() == 4 ) {

				//drawContours(image_origin, conPoly, i, Scalar(255, 0, 255), 5);
				biggest_contours = { corners_contours[i][0],corners_contours[i][1] ,corners_contours[i][2] ,corners_contours[i][3] };
				max_area = contour_area;
			}

//			// 只繪製角點之間的邊框線,Debug用,取消註釋可以看到檢測出的所有邊界框
//			drawContours(image_origin, corners_contours, i, Scalar(255, 0, 255), 2);
//			rectangle(image_origin, bounding_box[i].tl(), bounding_box[i].br(), Scalar(0, 255, 0), 5);
		}
	}

	// 返回最大的輪廓
	return biggest_contours;
}

/*
 * 函數功能:繪製一些點
 * 輸入:點集,顏色
 * */
void DrawPoints(vector<Point> points, const Scalar& color)
{
	for (int i = 0; i < points.size(); i++)
	{
		circle(image_origin, points[i], 10, color, FILLED);
		putText(image_origin, to_string(i), points[i], FONT_HERSHEY_PLAIN, 4, color, 4);
	}
}

/*
 * 函數功能:重新排列四個角點的順序
 * 最終順序為: 0  1
 * 			  2  3
 * 			  陣列中為左上角-右上角-左下角-右下角
 * */
vector<Point> ReorderPoints(vector<Point> points)
{
	vector<Point> newPoints;
	vector<int>  sumPoints, subPoints;

	// OpenCV中左上頂點為(0,0),右為x軸正向,下為y軸正向。
	for (int i = 0; i < 4; i++)
	{
		// 將每個點的xy座標值相加(x+y),左上角的點的座標和應該是最小的,右下角的點的座標和應該是最大的
		sumPoints.push_back(points[i].x + points[i].y);
		// 將每個點的xy座標值相減(x-y),左下角的點的座標差應該是最小的,右上角的點的座標差應該是最大的
		subPoints.push_back(points[i].x - points[i].y);
	}

	// 重新排列
	newPoints.push_back(points[min_element(sumPoints.begin(), sumPoints.end()) - sumPoints.begin()]); // 0 和的最小值
	newPoints.push_back(points[max_element(subPoints.begin(), subPoints.end()) - subPoints.begin()]); // 1 差的最大值
	newPoints.push_back(points[min_element(subPoints.begin(), subPoints.end()) - subPoints.begin()]); // 2 差的最小值
	newPoints.push_back(points[max_element(sumPoints.begin(), sumPoints.end()) - sumPoints.begin()]); // 3 和的最大值

	return newPoints;
}

/*
 * 函數功能:
 * 輸入:源影象,四個角點的集合(角點的順序為,左上角-右上角-左下角-右下角),輸出的寬,輸出的高
 * 輸出:透視變換後的影象
 * */
Mat PerspectiveTrans(const Mat& img, vector<Point> points, float width, float height )
{
	// 前面經過重新排列,四個角點的順序為:左上角-右上角-左下角-右下角
	Point2f src[4] = { points[0],points[1],points[2],points[3] };
	// 變換後的四個角點
	Point2f dst[4] = { {0.0f,0.0f},{width,0.0f},{0.0f,height},{width,height} };

	// 建立變換矩陣
	Mat matrix = getPerspectiveTransform(src, dst);
	// 透視變換
	warpPerspective(img, image_trans, matrix, Point(width, height));

	return image_trans;
}

int main()
{
	// 1.讀取原始影象
	string path = "res/image_origin.jpg";
	image_origin = imread(path);

//	// 若影象太大可以先進行縮放處理
//	resize(image_origin, image_origin, Size(), 0.5, 0.5);

	// 獲取原始影象的寬和高
	origin_width  = image_origin.size().width;
	origin_height = image_origin.size().height;

	// 2.對影象進行預處理得到邊緣,依次進行灰度處理、高斯模糊、邊緣檢測、膨脹、腐蝕。
	image_preprocess = PreProcess(image_origin, 0);

	// 3.找到最大的輪廓,並提取角點
	origin_points = GetMaxContour(image_preprocess);
//	DrawPoints(origin_points, Scalar(0, 0, 255)); // 紅色
	// 此時發現,角點的順序不固定,為了後面進行透視變換時與程式碼中變換後點集的順序相同,需要將其排列成一個固定的順序,排列後的順序為:左上角-右上角-左下角-右下角
	reorder_points = ReorderPoints(origin_points);
//	DrawPoints(reorder_points, Scalar(0, 255, 0)); //綠色

	// 4.透視變換
	image_trans = PerspectiveTrans(image_origin, reorder_points, origin_width, origin_height);

	// 透視變換後有一些毛邊,若需要可以進行裁剪
	// 四周裁剪5畫素
	int cropVal= 5;
	// 建立一個矩形用來裁剪
	Rect roi(cropVal, cropVal, origin_width - (2 * cropVal), origin_height - (2 * cropVal));
	image_crop = image_trans(roi);
	// 裁剪後重新調整比例
	resize(image_crop, image_crop, Size(origin_width, origin_height));

	// 5.顯示並輸出變換後影象
	imshow("源影象", image_origin);
	imshow("最終影象", image_crop);
    
	imwrite("res/image_output.jpg", image_crop);

	waitKey(0);
}

效果

到此這篇關於C++ OpenCV實現檔案矯正功能的文章就介紹到這了,更多相關OpenCV檔案矯正內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com