简介这份资源是面向教育信息化开发者、计算机视觉初学者及课程设计学生的Python答题卡智能识别判卷项目源码旨在用自动化方案替代人工阅卷解决批量答题卡评分效率低、易出错的问题。压缩包共21个文件约11.54MB以16张png识别效果图、3个xml配置文件和1个py主程序为主图片直观展示各阶段识别结果xml记录项目环境配置py脚本承载核心判卷逻辑。目前已有1318人学习下载适合作为计算机视觉与机器学习落地的参考案例。项目覆盖OpenCV图像预处理、PIL格式调整、模板匹配定位题区、特征提取与SVM/CNN分类、OCR手写识别、数据归一化及准确率与F1评估等完整链路并包含GUI交互与自动化评分流程读者可据此掌握从图像上传到结果输出的端到端实现思路快速搭建可复用的智能判卷原型。1. 从一张手机拍的答题卡说起Python 判卷到底能省掉多少人工考试结束答题卡收上来一摞如果还靠两个老师对着标准答案一张张翻、一格一格数一个班五十人、一百道选择题光核对就要小半天眼睛花、手抖、串行都是常事。基于 Python 的答题卡智能识别判卷要解决的就是这件事用摄像头或扫描仪把答题卡变成图片程序自动定位填涂区域、判断哪个选项被涂黑、跟标准答案比对、算出分数并导出成绩表。它适合学校老师、培训机构教务、做课程设计的学生也适合想练手 OpenCV 图像处理的 Python 入门者。整套流程不依赖昂贵的专用阅卷机一台普通电脑加一个摄像头就能跑起来源码结构清晰改起来也方便。下面我按自己实际搭过的一套方案把从环境到出分的路径讲透。2. 答题卡识别判卷的技术链路从图像到分数的五步拆解2.1 为什么选 OpenCV NumPy 这套组合答题卡识别的本质是图像处理问题不是深度学习问题。一张标准答题卡上填涂区是固定位置的矩形被涂黑的选项和没涂的选项在灰度值上有明显差异用传统图像处理就能稳定区分没必要上模型。OpenCV 负责图像读取、透视校正、二值化、轮廓检测NumPy 负责矩阵运算和区域切片这两个库配合起来足够覆盖全流程。选它们还有一个现实原因安装简单、文档多、跑在普通 CPU 上就够快一张答题卡从读入到出分通常在一秒以内。如果换成深度学习方案要标注数据、要训练、要 GPU对判卷这种规则明确的任务是过度设计。常见做法是先用 OpenCV 把答题卡摆正、切出每个选项的小格子再统计格子里的黑色像素占比超过阈值就算涂了。2.2 五步链路校正、定位、切格、判涂、比对整套流程拆成五步每一步都有明确的输入输出。第一步是透视校正答题卡拍照时会有倾斜和透视变形要用四点透视变换把它拉回标准矩形。第二步是定位锚点答题卡四角通常有黑色定位方块找到它们才能确定坐标系。第三步是切格根据锚点算出每个题号、每个选项的相对坐标把填涂区切成一个个小图。第四步是判涂统计每个小图里黑色像素的比例跟阈值比较判断是否被涂。第五步是比对把识别出的答案跟标准答案逐题对比算分并输出。这五步里透视校正和判涂阈值是最容易翻车的两个环节后面会专门讲。2.3 最小可跑通的环境搭建命令先把环境搭起来。Python 建议用 3.8 以上版本OpenCV 用 opencv-python 就够不需要装完整版。下面这组命令在 Windows、macOS、Linux 上都能用装完直接能跑识别脚本。# 创建独立虚拟环境避免污染系统 Python python -m venv answer_sheet_env # 激活环境Windows 用下面这行 answer_sheet_env\Scripts\activate # macOS / Linux 用下面这行 source answer_sheet_env/bin/activate # 安装核心依赖opencv-python 是主库numpy 随它一起装 pip install opencv-python numpy # 如果要做成绩导出再加一个 pandas pip install pandas装完之后用一行命令验证 OpenCV 是否可用能打印出版本号就说明环境没问题。import cv2 import numpy as np # 打印版本确认库能正常导入 print(OpenCV:, cv2.__version__) print(NumPy:, np.__version__)这里有个参数要留意opencv-python 和 opencv-contrib-python 不要同时装两者会冲突导致 imshow 之类的函数报错。如果之前装过完整版先 pip uninstall opencv-contrib-python 再装 opencv-python。虚拟环境的作用是把依赖隔离换项目时不会互相干扰这也是 vscode 配置 Python 环境时推荐的做法。2.4 透视校正把歪着拍的答题卡拉正拍照角度不可能正对答题卡所以第一步必须做透视校正。思路是找到答题卡四个角的定位点用 cv2.getPerspectiveTransform 算出变换矩阵再用 cv2.warpPerspective 把图像拉成标准矩形。定位点怎么找常见做法是对图像做灰度化、高斯模糊、边缘检测然后找面积最大的四边形轮廓。import cv2 import numpy as np def order_points(pts): # 把四个点按左上、右上、右下、左下排序 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 def four_point_transform(image, pts): # 输入原图和四个角点输出校正后的正视图 rect order_points(pts) (tl, tr, br, bl) rect # 计算校正后图像的宽高 width max(int(np.linalg.norm(br - bl)), int(np.linalg.norm(tr - tl))) height max(int(np.linalg.norm(tr - br)), int(np.linalg.norm(tl - bl))) dst np.array([ [0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtypefloat32) # 求透视变换矩阵并应用 M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (width, height)) return warped # 读取答题卡照片转灰度后做边缘检测找轮廓 image cv2.imread(sheet.jpg) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) edged cv2.Canny(blurred, 75, 200) cnts, _ cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 取面积最大的轮廓作为答题卡外框 doc_cnt max(cnts, keycv2.contourArea) peri cv2.arcLength(doc_cnt, True) approx cv2.approxPolyDP(doc_cnt, 0.02 * peri, True) warped four_point_transform(image, approx.reshape(4, 2)) cv2.imwrite(warped.jpg, warped)这段代码里GaussianBlur 的核大小 (5, 5) 是经验值图像噪点多可以调到 (7, 7)。Canny 的两个阈值 75 和 200 控制边缘检测的灵敏度光照暗的时候可以降到 50 和 150。approxPolyDP 的 0.02 是轮廓逼近精度值越小越贴近原始轮廓但可能逼近出超过四个点导致找不到四边形。如果答题卡边框是浅色Canny 可能检测不到这时候改用自适应二值化 cv2.adaptiveThreshold 效果更稳。2.5 切格与判涂黑色像素占比怎么定阈值校正之后答题卡变成标准矩形每个填涂区的位置就固定了。假设答题卡是 20 题、每题 4 个选项横向排列那每个选项格子的坐标可以用比例算出来。切出小图后统计黑色像素占比超过阈值就算涂了。def detect_filled(warped, row, col, total_rows, total_cols): # 按行列比例切出单个选项格子 h, w warped.shape[:2] cell_h h // total_rows cell_w w // total_cols y1, y2 row * cell_h, (row 1) * cell_h x1, x2 col * cell_w, (col 1) * cell_w cell warped[y1:y2, x1:x2] # 转灰度并二值化黑色像素为 0 gray_cell cv2.cvtColor(cell, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray_cell, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 统计黑色像素占比 black_ratio np.count_nonzero(binary) / binary.size return black_ratio # 对第 0 题第 0 个选项判涂阈值设 0.3 ratio detect_filled(warped, 0, 0, 20, 4) print(黑色像素占比:, ratio, 是否涂了:, ratio 0.3)阈值 0.3 是起点不是定值。铅笔涂得浅占比可能只有 0.15圆珠笔涂得重可能到 0.6。实际用的时候先拿几张真实答题卡跑一遍打印每格的占比看涂了的和没涂的分布在哪里再定阈值。如果涂了和没涂的占比有重叠说明切格位置偏了要回去检查锚点定位。Otsu 自动阈值在光照均匀时很好用光照不均时改用固定阈值 127 更可控。3. 判卷逻辑与成绩导出从识别结果到一张成绩表3.1 标准答案怎么存、怎么比对识别出每题的填涂结果后要跟标准答案比对。标准答案建议单独存成一个列表或 JSON 文件不要硬编码在脚本里方便换卷子。每题可能有单选也可能有多选比对逻辑要区分开。import json # 标准答案单选存字符串多选存列表 answer_key { 1: A, 2: C, 3: [A, B], # 多选题 4: D } def grade_one(student_ans, correct_ans): # 单选直接比较多选要求集合完全一致 if isinstance(correct_ans, list): return set(student_ans) set(correct_ans) return student_ans correct_ans # 假设识别出的学生答案 student {1: A, 2: B, 3: [A, B], 4: D} score 0 for q, correct in answer_key.items(): if grade_one(student.get(q, ), correct): score 1 print(得分:, score, /, len(answer_key))这里有个细节多选题如果学生只涂了一半set 比较会判错这是符合预期的。如果学校允许多选漏选给半分就要在 grade_one 里加部分匹配逻辑比如算交集大小。标准答案用 JSON 存的好处是换一套卷子只改 JSON 文件脚本不用动。3.2 批量处理与成绩导出单张答题卡跑通后要能批量处理一个文件夹里的所有图片并把结果导出成 Excel 或 CSV。用 os.listdir 遍历用 pandas 写表。import os import pandas as pd def batch_grade(folder, answer_key): results [] for fname in os.listdir(folder): if not fname.lower().endswith((.jpg, .png)): continue path os.path.join(folder, fname) # 这里调用前面的校正、切格、判涂流程 student_ans recognize_sheet(path) score sum(1 for q, c in answer_key.items() if grade_one(student_ans.get(q, ), c)) results.append({文件名: fname, 得分: score, 总分: len(answer_key)}) df pd.DataFrame(results) df.to_excel(成绩表.xlsx, indexFalse) return df # 批量跑一个文件夹 # batch_grade(./sheets, answer_key)导出时文件名保留原始图片名方便对应到具体学生。如果图片名是学号那成绩表直接就能跟名单对上。pandas 写 Excel 需要 openpyxl装一下 pip install openpyxl 就行。批量处理时要注意图片尺寸不一致的问题透视校正会把它们统一到各自的实际尺寸切格用的是比例所以尺寸不同不影响。3.3 识别结果的可视化校验判卷最怕的是识别错了还不知道。所以每张答题卡处理完最好生成一张标注图把识别为涂了的格子框出来人工抽查几张就能判断阈值是否合适。def draw_result(warped, detected, total_rows, total_cols): # detected 是识别出的填涂坐标列表 h, w warped.shape[:2] cell_h h // total_rows cell_w w // total_cols for (r, c) in detected: cv2.rectangle(warped, (c * cell_w, r * cell_h), ((c 1) * cell_w, (r 1) * cell_h), (0, 0, 255), 2) cv2.imwrite(check.jpg, warped)红色框标出程序认为涂了的选项跟原图对照一眼就能看出有没有误判。这个步骤在调试阈值阶段特别有用正式跑批时可以关掉以提速。可视化校验是判卷系统里最容易被忽略但最该保留的环节因为一旦阈值偏了整批成绩都是错的。4. 避坑与排查答题卡识别判卷最容易翻车的五个地方4.1 现象所有题都识别成没涂占比全是 0原因通常是二值化方向反了。cv2.threshold 用 THRESH_BINARY_INV 是把暗的变白、亮的变黑如果答题卡本身是深色底浅色格方向就错了。解决方法是先打印一张格子的灰度直方图看填涂区的灰度分布确认暗色是填涂还是背景。另一个可能是切格坐标算错切到了空白区域检查锚点定位的四个角点顺序是否正确。4.2 现象同一张卡有的题识别对有的题识别错这是切格偏移的典型表现。透视校正后图像尺寸跟预期不一致导致按比例算的格子位置整体偏移。解决方法是校正后先固定输出尺寸比如统一缩放到 1000x1400再按固定像素坐标切格而不是按比例。固定尺寸的好处是每个格子的坐标是常量调试时可以直接在图上画网格线核对。4.3 现象光照不均时阈值失效左边识别对右边识别错Otsu 全局阈值在光照不均时会把暗的一侧整体判为涂了。解决方法是改用自适应阈值 cv2.adaptiveThreshold它按局部区域算阈值能抵消光照渐变。参数 blockSize 取 11 到 31 之间的奇数C 取 2 到 10具体值拿真实图片试。如果还不行就在拍照环节加个补光灯从源头解决光照问题比在算法上补救更省事。4.4 现象铅笔涂得浅占比只有 0.1被判为没涂阈值定太高了。解决方法是把阈值降到 0.1 到 0.15同时把二值化的阈值调低让浅灰也算黑色。但降阈值会带来误判风险没涂的格子如果有污渍也可能超过 0.1。折中办法是同时看占比和连通域面积只有占比超过阈值且存在一个面积足够大的连通域才算涂了这样能过滤掉零散污渍。4.5 现象批量处理时程序崩溃或卡死常见原因是图片尺寸差异太大或者某张图片损坏读不出来。解决方法是在读图后加判断 if image is None: continue跳过损坏文件。另外 cv2.imread 不支持中文路径如果文件夹名或文件名带中文要用 cv2.imdecode 配合 np.fromfile 读取。批量处理时加个进度打印卡在哪张图一目了然。5. 把识别率从 90% 推到 99%三个我踩过坑才总结出的技巧第一个技巧是给答题卡加定位锚点。很多现成的答题卡只有四角黑块一旦黑块被裁掉或遮挡透视校正就失败。我后来在答题卡设计阶段就加了四个定位圆点用 cv2.HoughCircles 检测比找四边形轮廓稳得多。圆点检测对旋转和部分遮挡都不敏感识别率直接从 90% 提到 97%。如果答题卡已经印好了改不了那就退而求其次用答题卡上的题号数字做锚点通过模板匹配定位。第二个技巧是判涂时不要只看单格占比要看整题的相对分布。同一题四个选项正常情况下最多一两个被涂如果四个格子占比都超过 0.2说明这一题切格整体偏了应该触发告警而不是硬判。我一般会算每题四个选项占比的方差方差太小就标记为可疑人工复核。这个逻辑加上去之后误判率降了一个数量级。第三个技巧是留后悔药每张答题卡的处理中间结果都存下来。校正后的图、二值化后的图、标注了识别结果的图各存一份。出问题的时候不用重新跑直接看中间图就能定位是哪一步错了。存储成本很低一张图几百 KB但排查效率提升明显。我现在的习惯是任何图像处理项目中间结果一律落盘宁可多占点硬盘也不要出了问题抓瞎。最后说个验证方法拿一批已经人工判过的答题卡做基准程序跑一遍逐题对比算准确率。准确率低于 98% 就不要上线回去调阈值和切格。上线之后也要定期抽检因为答题卡印刷批次不同、学生用笔不同识别率会漂移。这套方案值不值得做如果判卷量每周超过两百张值得如果一学期就判两次手工可能更快。希望帮到你。本文还有配套的精品资源点击获取