尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python+OpenCV答题卡识别判分系统:从拍照到Excel成绩单全流程
简介这份资源是面向高校学生与开发者的计算机视觉实战项目包对应毕业设计、课程设计等场景核心是用Python实现答题卡图像的自动识别与判分。项目以Django搭建Web服务端借助OpenCV完成灰度化、二值化、边缘与轮廓检测、模板匹配等图像处理流程并配合数据库存储题目、答案与学生信息形成从上传答题卡到输出评分的完整闭环。压缩包共287个文件约9.15MB包含30个py源码、32个pyc编译文件、48个js脚本、26个css与12个html页面以及gif、png、jpg等界面与演示素材另有sql建表脚本和说明文档前端采用bootstrap与layui等框架目录结构清晰便于按模块阅读与二次开发。目前已有418人学习下载适合想了解计算机视觉与Web开发结合方式、需要可运行参考项目的读者可据此掌握图像预处理、特征定位、答案匹配及Django视图与ORM的落地思路。1. 从一张歪着拍的答题卡说起这套 Python 计算机视觉判分系统到底解决什么问题答题卡识别这件事真正做过的人都知道难点从来不是识别 ABCD而是老师用手机随手一拍、光线不均、纸张带折痕、四个角标还被手指挡住一角的那种图。基于 Python 的计算机视觉答题卡识别及判分系统要干的就是把这种脏图变成一张干净的成绩表先定位答题卡区域并做透视矫正再切出每个题目的填涂框判断哪一框被涂黑最后对照标准答案算分并导出结果。它适合两类人一类是学校里想给单元测验做快速批改的老师或教务另一类是正在找计算机视觉项目练手、想跑通一条完整链路的同学——从图像预处理到几何变换再到判分逻辑这条链路几乎把计算机视觉入门阶段该练的基本功都串了一遍。整套东西用 Python 加 OpenCV 就能落地不需要 GPU一台普通笔记本足够跑。2. 答题卡识别判分系统的技术链路从拍照到成绩单要过哪几道关2.1 整条流水线的五个阶段把系统拆开看一张答题卡从进到出要经过五个阶段每个阶段都有明确的输入输出这样出问题时才能定位到底是哪一环翻车。第一阶段是图像获取与预处理。输入是手机或扫描仪给的原始图输出是灰度化、去噪、二值化之后的干净图。这一步的目标不是好看而是让后续的边缘检测能稳定找到答题卡的外轮廓。常见做法是先转灰度再用高斯模糊压掉噪点最后用自适应阈值或者 Canny 拿到边缘。第二阶段是答题卡定位与透视矫正。输入是二值边缘图输出是正对着你的矩形答题卡。核心是找轮廓、筛出面积最大的四边形、取四个角点然后用透视变换把它拉正。这一步是整个系统里最容易出玄学的地方角点找偏一点后面所有题目的坐标全跟着偏。第三阶段是填涂区域切分。答题卡拉正之后题目位置就相对固定了。你可以按比例硬切也可以先检测定位黑块再按行列推算。硬切简单但要求模板固定检测定位灵活但代码复杂选哪个取决于你的答题卡是不是统一印刷的。第四阶段是填涂判定。对每个选项框算黑色像素占比超过阈值就算涂了。听起来简单但阈值怎么定、多涂怎么处理、擦除痕迹怎么算全是细节。第五阶段是判分与输出。把识别结果和标准答案比对算总分导出成 Excel 或者 CSV。这一步纯逻辑没有视觉成分但格式设计不好老师用起来照样骂人。2.2 为什么选 OpenCV 而不是深度学习方案很多人一上来就想上 YOLO 或者 CNN 做端到端识别觉得这样高级。但对于答题卡这种结构化极强的场景传统计算机视觉方案反而更稳、更快、更好调。原因有三。第一答题卡的几何结构是已知的四个角标、固定的行列布局这些先验信息用传统方法能直接吃进去而深度学习方案反而要把这些先验学出来浪费算力。第二传统方案不需要标注数据你不需要标几千张答题卡才能跑起来改个阈值就能适配新模板。第三可解释性强识别错了你能一步步看是哪一环节出的问题而不是面对一个黑匣子干瞪眼。当然如果你的场景是任意角度、任意光照、任意模板的极端情况那深度学习确实有优势。但绝大多数校内测验场景答题卡是统一印刷的传统方案足够用而且部署成本低得多。2.3 环境准备与最小可运行骨架先把环境搭起来。Python 版本建议 3.8 以上核心依赖就三个OpenCV、NumPy、openpyxl导出 Excel 用。安装命令如下pip install opencv-python numpy openpyxl如果你用 PyCharm 或者 VS Code直接在项目里建虚拟环境再装避免污染全局。装完之后跑一句import cv2; print(cv2.__version__)确认没问题。下面是一个最小骨架把整条流水线的函数签名先立起来后面几章逐个填实现import cv2 import numpy as np def preprocess(image_path): 读图并做灰度、模糊、边缘检测 img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) edged cv2.Canny(blurred, 75, 200) return img, edged def find_card_contour(edged): 找面积最大的四边形轮廓 contours, _ cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours sorted(contours, keycv2.contourArea, reverseTrue) for c in contours[:5]: peri cv2.arcLength(c, True) approx cv2.approxPolyDP(c, 0.02 * peri, True) if len(approx) 4: return approx return None def four_point_transform(image, pts): 透视变换把答题卡拉正 rect order_points(pts.reshape(4, 2)) (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) return cv2.warpPerspective(image, M, (width, height)) 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这段代码里几个参数值得说清楚。GaussianBlur的核大小(5, 5)是经验值图越大可以适当加大到(7, 7)但必须是奇数。Canny的两个阈值75和200是低阈值和高阈值低阈值管弱边缘要不要保留高阈值管强边缘从哪开始光照差的时候把低阈值降到 50 左右能救回一些断掉的边。approxPolyDP里的0.02 * peri是逼近精度值越小越贴近原始轮廓但太小会导致本该是四边形的轮廓被逼近成五六个点一般 0.02 到 0.05 之间调。order_points这个函数是透视变换的关键。它用坐标和找左上和最小和右下和最大用坐标差找右上差最小和左下差最大。这个逻辑对正放的答题卡没问题但如果答题卡旋转超过 45 度就可能排错后面避坑章节会讲怎么处理。3. 填涂区域切分与判分逻辑坐标怎么定、阈值怎么调3.1 按模板比例切分 vs 按定位块动态切分答题卡拉正之后接下来要把每个题目的选项框切出来。这里有两条路。第一条是按模板比例硬切。假设你的答题卡是 A4 大小拉正后图像宽高固定那么第 1 题 A 选项的框位置就是固定的像素坐标。你只需要在代码里写死一组坐标或者写一个比例表按图像实际尺寸换算。优点是简单、快、不需要额外检测缺点是换一种答题卡模板就得改坐标而且对透视矫正的精度要求高矫正偏几个像素切出来的框就可能压线。第二条是按定位块动态切分。答题卡上通常有黑色的定位方块或者定位线先检测这些定位块的位置再根据它们推算每个题目框的坐标。优点是适应性强轻微形变也能扛缺点是多了一层检测逻辑定位块本身没检测到就全盘崩。我一般会这么做如果答题卡是学校统一印刷、模板固定的直接用比例切分代码简单可控如果是多种模板混用或者扫描质量参差不齐就上定位块方案。下面给的是比例切分的实现因为它是大多数项目的起点。def split_bubbles(warped, rows, cols, start_x, start_y, gap_x, gap_y, box_size): 按固定行列切分选项框 rows: 题目行数 cols: 每题选项数一般 4 start_x, start_y: 第一个框左上角坐标 gap_x, gap_y: 框之间的水平和垂直间距 box_size: 每个框的边长 boxes [] for r in range(rows): row_boxes [] for c in range(cols): x start_x c * gap_x y start_y r * gap_y box warped[y:y box_size, x:x box_size] row_boxes.append(box) boxes.append(row_boxes) return boxes参数说明start_x和start_y需要你在拉正后的图上量一次量准了后面就固定。gap_x是同一题相邻选项框的间距gap_y是相邻题目之间的行距。box_size是框的边长一般取比实际填涂框略小一点避免把框线本身算进去。这几个参数建议做成配置文件或者常量方便换模板时改。3.2 填涂判定的阈值怎么定才不飘切出框之后判定逻辑就是算黑色像素占比。核心代码def is_filled(box, threshold0.35): 判断一个选项框是否被涂黑 gray cv2.cvtColor(box, cv2.COLOR_BGR2GRAY) if len(box.shape) 3 else box _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) black_ratio np.sum(binary 255) / binary.size return black_ratio threshold, black_ratio这里用了 OTSU 自动阈值做二值化比固定阈值稳。THRESH_BINARY_INV是把黑色变成 255、白色变成 0这样统计 255 的数量就是黑色像素数。threshold0.35是判定阈值意思是黑色占比超过 35% 就算涂了。这个 0.35 不是拍脑袋来的。正常涂满的框黑色占比在 0.6 到 0.8 之间完全没涂的在 0.05 以下半涂或者擦除痕迹在 0.2 到 0.4 之间。取 0.35 是为了把涂了一半也算作有效填涂同时排除掉框线本身和噪点。如果你的答题卡框线比较粗或者扫描分辨率低导致框内噪点多这个值要往上调到 0.4 甚至 0.45。实际项目里我建议不要只用一个全局阈值而是对每个框算完占比后先收集所有框的占比分布看看有没有明显的双峰。如果有取谷底作为阈值如果没有明显双峰说明图像质量有问题得回头查预处理。3.3 多涂、漏涂和擦除痕迹的处理真实答题卡上什么情况都有。一个题涂了两个选项这是多涂一个都没涂这是漏涂涂了又擦留下一片灰这是擦除痕迹。这三种情况判定逻辑要分开处理。多涂的处理如果一个题有超过一个框的占比超过阈值标记为多涂判分时算错同时在结果里标注出来让老师人工复核。不要自作聪明取占比最高的那个因为学生可能就是涂了两个。漏涂的处理如果一个题所有框的占比都低于阈值标记为漏涂同样算错并标注。擦除痕迹的处理擦除痕迹的占比通常在 0.2 到 0.35 之间正好卡在阈值边缘。这时候可以加一个灰区判断占比在 0.2 到 0.35 之间的框标记为疑似输出时高亮让老师决定。这个灰区范围要根据你的实际图像调不是固定的。def judge_question(boxes, high0.35, low0.20): 返回该题的判定结果和状态 ratios [is_filled(b)[1] for b in boxes] filled [i for i, r in enumerate(ratios) if r high] suspect [i for i, r in enumerate(ratios) if low r high] if len(filled) 1: return filled[0], normal elif len(filled) 0: return None, blank if not suspect else suspect else: return None, multi这段逻辑把每道题分成 normal、blank、suspect、multi 四种状态判分时只有 normal 才参与对错计算其余都算错但保留状态供复核。这样老师拿到结果能一眼看出哪些是机器不确定的而不是被一个错误的自动判分误导。4. 避坑与排查那些让识别率从 95% 掉到 60% 的细节4.1 现象透视矫正后答题卡是歪的切出来的框全部压线原因order_points用坐标和、坐标差排序在答题卡旋转角度接近 45 度时会排错导致透视变换的四个目标点对错位置拉正后图像被翻转或拉伸。解决不要只依赖坐标和排序。更稳的做法是先算四个点的质心然后按极角排序再根据角度关系确定左上、右上、右下、左下。或者加一个校验变换后图像的宽高比如果和预期差太多就换一种排序方式重试。4.2 现象同一张答题卡白天拍识别正常晚上开灯拍就大面积漏检原因Canny 的固定阈值对光照敏感。晚上灯光偏黄、对比度低边缘检测出来的轮廓断断续续findContours找不到完整的四边形。解决把 Canny 换成自适应阈值或者在做 Canny 之前先做一次直方图均衡化。更省事的办法是用cv2.adaptiveThreshold替代 Canny 做二值化它对局部光照变化更鲁棒。如果坚持用 Canny把低阈值从 75 降到 50高阈值从 200 降到 150能救回大部分暗光图。4.3 现象填涂判定把框线当成了填涂所有题都判成多涂原因切分时box_size取大了把选项框的黑色边框也切了进去。边框的黑色像素占比可能就有 0.3 以上加上框内如果有一点填涂直接超过阈值。解决两个办法。一是把box_size缩小只取框内部区域一般比实际框小 20% 到 30%。二是在判定前先做一次形态学腐蚀把细线条去掉。我一般两个一起用先缩框再腐蚀基本能解决。4.4 现象程序跑起来很慢一张图要十几秒原因findContours在整张高分辨率图上跑轮廓数量巨大或者透视变换后的图没有缩放后续切分和判定都在大图上做。解决读图之后先缩放到固定宽度比如 1000 像素再做后续所有处理。答题卡识别不需要原始分辨率1000 像素宽足够切出准确的框。另外findContours之前先做一次形态学闭运算把碎轮廓合并能减少轮廓数量。这两步做完一张图通常能压到 1 秒以内。4.5 现象换了一种答题卡模板所有坐标全错原因比例切分的坐标是写死的换模板等于换布局坐标自然对不上。解决把坐标参数抽出来做成配置字典每种模板一个配置。更彻底的做法是上定位块方案让程序自己找定位块再推算坐标。如果只是偶尔换模板配置字典就够了如果模板经常变值得花时间做定位块检测。5. 把判分结果导出成老师能直接用的 Excel以及一个提高鲁棒性的小技巧判分逻辑跑通之后最后一步是输出。老师不会去看你的控制台打印他们要的是能直接打开、能排序、能打印的表格。用 openpyxl 导出 Excel 是最实用的选择。from openpyxl import Workbook def export_result(results, answer_key, output_path): results: 每题的识别结果列表元素为 (题号, 选项索引, 状态) answer_key: 标准答案列表元素为选项索引 wb Workbook() ws wb.active ws.title 判分结果 ws.append([题号, 识别选项, 标准答案, 状态, 得分]) total 0 for i, (q_num, choice, status) in enumerate(results): correct answer_key[i] if status normal and choice correct: score 1 total 1 else: score 0 choice_str chr(65 choice) if choice is not None else - answer_str chr(65 correct) ws.append([q_num, choice_str, answer_str, status, score]) ws.append([, , , 总分, total]) wb.save(output_path) return total这段代码把每题的结果、标准答案、状态和得分写进表格最后一行汇总总分。status列很关键老师可以按这一列筛选出所有非 normal 的题目快速定位需要人工复核的地方。选项索引用chr(65 choice)转成 A、B、C、D比直接输出数字友好得多。导出之后建议再加一步把原始答题卡图、矫正后的图、以及标注了识别结果的图一起存下来。这样老师如果对某道题有疑问能直接翻图核对而不是只能相信表格里的数字。这个后悔药在项目刚上线、识别率还没调稳的时候特别有用。最后说一个提高鲁棒性的小技巧不要只跑一次识别就出结果而是对同一张图跑两次一次用原始参数一次用稍微放宽的阈值然后对比两次结果。如果两次结果一致说明这道题识别很稳如果不一致标记为低置信度让老师重点看。这个双跑策略几乎不增加多少计算量但能把误判率压下去一截。我自己做过的几个版本里加了这一步之后需要人工复核的题目比例从 15% 降到了 5% 左右老师的使用意愿明显不一样。这套系统从拍照到出成绩单核心链路就是预处理、透视矫正、切分、判定、导出五步每一步都有可以调的参数和可以踩的坑。先把最小链路跑通再针对你自己的答题卡模板和拍摄条件去调阈值和坐标比一上来就追求全自动高精度要靠谱得多。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Keil软件仿真进不了main?STM32启动流程与内存映射避坑指南

Keil软件仿真进不了main?STM32启动流程与内存映射避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/28 1:15:44
CAN通信实战:从终端电阻到数据解析的自主导航避坑指南

CAN通信实战:从终端电阻到数据解析的自主导航避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/28 1:15:44
Modbus变量编址与软元件映射:汇川CodeSys平台从入门到实战

Modbus变量编址与软元件映射:汇川CodeSys平台从入门到实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/28 1:15:44
MORE NEWS

更多资讯

📰

Keil 实时调试:不暂停程序也能监控变量的几种方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Hierarchical Flow避坑指南:数字IC设计从逻辑综合到布局布线的分层实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

YOLOv11与K230端侧部署实战:从模型训练到int8量化推理优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

FPGA远程升级双保险:MultiBoot与看门狗回退机制详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

ESP32与INMP441 I2S麦克风音频采集实战:从硬件连接到WAV存储

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

车规级芯片功能安全机制拆解:从ISO 26262到SPFM/LFM的工程落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬