一、总体概述这是一个完整的答题卡自动识别和评分系统主要流程包括图像预处理→答题卡定位→透视变换→选项检测→答案判断→评分输出。二、详细分析1.准备工作ANSWER_KEY {0: 1, 1: 4, 2: 0, 3: 3, 4: 1} # 正确答案存储标准答案题目索引→正确选项索引0-based这里表示第0题正确答案是B索引1第1题正确答案是E索引4等2.关键函数定义(1)order_points()- 坐标排序def order_points(pts): # 找出4个坐标位置 rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] # 左上 rect[2] pts[np.argmax(s)] # 右下 diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] # 右上 rect[3] pts[np.argmax(diff)] # 左下 return rect功能将4个点按顺序排列为左上、右上、右下、左下原理左上点xy最小右下点xy最大右上点x-y最小左下点x-y最大(2)four_point_transform()- 透视变换def four_point_transform(image, pts): # 获取输入坐标点并做透视变换 rect order_points(pts) # 找出4个坐标位置 (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]], dtypefloat32) # 计算变换矩阵 M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warped # 返回变换后结果功能将倾斜的答题卡矫正为正面视图步骤计算原始四边形和变换后矩形的对应关系使用cv2.getPerspectiveTransform()计算变换矩阵应用透视变换得到矫正后的图像(3)sort_contours()- 轮廓排序def sort_contours(cnts, methodleft-to-right): # 对轮廓进行排序 reverse False i 0 if method right-to-left or method bottom-to-top: reverse True if method top-to-bottom or method bottom-to-top: i 1 boundingBoxes [cv2.boundingRect(c) for c in cnts] (cnts, boundingBoxes) zip(*sorted(zip(cnts, boundingBoxes), keylambda b: b[1][i], reversereverse)) return cnts, boundingBoxes功能按指定方向左→右、上→下等对轮廓排序实现通过bounding box的坐标进行排序3.主流程分析第一阶段图像预处理和答题卡定位1读取和灰度化image cv2.imread(rtest_01.png) contours_img image.copy() gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)2高斯模糊降噪blurred cv2.GaussianBlur(gray, (5, 5), 0) cv_show(blurred, blurred)3Canny边缘检测edged cv2.Canny(blurred, 75, 200) cv_show(edged, edged)4寻找轮廓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) docCnt None使用Canny边缘检测找出所有边缘筛选最大的轮廓应该是答题卡的外框第二阶段透视变换1寻找四边形轮廓# 根据轮廓大小进行排序准备透视变换 cnts sorted(cnts, keycv2.contourArea, reverseTrue) for c in cnts: # 遍历每一个轮廓 peri cv2.arcLength(c, True) approx cv2.approxPolyDP(c, 0.02 * peri, True) # 轮廓近似 if len(approx) 4: docCnt approx break2执行透视变换warped_t four_point_transform(image, docCnt.reshape(4, 2)) warped_newwarped_t.copy() cv_show(warped, warped_t) warped cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)通过cv2.approxPolyDP()近似多边形找到4个顶点的轮廓作为答题卡边界透视变换得到正视图第三阶段选项检测1阈值处理thresh cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] cv_show(thresh, thresh) thresh_Contours thresh.copy()2寻找所有轮廓# 找到每一个圆圈轮廓 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)3筛选圆形选项轮廓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))筛选标准宽高≥20像素排除噪声宽高比0.9-1.1接近圆形通过这两个条件筛选出选项圆圈第四阶段答案识别和评分1按行排序每题5个选项for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)): cnts sort_contours(questionCnts[i:i 5])[0] # 排序 bubbled None2逐题处理for (j, c) in enumerate(cnts): # 使用mask来判断结果 mask np.zeros(thresh.shape, dtypeuint8) cv2.drawContours(mask, [c], -1, 255, -1) # -1表示填充 cv_show(mask, mask) # 通过计算非零点数量来算是否选择这个答案 # 利用掩膜mask进行“与”操作只保留mask位置中的内容 thresh_mask_and cv2.bitwise_and(thresh, thresh, maskmask) cv_show(thresh_mask_and, thresh_mask_and) total cv2.countNonZero(thresh_mask_and) # 统计灰度值不为0的像素数3判断选择状态if bubbled is None or total bubbled[0]: # 通过阈值判断保存灰度值最大的序号 bubbled (total, j)4与正确答案对比if k bubbled[1]: # 判断正确 color (0, 255, 0) correct 1 cv2.drawContours(warped_new, [cnts[k]], -1, color, 3) # 绘图 cv_show(warpeding, warped_new)关键算法 - 判断哪个选项被选中掩膜技术为每个选项创建单独的掩膜区域统计在掩膜区域内统计非零像素数量决策逻辑被涂黑的选项有最多的非零像素因为是二值化后的白色具体实现# 创建选项掩膜 mask np.zeros(thresh.shape, dtypeuint8) cv2.drawContours(mask, [c], -1, 255, -1) # 只保留该选项区域 thresh_mask_and cv2.bitwise_and(thresh, thresh, maskmask) # 统计非零像素涂黑的部分 total cv2.countNonZero(thresh_mask_and)4.可视化过程代码中通过cv_show()函数展示多个中间结果blurred模糊后的图像edged边缘检测结果contours_img所有轮廓warped透视变换后thresh二值化结果mask单个选项的掩膜thresh_mask_and掩膜应用后的效果5.评分和标记score (correct / 5.0) * 100 # print([INFO] score: {:.2f}%.format(score)) print(score: {:.2f}%.format(score)) cv2.putText(warped_new, {:.2f}%.format(score), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2) cv2.imshow(Original, image) cv2.imshow(Exam, warped_new) cv2.waitKey(0)正确答案用绿色框标记错误答案用红色框标记最终显示得分百分比三、算法特点优点鲁棒的定位通过透视变换处理倾斜拍摄精确检测基于掩膜的统计方法准确可靠适应性强通过轮廓特征筛选选项不依赖固定位置可视化调试完整的中间结果展示局限性依赖预处理质量需要清晰的图像和适当的阈值固定题目数量需要预先知道题目和选项数量圆形选项假设假设选项都是圆形/接近圆形单选框假设只支持单选题这个实现是一个经典的计算机视觉应用案例展示了如何将多个OpenCV技术组合解决实际问题。