【OpenCV】基於Qt的“破產版”全能掃描王

功能介紹

  • 圖片打開和保存
  • 圖片矯正(證件掃描、文字糾正…)
  • 圖片銳化增強
  • 圖片清空
  • 閾值設置

項目實現

基本思路(證件掃描)

  • 摳圖:提取輪廓
  • 矯正:透視變換
  • 銳化增強:二值化

算法設計(證件掃描)

第一步:提取邊緣
  • 讀取圖像,轉化爲灰度圖
  • 降噪,二值化 高斯濾波 GaussianBlur()
  • 適當膨脹,提高檢測效率
  • 邊緣檢測 Canny(),打印出二值圖驗證
第二步:輪廓查找與篩選
  • 輪廓檢測 findContours()
  • 霍夫直線檢測 HoughLines()
  • 繪製檢測到的直線並驗證 line()
  • 排除距離過近、不相交的直線
  • 排除距離過近的兩直線交點
第三步:透視變換
  • 由第二步篩選出的四個頂點得出一組座標
  • 確定輸出圖像長寬(或自適應),驗證
  • 計算透視變換矩陣 GetPerspectiveTransform()
  • 透視變換函數 warpPerspective()
第四步:銳化增強
  • 必要的二值化 adaptiveThreshold()
  • 輸出圖像

UI設計

1

核心代碼

  • 證件掃描
Mat scanning()
{
	Mat src = imread(path);
	Mat source = src.clone();

	Mat bkup = src.clone();

	Mat img = src.clone();
	//二值化
	threshold(img, img, GRAY_THRESH, 255, CV_THRESH_BINARY);   

     //高斯濾波
	GaussianBlur(img, img, Size(5, 5), 0, 0); 

	//獲取自定義核
	Mat element = getStructuringElement(MORPH_RECT, Size(3, 3)); 
	//適當膨脹
	dilate(img, img, element);
	//邊緣提取
	Canny(img, img, 30, 120, 3);

	vector<vector<Point> > contours;
	vector<vector<Point> > f_contours;
	vector<Point> approx2;

	//輪廓檢測
	findContours(img, f_contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);

	//求出面積最大的輪廓
	int max_area = 0;
	int index;
	for (int i = 0; i < f_contours.size(); i  )
	{
		double tmparea = fabs(contourArea(f_contours[i])); 
		if (tmparea > max_area)
		{
			index = i;
			max_area = tmparea;
		}
	}

	//找頂點
	Mat f_img = img.clone();
	vector<Vec4i> lines;
	vector<Point2f> corners;

    //驗證輪廓
	drawContours(f_img, contours, 0, Scalar(255)); 
	lines.clear();
	corners.clear();

	//這裏的閾值提供給用戶修改
	//直線檢測
	HoughLinesP(f_img, lines, 1, PI / 180, HOUGH_VOTE, 30, 10);

	//1.過濾不符條件的直線
	//2.計算直線交點
	//3.過濾不符條件的點

	DstSize(corners); //計算輸出尺寸

	Mat dst = Mat::zeros(dst_hight, dst_width, CV_8UC3);
	vector<Point2f> f_points; //四邊形頂點座標組

	f_points.push_back(Point2f(0, 0));
	f_points.push_back(Point2f(dst.cols, 0));
	f_points.push_back(Point2f(dst.cols, dst.rows));
	f_points.push_back(Point2f(0, dst.rows));

	Mat temp = getPerspectiveTransform(corners, f_points); //計算透視變換矩陣 
	warpPerspective(source, dst, temp, dst.size()); //透視變換

    //這裏也可以提供給用戶修改
	//自動增強
	Mat local, gray;
	cvtColor(dst, gray, CV_RGB2GRAY);
	int blockSize = 25;
	int constValue = 10;
	//自適應二值化
	adaptiveThreshold(gray, local, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, blockSize, constValue); 

	return local;
}
  • 文字糾正
Mat rotate(Mat srcImage)
{
	//轉換爲灰度圖
	Mat grayImage;
	cvtColor(srcImage, grayImage, CV_RGB2GRAY);

	//獲取圖片原尺寸
	const int nRows = grayImage.rows;
	const int nCols = grayImage.cols;

	//圖片尺寸轉換,獲取傅里葉變換尺寸
	//返回DFT最優尺寸大小的函數
	int mRows = getOptimalDFTSize(nRows);
	int mCols = getOptimalDFTSize(nCols);

	Mat newImage;
	//邊界擴充函數
	copyMakeBorder(grayImage, newImage, 0, mRows - nRows, 0, mCols - nCols, BORDER_CONSTANT, Scalar::all(0));

	//圖像DFT變換
	//通道組建立,使用Mat_容器,一個存實部,一個存虛部
	Mat groupImage[] = { Mat_<float>(newImage), Mat::zeros(newImage.size(), CV_32F) };
	Mat mergeImage;

	//合併通道
	merge(groupImage, 2, mergeImage);

	//離散傅里葉變換即DFT
	dft(mergeImage, mergeImage);

	//分離通道 
	split(mergeImage, groupImage);

	//調整數據
	//計算傅里葉變化各頻率的幅值
	magnitude(groupImage[0], groupImage[1], groupImage[0]);
	Mat magImage = groupImage[0];

	//歸一化操作,幅值加1
	magImage  = Scalar::all(1);

	//取對數
	log(magImage, magImage);


	//重新分配象限,使(0,0)移動到圖像中心,即把低頻部分移動到中心  
	//傅里葉變換之前要對源圖像乘以(-1)^(x y),進行中心化  
	int cx = magImage.cols / 2;
	int cy = magImage.rows / 2;
	Mat temp;
	//左上象限
	Mat LT(magImage, Rect(0, 0, cx, cy));
	//右上象限
	Mat RT(magImage, Rect(cx, 0, cx, cy));
	//左下象限
	Mat LB(magImage, Rect(0, cy, cx, cy));
	//右下象限
	Mat RB(magImage, Rect(cx, cy, cx, cy));

	//交換象限,左上換右下
	LT.copyTo(temp);
	RB.copyTo(LT);
	temp.copyTo(RB);

	//交換象限,右上換左下 
	RT.copyTo(temp);
	LB.copyTo(RT);
	temp.copyTo(LB);

	//歸一化
	//在0-1之間是統計概率分佈,爲了後續操作方便
	normalize(magImage, magImage, 0, 1, CV_MINMAX);
	//像素強度變換,輸出單通道灰度圖
	Mat magImg;
	magImage.convertTo(magImg, CV_8UC1, 255, 0);
	//imshow("magnitude", magImg);

	//檢測直線
	//二值化
	threshold(magImg, magImg, GRAY_THRESH, 255, CV_THRESH_BINARY);

	//構造8UC1格式圖像
	vector<Vec2f> lines;
	Mat houghImg(magImg.size(), CV_8UC3);

	//Houge直線檢測
	HoughLines(magImg, lines, 1, CV_PI / 180, HOUGH_VOTE, 0, 0);
	// cout << "檢測直線條數:  " << lines.size() << endl;

	//繪製檢測線
	for (int l = 0; l < lines.size(); l  )
	{
		float rho = lines[l][0], theta = lines[l][1];
		Point pt1, pt2;
		//座標變換生成線表達式
		double a = cos(theta), b = sin(theta);
		double x0 = a * rho, y0 = b * rho;
		pt1.x = cvRound(x0   1000 * (-b));
		pt1.y = cvRound(y0   1000 * (a));
		pt2.x = cvRound(x0 - 1000 * (-b));
		pt2.y = cvRound(y0 - 1000 * (a));
		line(houghImg, pt1, pt2, Scalar(255, 0, 0), 3, 8, 0);
	}
	// imshow("hough", houghImg);

	//獲取角度
	float angel = 0;
	float m = PI / 90;
	float n = PI / 2;
	for (int l = 0; l < lines.size(); l  )
	{
		//遍歷檢測直線的角度
		float theta = lines[l][1];
		if (abs(theta) > m && abs(n - theta) > m)
		{
			//取有效角度
			angel = theta;
			break;
		}
	}

	//確保角度在0到90度內
	angel = angel < PI / 2 ? angel : angel - PI;

	//角度換算
	if (angel != PI / 2)
	{
		//作圖一目瞭然
		float angelT = srcImage.rows * tan(angel) / srcImage.cols;
		angel = atan(angelT);
	}
	float angel_rad = angel * 180 / PI;
	// cout << "旋轉角度: " << angel_rad << endl;

	//取圖像中心
	Point2f centerPoint = Point2f(nCols / 2, nRows / 2);
	double scale = 1;

	//計算旋轉中心
	Mat rotateMat = getRotationMatrix2D(centerPoint, angel_rad, scale);

	//仿射變換
	Mat resultImage(grayImage.size(), srcImage.type());
	warpAffine(srcImage, resultImage, rotateMat, srcImage.size(), 1, 0, Scalar(255, 255, 255));
	return resultImage;
}

項目截圖

  • 證件掃描
    1
  • 文字糾正
    2
  • 效果對比
    1
  • 效果對比(娛樂向)
    2

項目總結

  • 本項目基本實現了證件掃描和文字糾正兩大基本功能,其中類似“全能掃描王”的掃描功能被我單獨做了個版本,所以上述截圖UI有些不一樣,特此說明;

  • 在這種基於透視變化的算法中,可以看見一定弊端:直線檢測的閾值、輪廓檢測的標準、頂點篩選的嚴密性等,對最終的結果影響很大,所以找到合理的、或者自適應的參數是最關鍵一步;故市面上的掃描軟件一定有更復雜的思路或算法,還需要繼續學習!

  • 剛開始寫的時候對OCR等詞彙的理解不當,所以在函數命名和UI設計上出現了失誤,特此指出;OCR(Optical Character Recognition,光學字符識別)意爲文字識別,與本項目的功能不同;

  • 希望本文能幫助到那些剛入門圖像處理的同學,咱們一起加油!

  • 關於證件掃描算法的疑惑可以參考 這位大神的乾貨文章

  • 完整源碼鏈接(僅供參考)

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