用Python+OpenCV给答题卡自动打分?我花了一个周末,总结了这份保姆级教程(附完整代码)

张开发
2026/4/3 17:06:52 15 分钟阅读
用Python+OpenCV给答题卡自动打分?我花了一个周末,总结了这份保姆级教程(附完整代码)
PythonOpenCV实现答题卡自动评分从原理到实战的完整指南上周我接到一个朋友求助——他需要批改300多份标准化考试答题卡。看着那堆纸质表格我突然想到为什么不写个程序自动完成这个枯燥任务于是整个周末都泡在了OpenCV和Python的世界里。现在我将把这次实战经验整理成这份保姆级教程让你也能轻松实现答题卡自动评分系统。1. 环境准备与基础知识1.1 工具安装与配置首先确保你的Python环境已经就绪推荐3.8版本然后通过pip安装必要的库pip install opencv-python numpy matplotlib这三个库将构成我们项目的基础OpenCV计算机视觉核心库处理图像识别NumPy高效处理图像矩阵数据Matplotlib辅助调试时可视化图像处理结果提示如果遇到安装问题可以尝试使用清华镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python1.2 答题卡设计原理一个标准的答题卡通常包含三个关键区域考生信息区学号、姓名等科目选择区考试科目标识答题区选择题选项填涂框我们的识别系统需要定位这些区域在图像中的位置检测填涂状态是否被标记将检测结果与正确答案比对计算分数2. 图像预处理技术2.1 读取与基本转换import cv2 import numpy as np # 读取图像 image cv2.imread(answer_sheet.jpg) # 转换为灰度图 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 高斯模糊降噪 blurred cv2.GaussianBlur(gray, (5, 5), 0)2.2 边缘检测与轮廓查找# Canny边缘检测 edged cv2.Canny(blurred, 50, 150) # 查找轮廓 contours, _ cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)2.3 答题卡区域定位我们需要从所有轮廓中筛选出答题卡的主要区域def find_answer_sheet_contour(contours): # 按面积排序 contours sorted(contours, keycv2.contourArea, reverseTrue)[:5] for c in contours: # 计算轮廓周长 peri cv2.arcLength(c, True) # 多边形近似 approx cv2.approxPolyDP(c, 0.02 * peri, True) # 如果是四边形答题卡外轮廓 if len(approx) 4: return approx return None3. 答题区域识别与评分3.1 透视变换校正图像获取答题卡轮廓后我们需要进行透视变换将其校正为正面视角def four_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]], dtypefloat32) # 计算变换矩阵并应用 M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warped3.2 选项检测算法检测每个选项是否被填涂的核心逻辑def detect_marked_answers(image, question_cnts, answer_cnts): # 初始化答案字典 answer_key {} # 遍历每个问题 for (q, i) in enumerate(np.arange(0, len(question_cnts), 4)): # 获取当前问题的4个选项 cnts answer_cnts[i:i 4] # 初始化标记区域 marked None # 遍历当前问题的每个选项 for (j, c) in enumerate(cnts): # 创建选项区域的掩膜 mask np.zeros(image.shape[:2], dtypeuint8) cv2.drawContours(mask, [c], -1, 255, -1) # 计算掩膜区域的平均像素强度 mask cv2.bitwise_and(image, image, maskmask) total cv2.countNonZero(mask) # 如果当前选项的像素强度最低最暗 if marked is None or total marked[0]: marked (total, j) # 确定正确答案 answer_key[q 1] marked[1] return answer_key4. 完整系统实现与优化4.1 主程序流程def grade_answer_sheet(image_path, correct_answers): # 1. 加载图像并预处理 image cv2.imread(image_path) 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) cnts imutils.grab_contours(cnts) answer_sheet_contour find_answer_sheet_contour(cnts) # 3. 应用透视变换 warped four_point_transform(gray, answer_sheet_contour.reshape(4, 2)) # 4. 二值化处理 thresh cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] # 5. 查找所有选项轮廓 cnts cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts imutils.grab_contours(cnts) question_cnts [] # 6. 筛选有效选项轮廓 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: question_cnts.append(c) # 7. 按位置排序轮廓 question_cnts sort_contours(question_cnts, methodtop-to-bottom)[0] # 8. 检测填涂答案 answer_key detect_marked_answers(thresh, question_cnts, len(correct_answers)) # 9. 计算得分 correct 0 for (k, v) in answer_key.items(): if correct_answers[k] v: correct 1 score (correct / float(len(correct_answers))) * 100 return score, answer_key4.2 常见问题与解决方案在实际开发中我遇到了几个典型问题图像倾斜导致识别错误解决方案增加透视变换前的轮廓验证步骤优化代码def validate_contour(contour): # 计算轮廓的凸包 hull cv2.convexHull(contour) # 计算面积比 area_ratio cv2.contourArea(contour) / cv2.contourArea(hull) return 0.9 area_ratio 1.1填涂不完整导致误判解决方案动态调整阈值优化代码def adaptive_threshold_detection(region): # 计算区域灰度平均值 mean_val np.mean(region) # 根据整体亮度动态调整阈值 threshold mean_val * 0.7 if mean_val 200 else 150 return threshold多答题卡批量处理解决方案封装为类并添加批处理功能示例代码class AnswerSheetGrader: def __init__(self, template_pathNone): self.template cv2.imread(template_path) if template_path else None def grade_batch(self, image_paths, correct_answers): results {} for path in image_paths: score, answers self.grade_single(path, correct_answers) results[path] {score: score, answers: answers} return results5. 高级功能扩展5.1 学号识别实现def recognize_student_id(thresh, id_contours): # 学号通常由多个数字框组成 id_digits [] # 按从左到右排序数字框 id_contours sort_contours(id_contours, methodleft-to-right)[0] for c in id_contours: # 提取单个数字区域 (x, y, w, h) cv2.boundingRect(c) roi thresh[y:y h, x:x w] # 计算ROI的非零像素比例 total_pixels roi.size filled_pixels cv2.countNonZero(roi) ratio filled_pixels / float(total_pixels) # 如果填充比例超过阈值则认为该数字被选中 if ratio 0.5: # 获取数字位置根据y坐标确定行 digit_pos int((y - 10) / 20) # 假设每行高度为20像素 id_digits.append(str(digit_pos)) return .join(id_digits)5.2 科目选择识别def recognize_subject(thresh, subject_contours, subject_mapping): subject_idx None max_filled 0 for i, c in enumerate(subject_contours): (x, y, w, h) cv2.boundingRect(c) roi thresh[y:y h, x:x w] filled cv2.countNonZero(roi) if filled max_filled: max_filled filled subject_idx i return subject_mapping.get(subject_idx, Unknown)5.3 性能优化技巧图像金字塔加速处理def resize_to_optimal(image, widthNone, heightNone): dim None (h, w) image.shape[:2] if width is None and height is None: return image if width is None: r height / float(h) dim (int(w * r), height) else: r width / float(w) dim (width, int(h * r)) return cv2.resize(image, dim, interpolationcv2.INTER_AREA)多线程批处理from concurrent.futures import ThreadPoolExecutor def parallel_grade_sheets(image_paths, correct_answers, max_workers4): with ThreadPoolExecutor(max_workersmax_workers) as executor: futures [executor.submit(grade_answer_sheet, path, correct_answers) for path in image_paths] return [f.result() for f in futures]结果可视化输出def visualize_results(image, answers, correct_answers, score): # 创建彩色副本用于标记 output image.copy() output cv2.cvtColor(output, cv2.COLOR_GRAY2BGR) # 标记正确和错误的答案 for q, a in answers.items(): color (0, 255, 0) if a correct_answers[q] else (0, 0, 255) # 绘制答案框 cv2.drawContours(output, [answer_contours[q][a]], -1, color, 2) # 添加分数文本 cv2.putText(output, fScore: {score:.2f}%, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2) return output在实际项目中我发现最耗时的部分是图像预处理和轮廓查找。通过将一些固定参数如答题卡模板尺寸、选项位置等预先存储可以显著减少运行时计算量。另外使用C扩展处理核心图像算法也是提升性能的有效方法。

更多文章