证件图像矫正
引子:
你是否遇到过这种情况:用手机对着身份证拍张照片,然后想打印出扫描件的效果?应用商城里有个热门app好像叫“扫描全能王”,大概提供了这个功能,但是它是收费的,普通用户用起来有些许不便。最近我也遇到了这个问题,别人用手机拍了个身份证照片传给我,如上面所示,让我把它打印出来:我最先想用PS,但是我发现无法用一个规则的矩形把它框得很好,我那没入门的PS技术无法解决这个问题,于是我就花了一晚上时间写了个程序,矫正结果如上面所示。本文将和大家分享一下我的代码。
一、矫正原理
根据宝典"Multiple View Geometry in Computer Vision",同一个相机从不同角度对一个平面成像,两个时刻成的像之间存在一个单应,也叫射影变换(3x3矩阵)。身份证表面是一个平面,现在的问题就是给定一个任意角度下拍摄的身份证照片,如何把它矫正成一个俯视视角下的照片,核心就是求解一个单应H。
单应是一个3阶矩阵,9个参数,8个自由度,求解单应需要8个线性方程,一对对应点(x,y)和可以构造2个线性方程,所以求解单应至少需要4对对应点。
如上图所示,我们依次选取身份证照片的左下角、左上角、右上角、右下角,然后将这四个位置坐标映射到一个标准矩形的四个顶点上,即可求出单应。在程序中,基于opencv的highgui实现了交互式从图像中选点,然后计算出身份证照片的长度height和宽度width,然后将原图中的身份证左下角映射到坐标(0,height),左上角映射到(0,0),右上角映射到(width,0),右下角映射到(width,height)。接着基于opencv已有函数求出单应。注:opencv以图像左上角为坐标原点,水平向右为x轴,竖直向下为y轴。这里有个小技术,因为身份证的长宽比是标准的,因此可以将width固定,height根据比例进行相应设置,则可以保证矫正后的身份证图片具有非常正确的比例。
如果直接应用上述单应对原图进行变化的话,会存在一些问题:图像中某些区域变换到负的坐标空间,从而被忽略掉,导致变换后的图像比原图像内容少了一大块。为了解决这个问题,只需要再上述变换的基础上再实施一个图像平移操作就可以了,具体地:根据上述单应,求出图像四个角的变换后的坐标,它们定义了图像有效像素的分布范围,如上图所示,红色线条定义的区域为有效图像区域。我们用一个外界矩形把有效图像区域包起来,然后把这个外界矩形的左上角移动到坐标原点即可。假设这个平移量为(dx,dy),可以通过对H矩阵的简单操作来达到一步完成图像矫正+平移:
最后一步是输出结果:程序提供了三种选择,(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版本应该都可以。