證件圖像校正

引子:

你是否遇到過這種情況:用手機對着身份證拍張照片,然後想打印出掃描件的效果?應用商城裏有個熱門app好像叫“掃描全能王”,大概提供了這個功能,但是它是收費的,普通用戶用起來有些許不便。最近我也遇到了這個問題,別人用手機拍了個身份證照片傳給我,如上面所示,讓我把它打印出來:我最先想用PS,但是我發現無法用一個規則的矩形把它框得很好,我那沒入門的PS技術無法解決這個問題,於是我就花了一晚上時間寫了個程序,矯正結果如上面所示。本文將和大家分享一下我的代碼。

一、矯正原理

根據寶典"Multiple View Geometry in Computer Vision",同一個相機從不同角度對一個平面成像,兩個時刻成的像之間存在一個單應,也叫射影變換(3x3矩陣)。身份證表面是一個平面,現在的問題就是給定一個任意角度下拍攝的身份證照片,如何把它矯正成一個俯視視角下的照片,核心就是求解一個單應H。

\begin{bmatrix}\bar x \\ \bar y \\ 1 \end{bmatrix} =H \begin{bmatrix}x \\ y \\ 1 \end{bmatrix}

單應是一個3階矩陣,9個參數,8個自由度,求解單應需要8個線性方程,一對對應點(x,y)和(\bar x, \bar y)可以構造2個線性方程,所以求解單應至少需要4對對應點。

如上圖所示,我們依次選取身份證照片的左下角、左上角、右上角、右下角,然後將這四個位置座標映射到一個標準矩形的四個頂點上,即可求出單應。在程序中,基於opencv的highgui實現了交互式從圖像中選點,然後計算出身份證照片的長度height和寬度width,然後將原圖中的身份證左下角映射到座標(0,height),左上角映射到(0,0),右上角映射到(width,0),右下角映射到(width,height)。接着基於opencv已有函數求出單應。注:opencv以圖像左上角爲座標原點,水平向右爲x軸,豎直向下爲y軸。這裏有個小技術,因爲身份證的長寬比是標準的,因此可以將width固定,height根據比例進行相應設置,則可以保證矯正後的身份證圖片具有非常正確的比例。

如果直接應用上述單應對原圖進行變化的話,會存在一些問題:圖像中某些區域變換到負的座標空間,從而被忽略掉,導致變換後的圖像比原圖像內容少了一大塊。爲了解決這個問題,只需要再上述變換的基礎上再實施一個圖像平移操作就可以了,具體地:根據上述單應,求出圖像四個角的變換後的座標,它們定義了圖像有效像素的分佈範圍,如上圖所示,紅色線條定義的區域爲有效圖像區域。我們用一個外界矩形把有效圖像區域包起來,然後把這個外界矩形的左上角移動到座標原點即可。假設這個平移量爲(dx,dy),可以通過對H矩陣的簡單操作來達到一步完成圖像矯正+平移:

H=\begin{bmatrix} h1 & h2 & h3\\ h4 & h5 & h6\\ h7 & h8 & h9 \end{bmatrix} \rightarrow \bar H = \begin{bmatrix} h1+h7\cdot tx & h2+h8\cdot tx & h3+h9\cdot tx\\ h4+h7\cdot ty & h5+h8\cdot ty & h6+h9\cdot ty\\ h7 & h8 & h9 \end{bmatrix}

最後一步是輸出結果:程序提供了三種選擇,(1)直接輸出全尺寸的矯正後的圖像;(2)輸出校正後的圖像中的身份證區域(四周各擴展30個像素);(3)A4尺寸的掃描件。其中第三個選項比較有意思,既然身份證大小知道,標準A4紙的尺寸也是知道的,那麼就可以根據身份證圖片的大小來設置一張成比例的A4紙背景圖片,然後把身份證圖片放進去即可,這就是模仿的掃描設備的成像了,其輸出圖片可以直接按A4尺寸打印。下面是程序運行的部分截圖:

上面最後一個圖:當使用第三種方式保存結果時,可以用畫圖軟件打開圖片,依次選擇“文件”-“打印”-“打印預覽”,如圖所示,將“方向”設置爲“縱向”,“頁邊距”全部設置爲0,然後“確定”。

-----------------------------------------分割線-------------------------

二、代碼

//main.cpp
//[email protected], 20190319

#include "cv.h"
#include "highgui.h"

int yuGrabPoints4(const cv::Mat &img, cv::Point pts[4]);

int main()
{
	std::string srcImage = "src.jpg";
	printf("*** This program will rectify a certificate image to get an upright photo of it. ***\n");
	printf(">> The src image file [default: %s]: ", srcImage.c_str());
	char buf[64];
	std::cin.getline(buf, 1024);
	if(strlen(buf))
		srcImage = buf;
	cv::Mat src = cv::imread(srcImage);


	printf(">> Select 4 points from image:\n");
	printf(">>Please left-click on the image to select the Bottom-Left corner,\
 Top-Left corner, Top-Right corner, Bottom-Right corner of the certificate in turn.\
 Right click to undo the last selection. ESC to finish. Press W/S/A/D to move the image up/down/left/right.\n");
	cv::Point pts[4];
	if(yuGrabPoints4(src, pts) < 4)
		return 0;
	std::vector<cv::Point2d> srcPnts(4);
	srcPnts[0] = pts[0];
	srcPnts[1] = pts[1];
	srcPnts[2] = pts[2];
	srcPnts[3] = pts[3];


	//width & height
	double width = 0, height = 0;
	cv::Point2d d;
	d = srcPnts[0] - srcPnts[1];
	height += sqrt(d.x * d.x + d.y * d.y);
	d = srcPnts[1] - srcPnts[2];
	width += sqrt(d.x * d.x + d.y * d.y);
	d = srcPnts[2] - srcPnts[3];
	height += sqrt(d.x * d.x + d.y * d.y);
	d = srcPnts[3] - srcPnts[0];
	width += sqrt(d.x * d.x + d.y * d.y);
	width *= 0.5, height *= 0.5;

	
	double certificateRatio = 0;
	printf(">> Official aspect ratio (width/height) of ID card is 85.6/54\n");
	printf(">> Define aspect ratio [format:(1)float;(2)float/float;(3)none:then keep the original aspect]: ");
	std::cin.getline(buf, 1024);
	if(strlen(buf)) {
		char *p = strtok(buf, "/");
		certificateRatio = atof(p);
		p = strtok(0, "/");
		if(p) {
			double h = atof(p);
			certificateRatio /= h;
		}
		printf("*** Output aspect ratio is: %f\n", certificateRatio);
		width = height * certificateRatio;
	}	


	//desired corner's positions
	std::vector<cv::Point2d> dstPnts(4);
	dstPnts[0] = cv::Point2d(0, height);
	dstPnts[1] = cv::Point2d(0, 0);
	dstPnts[2] = cv::Point2d(width, 0);
	dstPnts[3] = cv::Point2d(width, height);
	cv::Mat H = cv::findHomography(srcPnts, dstPnts, 0);


	//bounding box of the transformed image
	std::vector<cv::Point2f> pnts1(4), pnts2(4);
	pnts1[0] = cv::Point2f(0, src.rows);
	pnts1[1] = cv::Point2f(0, 0);
	pnts1[2] = cv::Point2f(src.cols, 0);
	pnts1[3] = cv::Point2f(src.cols, src.rows);
	cv::perspectiveTransform(pnts1, pnts2, H);
	cv::Rect box = cv::boundingRect(pnts2);
	cv::Size dstSize = box.size();


	//shift the output image so that the whole image region is visible
	double tx = -(double)box.x;
	double ty = -(double)box.y;
	double *p = (double*)H.data;
	double m31 = p[6], m32 = p[7], m33 = p[8];
	p[0] += m31 * tx;
	p[1] += m32 * tx;
	p[2] += m33 * tx;
	p[3] += m31 * ty;
	p[4] += m32 * ty;
	p[5] += m33 * ty;
	cv::Mat dst;
	cv::warpPerspective(src, dst, H, dstSize);


	//position of the certificate region
	cv::perspectiveTransform(srcPnts, dstPnts, H);
	pnts2.assign(dstPnts.begin(), dstPnts.end());
	box = cv::boundingRect(pnts2);
	int dx = cvRound(width * 0.1); if(dx > 30) dx = 30;
	int dy = cvRound(height * 0.1); if(dy > 30) dy = 30;
	cv::Rect BOX = cv::Rect(box.x - dx, box.y - dy, box.width + dx*2, box.height + dy*2) & cv::Rect(0, 0, dst.cols, dst.rows);


	//cv::Mat J = dst(cv::Rect(cvRound(cx - dx), cvRound(cy - dy), cvRound(width), cvRound(height))).clone();
	cv::Mat J = dst(BOX).clone();
	cv::Mat I = J.clone();
	int fontFace = CV_FONT_HERSHEY_COMPLEX;
	double fontScale = 1.0;
	int thickness = 2;
	cv::Size fontSize;
	for(;;) {
		fontSize = cv::getTextSize("After closing this window, result", fontFace, fontScale, thickness, 0);
		if(fontSize.width > I.cols)
			fontScale *= 0.9;
		else
			break;
	}
	cv::putText(I, "After closing this window, result",
		cv::Point(3, fontSize.height + 5), fontFace, fontScale, CV_RGB(255, 255, 0), thickness);
	cv::putText(I, "will be written to rectified.jpg",
		cv::Point(3, fontSize.height * 2 + 10), fontFace, fontScale, CV_RGB(255, 255, 0), thickness);
	cv::imshow("rectified", I);
	if(cv::waitKey() == 27)
		return 0;
	cv::destroyAllWindows();

	
	printf(">> Output format:");
	printf("(1) Full rectified image.\n");
	printf("(2) Rectified image of the certificate region.\n");
	printf("(3) Rectified certificate proportionally placed in white A4 paper.\n");
	printf(">> Input 1/2/3 [default: 1]: ");
	std::cin.getline(buf, 1024);
	int choice = 1;
	if(strlen(buf))
		choice = atoi(buf);
	if(choice != 1 && choice != 2 && choice != 3)
		choice = 1;


	if(choice == 1)
		cv::imwrite("rectified.jpg", dst);
	else if(choice == 2)
		cv::imwrite("rectified.jpg", J);
	else {
		//standard A4 paper size is 210mm×297mm
		//standard ID certificate size is 85.6mm×54.0mm
		dstSize.width = cvRound(210.0 / 85.6 * box.width);
		dstSize.height = cvRound(297.0 / 54.0 * box.height);
		cv::Mat A4(dstSize, CV_8UC3, cv::Scalar::all(255));
		int gapx = (dstSize.width - BOX.width) / 2;
		int gapy = (dstSize.height - BOX.height) / 3;
		J.copyTo(A4(cv::Rect(gapx, gapy, BOX.width, BOX.height)));
		cv::imwrite("rectified.jpg", A4);
	}	
	return 0;
}
//get_4corners.cpp
//[email protected], 20190319

#include "cv.h"
#include "highgui.h"

void yuGrabPoints4Callback(int Event, int x, int y, int flags, void *param)
{
	cv::Point *pt = (cv::Point*)param;
	if(Event == CV_EVENT_RBUTTONDOWN) {
		pt->x = 0, pt->y = -1;
	}
	else if(Event == CV_EVENT_LBUTTONDOWN) {
		pt->x = x, pt->y = y;
	}
}

int yuGrabPoints4(const cv::Mat &img, cv::Point pts[4])
{
	if(img.type() != CV_8UC3)
		throw;
	cv::Point pt;
	const char *wnd = "yuGrabPoints4";
	cv::namedWindow(wnd, CV_WINDOW_AUTOSIZE); //create a window
	cv::moveWindow(wnd, 0, 0); //move it to up-left corner of screen
	cv::setMouseCallback(wnd, yuGrabPoints4Callback, &pt);

	int h = img.rows, w = img.cols;
	cv::Mat Im(h * 5, w, CV_8UC3);
	cv::Mat I[5] = { Im.rowRange(0, h), Im.rowRange(h, h * 2),
		Im.rowRange(h * 2, h * 3), Im.rowRange(h * 3, h * 4), Im.rowRange(h * 4, h * 5) };
	img.copyTo(I[0]);
	cv::Rect roi(0, 0, w, h);

	int k = 0, delta = 30; char key;
	for(; ;) {
		pt.x = pt.y = -1; key = -1;
		cv::imshow(wnd, I[k](roi));
		while(pt.x < 0 && key == -1)
			key = cv::waitKey(1);

		if(key == 27) // ESC
			break;
		else if(key == 'q' || key == 'Q') //Quit now
			break;
		else if(key == 'w' || key == 'W') // UP
			roi = cv::Rect(roi.x, roi.y + delta, w, h) & cv::Rect(0, 0, w, h);
		else if(key == 's' || key == 'S') // DOWN
			roi = cv::Rect(roi.x, roi.y - delta, w, h) & cv::Rect(0, 0, w, h);
		else if(key == 'a' || key == 'A') // LEFT
			roi = cv::Rect(roi.x + delta, roi.y, w, h) & cv::Rect(0, 0, w, h);
		else if(key == 'd' || key == 'D') // RIGHT
			roi = cv::Rect(roi.x - delta, roi.y, w, h) & cv::Rect(0, 0, w, h);
		else if(pt.y < 0) { //undo
			if(k)
				k--;
		}
		else if(pt.x >= 0 && k < 4) { //a point is selected
			pt.x += roi.x;
			pt.y += roi.y;
			pts[k] = pt;
			I[k].copyTo(I[k + 1]);
			cv::circle(I[k + 1], pt, 1, CV_RGB(255, 0, 0), 2);
			if(k) {
				cv::line(I[k + 1], pts[k - 1], pts[k], CV_RGB(255, 0, 0), 2);
				if(k == 3)
					cv::line(I[k + 1], pts[0], pts[3], CV_RGB(255, 0, 0), 2);
			}
			k++;
		}
	}

	cv::destroyWindow(wnd);
	return k;
}

依賴庫就是opencv了,沒別的,我用了opencv2.4.13,所有opencv2.x版本應該都可以。

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