目录
1.答题卡自动批阅整体实现思路
2.关键技术步骤与原理
答题卡区域提取
①轮廓检测并排序
②执行透视变换
③找到每一个圆圈轮廓
④先对所有圆圈轮廓从上到下排序
⑤再通过循环每次只提取出五个轮廓再进行从左到右的排序
3.完整代码
1.答题卡自动批阅整体实现思路
- 首先,通过图像处理技术将含答题卡的图片中的答题区域单独提取出来,使其变为一个标准的矩形。
- 然后,针对处理后的答题卡区域进行分析,逐一识别每一道题目的各个选项(A, B, C, D, E)是否被学生正确涂黑。
- 最终,根据判断结果计算出学生的得分,并以百分比和具体答案串等形式展示。
2.关键技术步骤与原理
答题卡区域提取
该过程采用“轮廓查找”与“透视变换”。通过灰度化、二值化等预处理,利用cv2.findContours找到答题卡的最外层轮廓,随后对轮廓进行近似得到四个角点,最后应用透视变换将答题卡区域整齐地裁剪出来。
image=cv2.imread(r'./images/test_04.png')
contours_img=image.copy()
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
blurred=cv2.GaussianBlur(gray,(5,5),0)
cv_show('blurred',blurred)
edged=cv2.Canny(blurred,75,200)
cv_show('edged',edged)
简单的预处理操作
①轮廓检测并排序
- 首先通过轮廓的边界框(bounding box)对所有选项轮廓进行从大到小的初步排序。
#轮廓检测
cnts=cv2.findContours(edged.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3)
cv_show('contours_img',contours_img)
donCnt=None
#根据轮廓大小排序,准备透视变换
cnts=sorted(cnts,key=cv2.contourArea,reverse=True)
for c in cnts:#遍历每一个轮廓peri=cv2.arcLength(c,True)approx=cv2.approxPolyDP(c,0.02*peri,True)if len(approx)==4:donCnt=approxbreak
approx的长度必须为四,得到答题卡区域轮廓的四个近似点坐标矩阵数组
②执行透视变换
#执行透视变换
warped_t=four_point_transform(image,donCnt.reshape(4,2))
warped_new=warped_t.copy()
cv_show('warped_t',warped_t)
二值化
thresh=cv2.threshold(warped,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]
cv_show('thresh',thresh)
thresh_Contours=thresh.copy()
③找到每一个圆圈轮廓
先找到所有轮廓
#找到每一个圆圈轮廓
cnts=cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_Contours=cv2.drawContours(warped_t,cnts,-1,(0,255,0),1)
cv_show('warped_Contours',warped_Contours)
对所有轮廓做外接矩形条件判断,筛选出所有圆形答题轮廓共25个
questionCnts=[]
for c in cnts:(x,y,w,h)=cv2.boundingRect(c)ar=w/float(h)if w>=20 and h>=20 and 0.9<=ar<=1.1:questionCnts.append(c)
print(len(questionCnts))#25
④先对所有圆圈轮廓从上到下排序
questionCnts=sort_contours(questionCnts,method='top-to-bottom')[0]
correct=0
⑤再通过循环每次只提取出五个轮廓再进行从左到右的排序
#每排有五个选项
for (q,i) in enumerate(np.arange(0,len(questionCnts),5)):cnts=sort_contours(questionCnts[i:i+5])[0]bubbled=None#遍历每一个结果for (j,c) in enumerate(cnts):#使用mask来判读结果mask=np.zeros(thresh.shape,dtype='uint8')cv2.drawContours(mask,[c],-1,255,-1)#-1表示填充cv_show('mask',mask)#通过计算非零点数量来算是否选择这个答案#利用掩膜(mask)进行与操作,只保留mask位置中的内容thresh_mask_and=cv2.bitwise_and(thresh,thresh,mask=mask)cv_show('thresh_mask_and',thresh_mask_and)total=cv2.countNonZero(thresh_mask_and)#统计灰度值不为零的像素数量if bubbled is None or total>bubbled[0]:bubbled=(total,j)#对比正确答案color=(0,0,255)answer=ANSWER_KRY[q]if answer==bubbled[1]:#判断正确color=(0,255,0)correct+=1cv2.drawContours(warped_new,[cnts[answer]],-1,color,3)cv_show('warpeding',warped_new)
选项识别与答案分析
- 使用逐个建立掩膜(mask)的方式,从原图中精确抠取出每个选项的图像。
- 计算抠取出的每个选项区域内的白色像素点数量。
- 通过比较不同选项的白色像素点数量,确定学生实际作答的选项,数量最多的即为作答的区域。
标准答案比对与评分
- 系统内部维护一个标准答案对照表(answer key),记录每道题的正确选项。
ANSWER_KRY={0:1,1:4,2:0,3:3,4:1}#正确答案
- 将程序识别出的答案与标准答案进行比对。
- 若答案正确,则计为1分,并使用绿色边框对正确选项进行标注;若答案错误,则不加分,并用红色边框标注正确选项。
- 最终,系统将根据总分和题目数量计算出最终得分并在图片上显示。
score=(correct/5.0)*100
print('[INFO] score:{:.2f}%'.format(score))
cv2.putText(warped_new,'{:.2f}%'.format(score),(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)
cv_show('Original',image)
cv_show('Exam',warped_new)#[INFO] score:20.00%
3.完整代码
import cv2
import numpy as npdef order_points(pts):rect=np.zeros((4,2),dtype='float32')#按顺序找到对应坐标0123即左上右上右下左下s=pts.sum(axis=1)#每行求和rect[0]=pts[np.argmin(s)]rect[2]=pts[np.argmax(s)]diff=np.diff(pts,axis=1)#每行求差rect[1]=pts[np.argmin(diff)]rect[3]=pts[np.argmax(diff)]return rect
def four_point_transform(image,pts):#获取输入坐标点rect=order_points(pts)(tl,tr,br,bl)=rect#计算输入的w和h值widthA=np.sqrt(((br[0]-bl[0])**2+(br[1]-bl[1])**2))widthB=np.sqrt(((tr[0]-tl[0])**2+(tr[1]-tl[1])**2))maxWidth=max(int(widthA),int(widthB))heightA=np.sqrt(((tr[0]-br[0])**2)+(tr[1]-br[1])**2)heightB=np.sqrt(((tl[0]-bl[0])**2)+(tl[1]-bl[1])**2)maxHeight=max(int(heightA),int(heightB))#变换对应的坐标位置dst=np.array([[0,0],[maxWidth-1,0],[maxWidth-1,maxHeight-1],[0,maxHeight-1]],dtype='float32')M=cv2.getPerspectiveTransform(rect,dst)#M是获取到的转换之间的关系warped=cv2.warpPerspective(image,M,(maxWidth,maxHeight))#返回变换后结果return warped
def sort_contours(cnts,method='left-to-right'):reverse=Falsei=0if method=='right-to-left'or method=='bottom-to-top':reverse=Trueif method=='top-to-bottom'or method=='bottom-to-top':i=1boundingBoxes=[cv2.boundingRect(c) for c in cnts](cnts,boundingBoxes)=zip(*sorted(zip(cnts,boundingBoxes),key=lambda a:a[1][i],reverse=reverse))return cnts,boundingBoxes
def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(0)ANSWER_KRY={0:1,1:4,2:0,3:3,4:1}#正确答案image=cv2.imread(r'./images/test_04.png')
contours_img=image.copy()
gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
blurred=cv2.GaussianBlur(gray,(5,5),0)
cv_show('blurred',blurred)
edged=cv2.Canny(blurred,75,200)
cv_show('edged',edged)
#轮廓检测
cnts=cv2.findContours(edged.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3)
cv_show('contours_img',contours_img)
donCnt=None
#根据轮廓大小排序,准备透视变换
cnts=sorted(cnts,key=cv2.contourArea,reverse=True)
for c in cnts:#遍历每一个轮廓peri=cv2.arcLength(c,True)approx=cv2.approxPolyDP(c,0.02*peri,True)if len(approx)==4:donCnt=approxbreak
#执行透视变换
warped_t=four_point_transform(image,donCnt.reshape(4,2))
warped_new=warped_t.copy()
cv_show('warped_t',warped_t)
warped=cv2.cvtColor(warped_t,cv2.COLOR_BGR2GRAY)
thresh=cv2.threshold(warped,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]
cv_show('thresh',thresh)
thresh_Contours=thresh.copy()
#找到每一个圆圈轮廓
cnts=cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_Contours=cv2.drawContours(warped_t,cnts,-1,(0,255,0),1)
cv_show('warped_Contours',warped_Contours)questionCnts=[]
for c in cnts:(x,y,w,h)=cv2.boundingRect(c)ar=w/float(h)if w>=20 and h>=20 and 0.9<=ar<=1.1:questionCnts.append(c)
print(len(questionCnts))
#按照从上到下进行排序
questionCnts=sort_contours(questionCnts,method='top-to-bottom')[0]
correct=0
#每排有五个选项
for (q,i) in enumerate(np.arange(0,len(questionCnts),5)):cnts=sort_contours(questionCnts[i:i+5])[0]bubbled=None#遍历每一个结果for (j,c) in enumerate(cnts):#使用mask来判读结果mask=np.zeros(thresh.shape,dtype='uint8')cv2.drawContours(mask,[c],-1,255,-1)#-1表示填充cv_show('mask',mask)#通过计算非零点数量来算是否选择这个答案#利用掩膜(mask)进行与操作,只保留mask位置中的内容thresh_mask_and=cv2.bitwise_and(thresh,thresh,mask=mask)cv_show('thresh_mask_and',thresh_mask_and)total=cv2.countNonZero(thresh_mask_and)#统计灰度值不为零的像素数量if bubbled is None or total>bubbled[0]:bubbled=(total,j)#对比正确答案color=(0,0,255)answer=ANSWER_KRY[q]if answer==bubbled[1]:#判断正确color=(0,255,0)correct+=1cv2.drawContours(warped_new,[cnts[answer]],-1,color,3)cv_show('warpeding',warped_new)
score=(correct/5.0)*100
print('[INFO] score:{:.2f}%'.format(score))
cv2.putText(warped_new,'{:.2f}%'.format(score),(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)
cv_show('Original',image)
cv_show('Exam',warped_new)