答题卡图像识别
需求分析、市场分析和技术实现
P.S 博客发布以来,获得多方的关注。相关内容我已经以教程的形式进行了整理发布(包括算法、硬件搭建和软件框架),如果需要请移步
http://edu.51cto.com/course/course_id-8637.html
当然,如果具备一定基础,那么阅读本文就应该能够掌握足够的信息了。
一、需求分析
一、以接口的方式开发此需求:
1:接收图片
以上传的方式把图片发送到接口。
2:识别图片
接口接收到图片后,进行图像识别。
3:返回数据
返回识别后的JSON格式数据。
二、答题卡图片识别的具体要求:
图片是通过手机、相机、扫描仪等设备拍照而来,其中手机、相机拍出的照片会出现像素低、图像不正、聚焦不清楚等问题;
1:图片只要是人眼能看清楚的即可完成识别;
2:800万像素以上的手机拍的照片能进行识别;
3:聚焦不清楚时也可以进行识别;
4:不符合要求的图片可以不识别,一旦识别,正确率必须保证100%。
三、其他要求:
1:此项目验收需要提供答题卡识别的所有源代码、接口说明文档。
2:接口需支持单张图片上传识别以及多张图片的上传识别。
3:接口使用的开发语言及开发工具不限。
四、需求分析:
这是一个典型的“机器视觉”应用。其中,答题卡的样式可以是由自己来设置的,图片的获取方式提到了可以是“手机拍照、相机拍照”这种比较方便的方式;本例的一个特殊的要求是:你可以识别不出来,但是你不能识别错误,这是项目的特殊要求
五、需求分析:
普通的答题卡是这样的:
用于机器识别的答题卡是这样的,最明显的区别在于在边界处提供了用于标定的黑边。由于这里的答题卡是可以自己来设计的,就应该设计得最适合识别:
经过我修改的答题卡是这样的,主要是用圆点进行边界标定,因为在旋转和缩放的情况下,圆点都有更好的性能:
二、市场分析
答题卡已经出现好多年了,而且教育机构也是容易出现壁垒的领域。经过简单调查,制式的答题机应该是这种样子的,这种答题机采用的应该特殊的成像技术,比如红外之类的,否则也不需要做成这种样子:
其价格在数千元到万元左右,淘宝上也有人做出了机器识别的例子:
采用普通摄像头和特定的支架,销售情况不好。
但是,图像确是多种多样的。
形式多样。值得关注的一点是,这些能够通过baidu直接搜索得到的答题卡在设计上和本文提供的答题开有两点比较大的不同,一个是在取消了比如圆点这样的标定点,二个是在横版面上采用了“点画”的方式进行标定
这样能够得到的结果还是使得答题卡更加的简洁,美观。
对于这个市场,我认为在网络和即时聊天工具更加发达的今天,答题卡作为一种非常正式的考试方法,还是有其市场的(比如高考中考,短时间内还不会出现直接采用移动设备进行答卷);但是专门去做一套这样的设备,市场已经基本饱和,而且教育市场的壁垒应该很高,不是很容易就能够进入的。但是,对于在日常非正式考试中需要答题卡相关设备,而不希望担负一套昂贵的专业系统的人或单位来所,如果能够以一种比较低廉的价格,并且已一种比较方便操作的方式(比如直接利用手机,或普通相机)进行实现,应该是有一定的市场的。
三、技术实现
本例的技术难度不是很大,非常关键的一点是由于卡片是可以由自己来设计的。而且图像的获取也比较容易被优化。这里以最前面的图片进行设计分析,其他的例子情况可以以此类推;并且公布核心代码。
1)仿照实际的情况,对原始图片进行相关处理。在实际拍摄的时候,可能会出现“缩放”、“透视变化”等影响最终实际结果的情况:
变小
透视变化
同时透视和缩放
2)编写获取锚点(就是圆点)的函数. FetchAnchorPoints函数的主要过程是将输入的图片划分为四个部分,并且分别找到其中的圆点。参数中mattmp是模板图片,也就是哪个小圆的图片。
void FetchAnchorPoints(Mat src,Mat mattmp,Point &anchor01,Point &anchor02,Point &anchor03,Point &anchor04)
{
Mat imagematch;
Point minLoc;
Point maxLoc01,maxLoc02,maxLoc03,maxLoc04;
//Point anchor01,anchor02,anchor03,anchor04;
double minVal;
double maxVal2;
//Mat src = imread("C:/answercard/1.jpg",0);//读入黑白原始图像
int srcRows = src.rows;
int srcCols = src.cols;
Mat src01 = src(Rect(0,0,srcCols/2,srcRows/2));
Mat src02 = src(Rect(srcCols/2,0,srcCols/2,srcRows/2));
Mat src03 = src(Rect(0,srcRows/2,srcCols/2,srcRows/2));
Mat src04 = src(Rect(srcCols/2,srcRows/2,srcCols/2,srcRows/2));
//imshow("src01",src01);imshow("src02",src02);imshow("src03",src03);imshow("src04",src04);
matchTemplate( mattmp, src01, imagematch, 5 );
normalize( imagematch, imagematch, 0, 1, NORM_MINMAX, -1, Mat() );
minMaxLoc( imagematch, &minVal, &maxVal2, &minLoc, &maxLoc01, Mat() );
anchor01 = maxLoc01;
//circle(src,maxLoc01,3,Scalar(0),3);
matchTemplate( mattmp, src02, imagematch, 5 );
normalize( imagematch, imagematch, 0, 1, NORM_MINMAX, -1, Mat() );
minMaxLoc( imagematch, &minVal, &maxVal2, &minLoc, &maxLoc02, Mat() );
anchor02 = Point(maxLoc02.x+srcCols/2,maxLoc02.y);
//circle(src,anchor02,3,Scalar(0),3);
matchTemplate( mattmp, src03, imagematch, 5 );
normalize( imagematch, imagematch, 0, 1, NORM_MINMAX, -1, Mat() );
minMaxLoc( imagematch, &minVal, &maxVal2, &minLoc, &maxLoc03, Mat() );
anchor03 = Point(maxLoc03.x,maxLoc03.y+srcRows/2);
//circle(src,anchor03,3,Scalar(0),3);
matchTemplate( mattmp, src04, imagematch, 5 );
normalize( imagematch, imagematch, 0, 1, NORM_MINMAX, -1, Mat() );
minMaxLoc( imagematch, &minVal, &maxVal2, &minLoc, &maxLoc04, Mat() );
anchor04 = Point(maxLoc04.x+srcCols/2,maxLoc04.y+srcRows/2);
//circle(src,anchor04,3,Scalar(0),3);
}
获得的结果
3)采用warpPerspective进行透视变换,如果对warpPerspective不是很了解可以查看我前面的blog
Point2f dst_vertices[4];
//获得矫正结果图像的参数
Mat matstandard = imread("C:/answercard/1.jpg",0);//读入黑白原始图像
Mat mattmp = imread("C:/answercard/temp.jpg",0);
FetchAnchorPoints(matstandard,mattmp,anchor01,anchor02,anchor03,anchor04);
std::cout<<"anchor01"<<anchor01<<" "<<"anchor02"<<anchor02<<" "<<"anchor03"<<anchor03<<" "<<"anchor04"<<anchor04;
dst_vertices[0] = anchor01;
dst_vertices[1] = anchor02;
dst_vertices[2] = anchor03;
dst_vertices[3] = anchor04;
//dst_vertices.push_back(anchor01);dst_vertices.push_back(anchor02);dst_vertices.push_back(anchor03);dst_vertices.push_back(anchor04);
//获得需要矫正图像参数
Mat matsrc = imread("C:/answercard/bigroatate.jpg",0);
FetchAnchorPoints(matsrc,mattmp,anchor01,anchor02,anchor03,anchor04);
cout<<" ";
std::cout<<"anchor01"<<anchor01<<" "<<"anchor02"<<anchor02<<" "<<"anchor03"<<anchor03<<" "<<"anchor04"<<anchor04;
src_vertices[0] = anchor01;
src_vertices[1] = anchor02;
src_vertices[2] = anchor03;
src_vertices[3] = anchor04;
//src_vertices.push_back(anchor01);src_vertices.push_back(anchor02);src_vertices.push_back(anchor03);src_vertices.push_back(anchor04);
//透视变化
Mat warpMatrix = getPerspectiveTransform(src_vertices, dst_vertices);
cv::Mat rotated;
warpPerspective(matsrc, rotated, warpMatrix, rotated.size(), INTER_LINEAR, BORDER_CONSTANT);
imshow("rotated",rotated);
imshow("matstandard",matstandard);
Mat roi01;Mat roi02;Mat roi03;Mat roi04;
anchor02 =dst_vertices[1] ;
anchor03 =dst_vertices[2] ;
anchor04 =dst_vertices[3] ;
//TODO这个地方最终的时候需要改成rotated
roi01 = matstandard(Rect(anchor01.x,anchor01.y+mattmp.rows,20,anchor03.y-anchor01.y-mattmp.rows));
roi02 = matstandard(Rect(anchor01.x+mattmp.cols,anchor01.y,anchor02.x-anchor01.x-mattmp.cols,20));
roi03 = matstandard(Rect(anchor02.x+8,anchor02.y+mattmp.rows,17,anchor04.y-anchor02.y-mattmp.rows));
roi04 = matstandard(Rect(anchor03.x+mattmp.cols,anchor03.y+5,anchor04.x-anchor03.x-mattmp.cols,13));
//roi02 = FetchMaxContour(roi02);
//imshow("roi01",roi01);
//imshow("roi02",roi02);
//imshow("roi03",roi03);
//imshow("roi04",roi04);
//函数作用: 对区域进行预处理,返回最大的连续区域
//参 数: src [in] 输入mat
//返 回:投影值
vector<int> FetchMaxContour(Mat src)
{
//读取图像
Mat testmat = src.clone();
Mat testclone = src.clone();
Mat matcanny;
//用于寻找轮廓
Mat threshold_output;
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
int imax = 0;int maxsize = 0;
RotatedRect theMinRect;
RotatedRect theMinEllipse;
//imshow("原始图像",testmat);
//大津法找到敏感区域
threshold(testmat,testmat,0,255,cv::THRESH_OTSU);
//imshow("大津法",testmat);
//为什么要转换,因为白色是有数据的区域,轮廓是围绕白色区域的
threshold(testmat,testmat,0,255,THRESH_BINARY_INV);
imshow("二值",testmat);
//计算纵向投影
vector<int> vcol;itmp = 0;
for (int i=0;i<testmat.cols;i++)
{
for (int j=0;j<testmat.rows;j++)
{
if (testmat.at<uchar>(j,i))
{
itmp = itmp +1;
}
}
vcol.push_back(itmp);
itmp = 0;
}
////对得到的结果进行处理,计算波峰
//int isum = 0;//一共多少个波峰
vector<int> vrise;
for (int i=1;i<vcol.size();i++)
{
if (vcol[i-1]==0 && vcol[i]>0)
{
vrise.push_back(i);
//isum = isum+1;
}
}
return vrise;
}
for (int i=1;i<vroi02.size();i++)
{
Mat roi = rotated(Rect(mattmp.cols+anchor01.x+vroi02[i],roi02.rows+38,11,92));
//imshow("roi",roi);
vmat02.push_back(roi);
circle(rotated,Point(mattmp.cols+anchor01.x+vroi02[i],roi02.rows+38),1,Scalar(0),1);
}
vector<int> vroi04 = FetchMaxContour(roi04);
vector<Mat> vmat04;
for (int i=0;i<vroi04.size();i++)
{
Mat roi = rotated(Rect(mattmp.cols+anchor03.x+vroi04[i],153,11,198));
//imshow("roi",roi);
//vmat02.push_back(roi);
circle(rotated,Point(mattmp.rows+anchor03.x+vroi04[i],153),1,Scalar(0),1);
}
imshow("rotated",rotated);
resize(matsrc,matsrc,Size(600,500));
FetchAnchorPoints(matsrc,mattmp,anchor01,anchor02,anchor03,anchor04);
效果如此。
vector<int> vroi02 = FetchMaxContour(roi02);
vector<Mat> vmat02;
vector<int> vroi03 = FetchMaxContour(roi03,1);
//减去偏移,这里的偏移量可以从roi03第一个值得出
for (int i=0;i<vroi03.size();i++)
{
vroi03[i] = vroi03[i]-30;
}
int resulttmp = 9;
cout<<"vroi02"<<endl;
//这里i = 0的数据是无用数据
for (int i=1;i<vroi02.size();i++)
{
Mat roi = rotated(Rect(mattmp.cols+anchor01.x+vroi02[i],roi02.rows+38,11,92));
//vmat02.push_back(roi);
vector<int> vtmp = FetchMaxContour(roi,1);
vtmp[0] = vtmp[0]+4;
for (int k = 0;k<9;k++)
{
if (vtmp[0]>=vroi03[k] && vtmp[0]<vroi03[k+1])
{
resulttmp = k;
break;
}
}
cout<<i<<" is "<<resulttmp<<" | ";
cout<<endl;resulttmp = 9;
if (IsDebug)
{
char* tmp = new char[100];
sprintf(tmp,"C:/answercard/vmat02/%d.jpg",i);
imwrite(tmp,roi);
circle(rotated,Point(mattmp.cols+anchor01.x+vroi02[i],roi02.rows+38),1,Scalar(0),1);
}
}