
简介这份PDF文献面向教育技术研究者、机器视觉方向的学生与系统开发人员针对传统人工合分效率低、易出错、Excel录入繁琐等痛点给出了一套基于机器视觉的试卷分数智能识别系统完整设计方案。资源包为1个PDF文件大小约1.26MB内容以论文形式呈现涵盖系统架构、硬件选型与算法流程便于直接阅读与引用。文中详细拆解了图像采集环节的工业相机、工业镜头与环形光源配置图像预处理中的去噪与对比度增强分数轮廓边缘提取算法以及文字OCR识别算子和分数统计报表生成等模块并附有系统架构图、软件流程图与识别对比试验数据。目前已有138人学习适合作为课程设计、毕业设计或相关课题的参考文献帮助读者快速理解从硬件搭建到软件算法落地的整体思路掌握轮廓识别与OCR结合的技术要点为后续开发或论文写作提供专业指导。1. 从一张手写分数到结构化成绩机器视觉试卷分数识别到底在做什么期末阅卷结束教务要的是每道题的得分、总分、班级排名可眼前只有一摞摞卷面上红笔写的数字。人工录入一个班五十人、每张卷子十几个分数两小时起步还容易看串行。机器视觉加OCR的试卷分数智能识别系统要解决的就是把「卷面上手写的红色分数」自动变成「Excel 里一行行结构化数据」这件事。它适合两类人一类是学校教务或考试机构的技术负责人想评估这套方案能不能落地另一类是做机器视觉方向的工程师想找一个完整闭环的练手项目。核心难点不在「识别数字」本身而在分数是手写的、颜色是红的、位置是随题的、卷面还有印刷体干扰。这篇笔记按我实际做过的路径从图像预处理一路讲到分数与题号的绑定把参数、代码和翻车点都摊开。2. 试卷分数识别的技术选型为什么不是直接丢给通用 OCR2.1 通用 OCR 在红色手写分数上为什么会翻车很多人第一反应是调一个通用 OCR 接口把整张卷子图片传上去让它自己找数字。我最早也这么干过结果很惨。通用 OCR 的训练数据以印刷体、黑字、规整排版为主而试卷分数有三个致命特征手写、红色、孤立。手写数字的笔画粗细不均、连笔、倾斜印刷体模型没见过红色在灰度化后对比度会塌掉如果预处理没做好红笔分数直接糊成一片分数是孤立的两位数或三位数没有上下文语义OCR 的语言模型帮不上忙反而可能把「87」纠成「8 7」或者「B7」。所以正确的思路是分两段先用机器视觉把「分数区域」从整张卷面里抠出来再对抠出来的小图做针对性的数字识别。前者靠颜色分割和轮廓识别后者靠轻量级分类模型或专门训练的数字 OCR。这个分工是整个系统能不能跑通的关键把通用 OCR 当万能药是第一个大坑。2.2 颜色空间选择HSV 比 RGB 更适合抠红笔红笔分数的分割核心是「把红色像素挑出来」。在 RGB 空间里红色是 R 高、G 低、B 低但受光照影响极大同一支笔在阴影下和强光下 R 值能差几十。换到 HSV 空间红色集中在 H 的 0 附近或 180 附近取决于 OpenCV 的 0-180 范围S 和 V 控制饱和度和亮度对光照的鲁棒性好得多。我一般用 OpenCV 做这件事先把 BGR 转 HSV再用两个区间卡红色因为红色在 H 轴上跨越 0 和 180 两端import cv2 import numpy as np def extract_red_mask(image_path): img cv2.imread(image_path) # 转 HSVOpenCV 中 H 范围是 0-179 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 红色在 H 轴两端需要两个区间合并 # 区间一H 在 0-10S 和 V 设下限过滤浅色和暗色 lower_red1 np.array([0, 70, 50]) upper_red1 np.array([10, 255, 255]) # 区间二H 在 170-179 lower_red2 np.array([170, 70, 50]) upper_red2 np.array([179, 255, 255]) mask1 cv2.inRange(hsv, lower_red1, upper_red1) mask2 cv2.inRange(hsv, lower_red2, upper_red2) mask cv2.bitwise_or(mask1, mask2) # 形态学闭运算把断开的笔画连起来 kernel np.ones((3, 3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) return img, mask这段代码的逻辑是inRange把落在红色区间的像素置 255其余置 0得到二值掩膜。两个区间用bitwise_or合并是因为红色在 H 轴上不连续。lower里的 S 下限 70 用来排除偏灰的红色比如褪色的红笔V 下限 50 排除过暗的噪点。形态学闭运算的iterations2是我调出来的经验值太小笔画断太大相邻分数会粘连。参数不是死的换一批卷子要重新看掩膜效果。2.3 轮廓识别定位分数面积和长宽比是两道筛子拿到红色掩膜后用findContours找连通区域每个区域可能是一个分数、一个红勾、一个红叉或者批改痕迹。要筛出「分数」靠两个几何特征面积和长宽比。手写分数通常是两位数或三位数横向排列长宽比偏大宽比高长面积在一个合理区间内。红勾红叉面积小且形状不规则会被面积下限滤掉。def locate_score_regions(mask, min_area300, max_area20000, min_ratio1.2): contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) regions [] for cnt in contours: area cv2.contourArea(cnt) if area min_area or area max_area: continue x, y, w, h cv2.boundingRect(cnt) ratio w / float(h) if h 0 else 0 if ratio min_ratio: continue # 适当外扩避免笔画边缘被切掉 pad 5 regions.append((max(0, x - pad), max(0, y - pad), w 2 * pad, h 2 * pad)) # 按从上到下、从左到右排序方便后续和题号绑定 regions.sort(keylambda r: (r[1] // 50, r[0])) return regionsmin_area300和max_area20000是像素面积取决于你的扫描分辨率。300 dpi 下一个两位数分数大概在 800 到 5000 像素之间红勾通常小于 300。min_ratio1.2是长宽比下限单个数字接近正方形比例约 1两位数比例约 1.5 到 2.5所以 1.2 能滤掉大部分单个符号。regions.sort里的r[1] // 50是按行分组的技巧同一行的分数 y 坐标接近除以 50 取整后能归到同一组再按 x 排序得到阅读顺序。这个 50 要根据行高调整行距大的卷子要调大。3. 从分数小图到数字识别模型与后处理怎么配3.1 手写数字识别轻量 CNN 比调接口更可控抠出来的分数小图接下来要识别成数字。这里有两个选择调云端 OCR 接口或者自己训一个轻量 CNN。我倾向后者原因有三分数是纯数字类别只有 0-9 加一个小数点问题被极大简化自己训的模型可以离线跑批量处理几千张卷子不受接口限流影响最关键的是可控识别错了能看混淆矩阵定位是哪个数字的问题调接口只能看到返回结果是个黑匣子。网络结构不用复杂两个卷积块加全连接就够。输入统一缩放到 32x32 灰度图输出 11 类0-9 和「.」。训练数据用公开的手写数字数据集如 MNIST打底再拿自己扫描的分数小图做微调。MNIST 是黑字白底我们的分数是红字白底预处理时统一转成灰度并反色让笔画为白、背景为黑和 MNIST 对齐。import torch import torch.nn as nn class DigitCNN(nn.Module): def __init__(self, num_classes11): super().__init__() self.features nn.Sequential( # 输入 1x32x32 nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 16x16 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 8x8 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))两个卷积块负责提取笔画特征Dropout(0.3)防过拟合因为手写数字样本量通常不大。输入 32x32 是权衡再小笔画细节丢失再大推理变慢且收益有限。训练时学习率用 1e-3batch size 64跑 20 个 epoch 基本收敛。如果某类数字比如 1 和 7、4 和 9混淆严重针对性补这类样本比调网络结构有效。3.2 分数与题号的绑定位置关系比 OCR 更可靠识别出数字还不够得知道这个分数是哪道题的。常见做法是试卷模板固定每道题的得分框位置已知把识别到的分数按坐标匹配到最近的得分框。如果卷面没有固定得分框就靠「题号」这个锚点——题号通常是印刷体用通用 OCR 识别题号位置再把分数按空间关系分数在题号右侧或下方绑定。我一般用模板匹配的思路先对一张空白卷做标注记录每道题得分框的坐标存成 JSON。后续所有卷子都按这个模板对齐用卷面印刷的定位点做仿射变换校正再把分数坐标映射回模板坐标就近绑定。这样比纯靠 OCR 读题号稳因为题号可能被红笔覆盖或模糊而坐标关系是几何的不受识别错误影响。import json def bind_scores_to_questions(regions, template_path, max_dist80): with open(template_path, r, encodingutf-8) as f: template json.load(f) # [{qid: 1, box: [x, y, w, h]}, ...] result {} for (rx, ry, rw, rh) in regions: cx, cy rx rw / 2, ry rh / 2 best_qid, best_dist None, float(inf) for item in template: tx, ty, tw, th item[box] tcx, tcy tx tw / 2, ty th / 2 dist ((cx - tcx) ** 2 (cy - tcy) ** 2) ** 0.5 if dist best_dist: best_dist, best_qid dist, item[qid] # 距离超过阈值认为不匹配避免误绑 if best_dist max_dist: result.setdefault(best_qid, []).append((rx, ry)) return resultmax_dist80是像素距离阈值超过就认为这个分数不属于任何题可能是批改痕迹误检。这个值取决于卷面分辨率和得分框间距间距大可以放宽。result用setdefault是因为一道题可能有多个得分点比如小题分和总扣分都归到同一题号下。3.3 后处理小数点、连笔和置信度过滤模型输出的是每个字符的类别和置信度直接拼成数字会出问题。三个后处理必须做第一小数点识别如果模型把「.」识别成「1」或漏掉87.5 会变成 875 或 87需要根据字符间距和位置判断小数点第二连笔拆分手写「4」和「9」连在一起时轮廓是一个整体识别会错可以在分割阶段用垂直投影找波谷切分第三置信度过滤低于阈值比如 0.6的结果标记为「待人工复核」不直接入库。置信度过滤这条特别重要我吃过亏。早期为了追求自动化率把所有结果都入库结果期末统计时发现几个班的平均分异常回头查是几个低置信度的「6」被识别成「0」。后来加了复核队列低置信度的分数在界面上高亮人工点一下确认整体准确率从 92% 提到 99% 以上而人工只需要处理不到 5% 的分数。4. 避坑与排查试卷分数识别里最容易翻车的五件事4.1 现象红色掩膜把整张卷子都框住了原因HSV 的 S 或 V 下限设太低导致偏红或偏灰的印刷体、甚至纸张底色都被算进红色区间。有些卷子用浅红色印刷题号也会被误抓。解决把 S 下限从 70 提到 100 以上V 下限提到 80先跑一张图看掩膜只保留明显的红笔区域。如果卷面有浅红印刷再加一个面积上限印刷题号通常比手写分数小。4.2 现象相邻两个分数被识别成一个原因形态学闭运算的 kernel 太大或 iterations 太多把两个靠近的分数连成一个连通区域。或者两个分数本身写得太近轮廓外扩的 pad 让它们重叠。解决闭运算 kernel 从 3x3 起调iterations 不超过 2。如果还粘连在轮廓阶段加一步对宽高比异常大的区域比如比例超过 5用垂直投影找波谷强制切分。pad 从 5 降到 2 也能缓解。4.3 现象模型把「1」和「7」、「4」和「9」认混原因手写风格差异1 带钩像 74 的竖和 9 的圈连笔。训练数据里这两类样本不均衡或不够多样。解决针对性收集混淆样本每类补 200 张以上重新微调。输入分辨率从 32x32 提到 48x48 也能改善因为笔画细节更多。另外在推理时如果置信度在 0.5 到 0.7 之间且类别是 1 或 7直接送人工复核不硬判。4.4 现象分数识别对了但绑错了题号原因模板对齐失败卷子扫描时放歪了或缩放比例不一致导致坐标映射偏移。或者一道题的得分框和相邻题的距离小于 max_dist就近绑定绑到了隔壁。解决在卷面四角或边缘放定位标记比如黑色方块用findContours找标记做仿射变换校正比靠印刷内容对齐稳。max_dist 根据得分框最小间距设一般取间距的一半。绑定后加一步校验如果一道题绑定了超过 3 个分数大概率是误绑标记复核。4.5 现象批量处理时内存爆掉或速度极慢原因一次性把所有卷子读进内存或者对每张图都重新加载模型。高分辨率扫描图比如 600 dpi单张就几十 MB几百张直接 OOM。解决用生成器逐张读图处理完立即释放。模型只加载一次放在循环外。如果还是慢把图像缩放到 300 dpi 再处理分数识别不需要 600 dpi 的精度缩放后速度能快一倍以上准确率几乎不掉。5. 把系统跑稳的进阶技巧模板校准与增量学习系统能跑通之后真正决定它能不能长期用的是两件事模板校准的自动化和模型的增量学习。模板校准这块我现在的做法是每批卷子先抽 5 张自动检测定位标记并计算仿射变换矩阵如果 5 张的矩阵差异超过阈值说明扫描设备或摆放有问题先报警让操作员检查而不是硬跑。这样能把「整批绑错题号」这种灾难性错误挡在入口。定位标记用实心黑方块边长约卷面宽度的 2%放在四角内侧cv2.findContours配合approxPolyDP找四边形四个标记的中心点算透视变换。这套流程跑下来模板对齐的成功率在 99% 以上剩下 1% 是卷子折角或标记被遮挡走人工。增量学习是另一个关键。学校每学期的手写风格会变新来的老师红笔颜色也可能不同。我一般每处理完一批卷子把置信度低于 0.7 的样本自动存到一个待标注池每周人工标一批通常几十张加入训练集重新微调模型。微调时学习率降到 1e-4只跑 5 个 epoch避免把之前学好的特征冲掉。这样模型能跟着实际数据慢慢进化而不是一次训练管一辈子。技巧触发条件操作预期收益模板自动校准每批卷子开头抽 5 张检测定位标记算仿射矩阵挡住整批绑错低置信度池每批处理完存置信度0.7 样本积累难例增量微调每周标注后 lr1e-4 跑 5 epoch适应新风格复核队列实时低置信度高亮人工确认准确率到 99%最后说个我自己的习惯每次上线新模型前一定拿上一批已经人工核对过的卷子做回归测试看准确率有没有掉。有一次微调后整体准确率涨了但「0」的召回率掉了 3 个点回归测试抓出来了否则期末统计又要出问题。这套系统没有一劳永逸靠的是模板校准兜底、增量学习跟进、回归测试把关。希望帮到你。本文还有配套的精品资源点击获取