
简介这份PDF文档面向教育技术研究者、机器视觉方向学生及学校信息化建设人员系统讲解了一套基于机器视觉的试卷分数智能识别系统设计方案用于解决人工合分速度慢、易出错、效率低等实际问题。文档共1个PDF文件压缩包约1.26MB内容涵盖系统架构、硬件模块与软件模块的完整设计思路。硬件部分详细介绍了工业相机、工业镜头与环形光源的选型依据及主要参数软件部分则围绕图像采集、预处理、分数轮廓边缘提取、文字OCR识别与分数统计分析等关键环节展开并给出算法验证与对比试验结果。读者可从中获取从硬件搭建到算法实现的完整技术路线理解轮廓识别与OCR算子结合的具体做法并参考其系统界面设计与试验数据为课程设计、毕业课题或相关项目开发提供可借鉴的参考文献与专业指导。目前已有137人学习关注。1. 从一张手写分数说起机器视觉怎么把试卷分数读准改卷老师最烦的不是批改是批改完之后那一摞分数录入。一个班五十份卷子每份正面的分数框里手写一个数字教务要求当天出成绩人工敲一遍至少半小时还容易把 7 敲成 1、把 0 敲成 6。我最早接触「试卷分数智能识别」这个方向就是因为一所中学的教务主任拿着手机拍的一叠卷子问我能不能让机器自己把分数读出来直接进 Excel。这件事的本质是机器视觉里一个非常典型的受限场景 OCR 问题目标区域固定、字符集极小通常只有 0-9 和少量符号、背景是印刷体表格线、前景是手写数字。它不像通用 OCR 那样要面对千变万化的版面也不像票据识别那样字段繁多。正因为约束多反而容易做稳。整套系统拆开看就是四步图像采集与预处理、分数区域定位轮廓识别为主、字符分割与识别OCR 或轻量分类网络、结果校验与导出。适合谁做有 Python 基础、懂一点 OpenCV、想找一个完整机器视觉项目练手的人也适合学校信息中心或小团队做一套内部工具。下面我按自己实际搭过的路径把每一步的参数、坑和验证方法讲清楚。2. 分数区域怎么定位轮廓识别与版面约束的配合2.1 为什么先定位再识别而不是整图丢给 OCR很多人第一反应是直接上 OCR把整张卷子丢进去让它自己找分数。我试过翻车得很彻底。通用 OCR 会把题干里的数字、题号、页码全部读出来你根本分不清哪个是分数。而且手写数字和印刷体混在一起识别引擎的置信度会互相干扰。正确思路是先用机器视觉把「分数框」这个 ROI感兴趣区域抠出来再只对这个区域做识别。试卷的分数框通常有强约束位置固定在卷首右上角或每道题号旁边、有印刷的方框或下划线、尺寸相对统一。这些约束就是定位的抓手。定位的主流做法有两类。一类是基于轮廓识别对图像二值化后找连通域用长宽比、面积、位置筛选出候选框。另一类是模板匹配预先裁一个标准分数框模板滑窗匹配。前者适应性更强后者在版面完全固定时更快。我一般用轮廓识别打底模板匹配做兜底校验。2.2 预处理把手机拍的歪卷子拉正手机拍摄的卷子一定有透视畸变和光照不均直接做轮廓会得到一堆碎块。预处理顺序我固定为灰度化 → 高斯模糊 → 自适应二值化 → 形态学闭运算 → 透视校正。import cv2 import numpy as np def preprocess(img_path): img cv2.imread(img_path) # 缩小到固定宽度减少后续计算量同时弱化手机高分辨率带来的噪点 h, w img.shape[:2] target_w 1200 scale target_w / w img cv2.resize(img, (target_w, int(h * scale))) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊核用 5x5太大糊掉细笔画太小去不掉纸张纹理 blur cv2.GaussianBlur(gray, (5, 5), 0) # 自适应二值化blockSize 取 31C 取 10对光照不均最稳 binary cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 10) # 闭运算连接断裂的框线核 3x3 足够 kernel np.ones((3, 3), np.uint8) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return img, closed这段代码里三个参数最关键。target_w1200是经验值A4 卷子在这个宽度下手写数字笔画大约 8-15 像素宽既够识别又不至于让轮廓检测跑太久。blockSize31控制自适应阈值的邻域大小卷面有阴影时调大到 41字迹淡时调小到 21。C10是阈值偏移值越大二值化越狠背景噪点多就往上加。透视校正需要先找到卷子的四个角点。常见做法是找最大轮廓的近似四边形用cv2.getPerspectiveTransform做映射。这一步不做后面所有坐标都是歪的分数框筛选会失准。2.3 用轮廓识别筛出分数框二值化之后分数框会表现为一个闭合的矩形轮廓。用cv2.findContours拿到所有轮廓然后按几何特征过滤。def find_score_boxes(binary, img_shape): contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) h, w img_shape[:2] candidates [] for cnt in contours: x, y, bw, bh cv2.boundingRect(cnt) area bw * bh aspect bw / float(bh) if bh else 0 # 分数框经验约束面积占比、长宽比、位置 if area 0.0005 * w * h or area 0.02 * w * h: continue if aspect 0.6 or aspect 3.0: continue # 分数框一般在卷面上半部分或题号右侧这里按需调整 candidates.append((x, y, bw, bh)) # 按 y 再按 x 排序保证输出顺序和阅读顺序一致 candidates.sort(keylambda b: (b[1] // 50, b[0])) return candidates参数说明面积下限0.0005*w*h是为了滤掉标点和噪点上限0.02*w*h是防止把整块答题区当成一个框。长宽比 0.6 到 3.0 覆盖了单数字框接近方形和横排分数框扁长。这两个阈值必须拿你自己学校的卷子实测调整不同排版差异很大。提示如果卷面有大量印刷表格线RETR_EXTERNAL会把整张表当成一个外轮廓。这时改用RETR_LIST并加一层「轮廓内是否有手写笔画」的判断或者先做直线检测把表格线抹掉。2.4 定位结果怎么验证定位对不对不能靠肉眼看几张图就下结论。我一般写一个批量验证脚本把检测到的框画在原图上输出到一个文件夹人工抽查 20 张。同时统计两个指标——召回率真实分数框被检出的比例和误检率非分数框被当成候选的比例。召回率低于 95% 就放宽面积和长宽比约束误检率高就收紧。这个循环通常要跑两三版才能稳定。3. 手写数字识别OCR 引擎与轻量分类网络怎么选3.1 通用 OCR 和自训练分类的边界分数区域抠出来之后识别环节有两条路。第一条是直接用 OCR 引擎比如 Tesseract、PaddleOCR 这类。第二条是自己训一个数字分类器把每个字符单独切出来送进 CNN。我的判断标准很简单如果你的分数是印刷体直接上 OCR配置好字符白名单只允许 0-9 和 . 就能到 99% 以上。如果是手写体通用 OCR 的准确率会掉到 70%-85%因为手写数字的笔画变异太大OCR 引擎的训练集以印刷体和规整手写为主。自训练分类器的优势在于字符集只有 11 类0-9 加点号数据需求小几百到几千张标注样本就能训到 98% 以上。代价是你得自己收集和标注数据还要处理字符分割。我一般建议先跑通 OCR 版本验证整条链路再根据实际准确率决定要不要上自训练模型。3.2 用 Tesseract 跑通最小可用版本先装环境。Tesseract 在 Windows 上直接下安装包Linux 用包管理器。# Ubuntu/Debian sudo apt-get install tesseract-ocr sudo apt-get install libtesseract-dev # Python 封装 pip install pytesseract opencv-python识别单个分数框的代码import cv2 import pytesseract import numpy as np def recognize_score(roi): # roi 是定位阶段裁出的分数框图像 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 放大有助于 Tesseract 识别小字符2 倍是常用值 gray cv2.resize(gray, None, fx2, fy2, interpolationcv2.INTER_CUBIC) # OTSU 二值化手写笔画和背景分离 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 关键配置--psm 7 表示单行文本-c 限定字符白名单 config --psm 7 -c tessedit_char_whitelist0123456789. text pytesseract.image_to_string(binary, configconfig) return text.strip()--psm 7是页面分割模式告诉 Tesseract 这是单行文本不要按段落分析。tessedit_char_whitelist限定只输出数字和小数点能挡掉大量误识别。放大 2 倍是因为 Tesseract 对小于 20 像素高的字符识别率明显下降。3.3 自训练数字分类器的数据与结构如果 OCR 版本准确率不够就上分类器。数据来源有两个一是从历史卷子里裁分数框人工标注二是用印刷体数字做数据增强加随机旋转、缩放、弹性形变模拟手写。网络结构不用复杂三层卷积加两层全连接足够import torch import torch.nn as nn class DigitNet(nn.Module): def __init__(self, num_classes11): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 64, 3, padding1), nn.ReLU(), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))输入统一缩放到 28x28 灰度图。Dropout(0.3)是防过拟合的关键手写样本少的时候尤其重要。训练时用交叉熵损失Adam 优化器学习率 1e-3batch size 64一般 20 个 epoch 收敛。3.4 字符分割粘连数字怎么切开分数框里可能是「95」「100」这种多位数分类器一次只能认一个字符必须先分割。投影法是最常用的对二值图做垂直投影统计每列的黑色像素数笔画之间的空白列就是切分点。def split_digits(binary): # binary 是白字黑底或黑字白底统一成黑字白底 col_sum np.sum(binary 0, axis0) # 阈值取列高的 5%低于认为无笔画 threshold binary.shape[0] * 0.05 splits [] in_char False start 0 for i, v in enumerate(col_sum): if v threshold and not in_char: start i in_char True elif v threshold and in_char: splits.append((start, i)) in_char False if in_char: splits.append((start, len(col_sum))) # 过滤过窄的碎片 return [s for s in splits if s[1] - s[0] 3]粘连的情况投影法会失效比如「1」和「0」挨在一起。这时用连通域分析或者上 CTC 这类序列识别方案。实际项目里我会在分割后加一步宽度校验单个数字宽度应该在框高的 0.3-0.7 倍之间超出就标记为可疑转人工复核。4. 避坑与排查分数识别系统上线前必须过的五道坎4.1 现象白天测得好好的晚上灯光下全乱原因自适应二值化的blockSize和C是在特定光照下调的换了光照条件阈值分布整体偏移轮廓检测跟着崩。解决不要用固定参数。在预处理前加一步光照归一化用 CLAHE限制对比度自适应直方图均衡把亮度拉平。或者干脆在采集端解决——固定拍摄台加环形补光灯把光照变量消掉。我吃过这个亏之后所有现场部署的方案都强制要求固定光源。4.2 现象分数框检出来了但识别结果是空的原因ROI 裁切时坐标算错或者裁出来的区域包含了大量框线二值化后笔画被框线淹没。解决裁切时向内收缩 10%-15% 的边距把框线排除在外。同时打印中间结果——把每个 ROI 单独存图肉眼确认裁的是不是分数区域。这个调试习惯能省掉大量瞎猜时间。4.3 现象同一个分数两次识别结果不一样原因Tesseract 对低质量图像的处理有随机性尤其是笔画断裂时。另外图像缩放用的插值方法也会影响结果。解决固定所有随机源。插值统一用INTER_CUBIC识别前做一次形态学闭运算把断裂笔画连上。如果还是不稳定说明图像质量已经低于 OCR 的可靠边界该上自训练模型了。4.4 现象小数点和数字「1」混淆原因手写小数点和短横线在低分辨率下形态接近字符白名单里同时有.和数字时容易串。解决先做位置判断——小数点一定在数字右下角且宽度明显小于数字。用连通域的位置和尺寸做二次校验。或者干脆在分数场景里禁用小数点因为试卷分数极少有小数。4.5 现象批量处理时越跑越慢内存爆掉原因OpenCV 和 PyTorch 的对象没有及时释放循环里不断累积。解决每处理完一张图显式del掉大对象必要时调gc.collect()。PyTorch 推理时用torch.no_grad()包住避免构建计算图。批量处理改成流式不要一次性把所有图读进内存。5. 把识别结果接进教务流程校验规则与置信度兜底识别出分数只是第一步真正决定这套系统能不能用的是「错了怎么办」。我的做法是给每个识别结果配一个置信度低于阈值的自动转人工复核而不是硬着头皮输出。Tesseract 可以用image_to_data拿到每个字符的置信度def recognize_with_confidence(roi): gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, None, fx2, fy2, interpolationcv2.INTER_CUBIC) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) config --psm 7 -c tessedit_char_whitelist0123456789 data pytesseract.image_to_data(binary, configconfig, output_typepytesseract.Output.DICT) text confs [] for t, c in zip(data[text], data[conf]): if t.strip(): text t confs.append(float(c)) avg_conf sum(confs) / len(confs) if confs else 0 return text, avg_conf置信度阈值我一般设在 75。低于 75 的结果标黄导出时单独列一个「待复核」sheet。这个机制上线后人工复核量从 100% 降到 8% 左右而且复核的人只需要看标黄的心理负担小很多。除了置信度还要加业务规则校验。分数有明确范围单题分数不会超过该题满分总分不会超过卷面总分。识别出「950」而卷面满分是 100直接判定为异常。这些规则用几行代码就能挡住大部分离谱错误。def validate_score(text, max_score): try: val float(text) except ValueError: return None, parse_error if val 0 or val max_score: return None, out_of_range return val, ok导出环节用 pandas 写 Excel把原图路径、识别分数、置信度、校验状态一起写进去。复核的人点开 Excel 就能看到对应的小图改完直接覆盖。这套流程跑顺之后一个班的分数录入从半小时压到三分钟以内。最后说个我自己的习惯每次部署到新学校我一定先拿 50 张真实卷子跑一遍全流程把失败案例全部截图存下来分类。是光照问题、版面问题还是手写太潦草分类完再决定调参数还是加规则。这个「先收集失败再优化」的顺序比一上来就调模型管用得多。希望帮到你。本文还有配套的精品资源点击获取