基于OpenCV的答题卡识别与评分系统(Python实现)
📌 摘要
在教育和考试场景中,自动批改答题卡能极大提升效率。本文使用OpenCV和Python实现了一个完整的答题卡识别与评分系统,涵盖图像预处理、透视变换、轮廓检测、选项识别和自动判分。同时,我将分享在实际运行中遇到的经典错误及解决方案,帮助初学者少走弯路。
1️⃣ 项目背景与目标
- 输入:一张拍摄的答题卡图片(含四个角标记、圆形涂写选项)。
- 输出:每道题的判定结果(正确/错误)以及总得分,并在原图上标注。
- 核心流程:
- 边缘检测 + 轮廓查找,定位答题卡主体(四边形)。
- 透视变换矫正倾斜。
- 二值化 + 轮廓筛选,提取所有圆形选项。
- 按行分组,对每行5个选项排序。
- 利用掩膜统计每个选项的涂黑像素数,选出被选中的选项。
- 与标准答案比对,计算得分并可视化。
2️⃣ 技术原理速览
| 技术点 | 作用 |
|---|---|
| Canny边缘检测 | 提取图像中的边缘,便于轮廓查找 |
| 轮廓查找与近似 | cv2.findContours+cv2.approxPolyDP,找出四边形区域 |
| 透视变换 | cv2.getPerspectiveTransform+cv2.warpPerspective,矫正视角 |
| OTSU二值化 | 自动阈值分割,突出涂黑区域(THRESH_BINARY_INV将涂黑变白) |
| 轮廓筛选 | 依据宽高比(≈1)和尺寸筛选圆形选项 |
| 掩膜(Mask) | 对每个选项单独创建掩膜,统计该区域内的非零像素数,确定是否被选中 |
| 排序 | 自定义sort_contours按坐标排序(从上到下,从左到右) |
3️⃣ 代码实现详解(分步注释)
3.1 工具函数定义
importnumpyasnpimportcv2 ANSWER_KEY={0:1,1:4,2:0,3:3,4:1}# 每道题的正确答案(索引从0开始)***order_points – 顺序化四个角点***deforder_points(pts):rect=np.zeros((4,2),dtype="float32")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)]# 左下(差最大)returnrect***four_point_transform – 透视变换***deffour_point_transform(image,pts):rect=order_points(pts)(tl,tr,br,bl)=rect# 计算目标宽度(取上下边平均)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)warped=cv2.warpPerspective(image,M,(maxWidth,maxHeight))returnwarped***sort_contours – 轮廓排序(支持四种方向)***defsort_contours(cnts,method="left-to-right"):reverse=Falsei=0ifmethod=="right-to-left"ormethod=="bottom-to-top":reverse=Trueifmethod=="top-to-bottom"ormethod=="bottom-to-top":i=1# 按Y坐标排序boundingBoxes=[cv2.boundingRect(c)forcincnts](cnts,boundingBoxes)=zip(*sorted(zip(cnts,boundingBoxes),key=lambdab:b[1][i],reverse=reverse))returncnts,boundingBoxes***cv_show – 辅助显示***defcv_show(name,img):cv2.imshow(name,img)cv2.waitKey(0)### 3.2 主流程(重点标注)步骤1:读取图像+预处理 image=cv2.imread(r'./images/test_01.png')contours_img=image.copy()gray=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)blurred=cv2.GaussianBlur(gray,(5,5),0)edged=cv2.Canny(blurred,75,200)步骤2:检测答题卡主体四边形 cnts=cv2.findContours(edged.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]cnts=sorted(cnts,key=cv2.contourArea,reverse=True)forcincnts:peri=cv2.arcLength(c,True)approx=cv2.approxPolyDP(c,0.02*peri,True)iflen(approx)==4:docCnt=approxbreak步骤3:透视变换矫正 warped_t=four_point_transform(image,docCnt.reshape(4,2))warped_new=warped_t.copy()warped=cv2.cvtColor(warped_t,cv2.COLOR_BGR2GRAY)步骤4:二值化(反色,使涂黑区域为白色) thresh=cv2.threshold(warped,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]步骤5:提取所有圆形轮廓并筛选 cnts=cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]# 注意索引questionCnts=[]forcincnts:(x,y,w,h)=cv2.boundingRect(c)ar=w/float(h)ifw>=20andh>=20and0.9<=ar<=1.1:questionCnts.append(c)步骤6:按行分组并识别每道题的选择 questionCnts=sort_contours(questionCnts,method="top-to-bottom")[0]correct=0for(q,i)inenumerate(np.arange(0,len(questionCnts),5)):cnts=sort_contours(questionCnts[i:i+5])[0]# 每行5个选项bubbled=Nonefor(j,c)inenumerate(cnts):mask=np.zeros(thresh.shape,dtype="uint8")cv2.drawContours(mask,[c],-1,255,-1)thresh_mask_and=cv2.bitwise_and(thresh,thresh,mask=mask)total=cv2.countNonZero(thresh_mask_and)ifbubbledisNoneortotal>bubbled[0]:bubbled=(total,j)k=ANSWER_KEY[q]color=(0,0,255)# 默认红色(错误)ifk==bubbled[1]:color=(0,255,0)# 绿色(正确)correct+=1cv2.drawContours(warped_new,[cnts[k]],-1,color,3)步骤7:计算得分并输出 score=(correct/5.0)*100print(f"[INFO] score:{score:.2f}%")cv2.putText(warped_new,f"{score:.2f}%",(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)cv2.imshow("Original",image)cv2.imshow("Exam",warped_new)cv2.waitKey(0)4️⃣ 运行效果与总结
在测试图片(test_01.png)上运行后,程序会:
显示每一步处理结果(模糊、边缘、轮廓、透视矫正、二值化、掩膜等)。
最终输出带评分和标注的答题卡图像。
效果展示(示例):
识别出的选项用绿色框(正确)或红色框(错误)标记。
左上角显示得分百分比。
📚 参考资料
OpenCV官方文档:https://docs.opencv.org/
透视变换原理:https://docs.opencv.org/4.x/da/d6e/tutorial_py_geometric_transformations.html