从零构建智能试卷解析系统
在教育信息化的圈子里泡久了,你会发现一个很有意思的现象:老师们手里最耗时间的活儿,除了备课上课,就是批改试卷和整理错题。尤其到了期中期末,一摞摞试卷堆在办公桌上,光是把题目录入电脑、按知识点归类、统计错误率,就要占用大量课余时间。我当初想做这套智能试卷解析系统,就是冲着这个痛点去的——把一张纸质试卷拍照上传,系统自动完成版面识别、题目切分、文本提取、知识点归类,最后生成结构化数据和错题统计表。这套东西做完之后,我自己都觉得应该早点动手。
如果你也在做类似的工具,或者正打算从零搭一套试卷解析系统,这篇文章会把我的完整思路、技术选型、踩坑记录和数据结果都摊开来说,你直接照着做或者根据自身场景改,能省不少弯路。
1. 项目定位与整体思路拆解
1.1 核心需求解析:一张试卷图到一个结构化数据集
先把这个项目要解决的问题说清楚。智能试卷解析系统,本质上是把“非结构化的试卷图像”转化为“结构化的题目数据”。这里面的关键路径有三步:图像预处理、版面分析、内容提取与归类。
- 图像预处理:解决照片拍摄带来的倾斜、光照不均、透视变形等问题。
- 版面分析:解决“哪些区域是题目”“题目之间如何分隔”“题目包含哪些元素(题干、选项、图片)”的问题。
- 内容提取:用识别引擎把题目文字提出来,再做答案匹配和知识点分类。
我在设计初期考虑了不同用户的差异化需求:一线教师要的是“能直接用的错题统计和知识点薄弱分布”,教研人员要的是“能批量导出分析的原始文本”,家长要的是“操作简单、拍照即用”。这决定了系统不能做成一个纯算法研究项目,而必须接近一个可落地的工具。所以我从第一版开始,就往模块化和可扩展方向设计,每个环节都可以单独替换、单独调参,方便后续维护和升级。
1.2 技术方案选型:开源工具的组合博弈
技术选型是我踩过最多坑的地方,先给大家看最终的选型表,再讲背后的选择逻辑。
| 功能模块 | 选型方案 | 备选方案 | 选择理由 |
|---|---|---|---|
| 开发语言 | Python 3.9+ | Java / C++ | 图像处理和AI生态最成熟,快速迭代能力强 |
| 图像处理 | OpenCV 4.x | PIL / scikit-image | 预处理算子丰富,轮廓查找算法稳定,性能好 |
| 文字识别 | PaddleOCR | Tesseract / EasyOCR | 中文识别精度领先,自带版面分析和方向检测 |
| Web框架 | Flask | FastAPI / Django | 轻量易上手,做轻量服务完全够用 |
| 数据存储 | SQLite + JSON | MySQL / MongoDB | 单机版无状态服务,轻量部署优先 |
| 部署方式 | Docker Compose | 裸机 systemd | 环境隔离,迁移方便 |
这个选型里最关键的决定是放弃纯离线Tesseract、转向PaddleOCR。Tesseract对印刷体英文识别还行,但一到中文、数学符号、带背景噪声的拍照文本就崩。PaddleOCR自带方向分类器、文本检测和文本识别三个串联模型,对拍照场景的鲁棒性高出不少。实测在同一张倾斜拍摄的试卷上,Tesseract的准确率大概只有73%,而PaddleOCR可以到92%以上。这个差距直接决定了解析结果可用不可用。
OpenCV的选择没什么悬念,它是做图像预处理和几何分析的主流方案,轮廓查找、透视变换、腐蚀膨胀这些操作用起来非常顺手。Flask则是考虑到整体项目结构简单,不需要重型框架,快速搭一个上传接口、跑通全流程更重要。
1.3 系统架构:一条流水线的自我修养
这个系统的整体架构可以用一句话概括:单向流水线,各模块解耦,中间产物可视。完整流程如下:
- 前端(或命令行)上传试卷图片 → 服务端接收并保存原图。
- 图像预处理模块:灰度化 → 去噪 → 二值化 → 倾斜校正 → 透视修正。
- 版面分析模块:轮廓检测 → 题目区域切分 → 子区域过滤与排序。
- 文字识别模块:逐区域送入OCR引擎 → 输出文本和置信度。
- 语义解析模块:根据正则规则和位置特征,提取题干、选项、答案空位。
- 结构化输出:生成JSON数据、Excel表格、错题知识点统计。
每个模块之间通过标准数据格式对接。比如预处理模块输出的是一张干净的图像,版面分析模块输出的是一组带坐标的题目矩形框,OCR模块输出的是一段带坐标的文本行。模块之间的数据格式我统一使用以“区域+坐标+文本”为中心的字典结构,这样任何一个模块出错,都能靠查看中间输出快速定位。
流水线架构有个明显的好处:你可以在任何一个环节插入可视化的调试输出。我专门写了一个调试工具,把每个环节的图像结果拼在一起输出,排错的时候一眼就能看到是预处理失败了,还是OCR识别烂了,不用从头到尾反复试。
2. 核心模块设计与实现要点
2.1 图像预处理:让基本功先扎实起来
图像预处理是整个系统里最不需要动脑筋、但也最容易出问题的模块。因为它直接决定后续版面分析和识别精度的上限。我的处理流程分了四步,每一步都有对应的场景考量。
第一步是灰度化和去噪。拍试卷的照片多半是在教室灯光下拍的,有阴影、有屏幕反光,甚至还有手指遮挡。直接用彩色图做二值化很容易把阴影区域也判定成文字区域。我先把BGR图转灰度,然后使用高斯模糊去除颗粒噪声。核大小选5x5,再大容易模糊文字笔画边缘,再小去噪效果不明显。这一步做完,后续二值化的质量会明显提升。
第二步是自适应二值化。这里我用的是OpenCV的adaptiveThreshold,而不是简单的全局阈值。因为试卷照片不同区域的光照往往不均匀,全局阈值在一个区域合适的值,换到另一个区域就会翻车。自适应阈值会按邻域动态计算阈值,对光照渐变有非常好的抵抗效果。blockSize一般设置31或51,C值设置5左右。这个参数不是死的,如果发现粗体标题被识别成黑块,可以适当加大C值。
第三步是倾斜校正。拍照时试卷很少是完全端正的,而OCR对这种旋转非常敏感。倾斜不超过5度的时候,很多OCR模型还能扛一下,一旦超过这个角度,识别率就断崖式下跌。我使用最小外接矩形来检测整体倾斜角度:先找到试卷纸张的边缘轮廓,再用minAreaRect计算出旋转角度,最后做仿射变换把图像转正。
第四步是透视修正。这一步主要应对从侧面拍摄导致的梯形变形。标准的做法是检测A4纸的四个顶点,然后做透视变换,把整个纸张映射成一个标准矩形。这里我写了两个版本的检测:一种是检测A4纸边缘,另一种是通过霍夫变换找直线交点。实际使用下来,纸张边缘清晰时第一种好使,边缘被手挡住时第二种更稳。
做完预处理后,图像的长边我会统一缩放到2000像素以内,避免后续模板匹配和OCR耗时太长。这个尺寸在这个项目中几乎不损失文字清晰度,但速度能提升接近一倍。
2.2 版面分析:怎么把一张试卷切成一道题
版面分析是这套系统里最核心的难点。很多人以为OCR识别完就有答案了,实际上是OCR只负责提取“文本内容”,如果你不知道一段文本在什么位置、它属于哪道题,那后续题目的切分、归类、统计都无从谈起。
我的切分思路是“自底向上”:先找到所有文本行和图形块的最小外接矩形,然后根据它们在垂直方向上的距离合并。具体操作分五步:
- 对预处理后的图像做膨胀操作,让相邻文字行连成块。这一步的核大小很讲究,我用15x7的矩形核做一次膨胀,让同一行文字连起来,但又不会让行与行间距过小的两行粘在一起。
- 通过OpenCV的findContours找到所有外轮廓,过滤掉面积过小的噪声区域。
- 按轮廓的y坐标排序,计算相邻轮廓之间的垂直间距,把间距小于特定阈值(比如25像素)的轮廓合并成一个大的区域。
- 对每个合并后的大区域做水平投影,看它内部是否还能切出独立的子块,比如一道大题中的分小题往往只有一行题号间距间隔。
- 最后用规则的先验知识校正:试卷上通常有题号“1、2、3”或“一、二、三”,可以识别这类特征点,把区域边界切齐到题号位置。
这道工序的难点在于合并阈值的设定。阈值太小,一道题被切得七零八落;阈值太大,多道题合并成了一个大块。我的做法是用图表统计所有相邻轮廓间距的分布,会发现明显的“双峰”——一个峰对应同一题目内部的微小间距,另一个峰对应题目之间的明显间距,取两峰之间的谷值作为阈值。这个方法虽然听起来复杂,但实际写不到50行代码,效果非常稳定。
版面分析做完,每道题的区域就出来了,我会把每道题裁剪成独立的小图,再按编排顺序重新编号。整个过程的可视化非常重要,建议你一定把切分好的框画在原图上,保存一张调试图,亲眼看看切分边界是否合理。
2.3 文本识别与语义解析:从字符到题干的跃迁
切完题目之后,就要对每道题做OCR识别了。PaddleOCR的用法很标准,加载模型、预测、输出文本行坐标和置信度,不需要调太多参数。我唯一想提醒的是:一定要自己遍历模型返回的文本行坐标,而不是直接拼接整个文本内容。因为模型返回结果的顺序不一定符合阅读顺序,你需要按照文本块的坐标位置重新排列,才能得到正确的题目顺序。我的排序策略是:先按行中心的y坐标排序,y接近的再按x坐标排序。
有了OCR文本和行为坐标之后,就到了“语义解析”。这部分我设计了三层提取逻辑:
- 题干提取:根据“1.”、“2.”、“(1)”等编号开头,识别题干起始位置。对选择题,题干一般到“A.”之前结束;对填空题,题干一般到“____”或括号之前。
- 选项提取:用正则匹配“A.”、“B.”、“C.”、“D.”开头的文本行,这些行可能有换行,需要把连续行合并成一个选项。
- 答案位置识别:对填空题,识别“____”或者连续下划线,标记为待填答案区域。对判断题,识别“( )”标记。
这里我踩过一个大坑:OCR偶尔会把“B.C.D.”识别成“B.C.D.”中间没有空格,正则匹配根本找不到。后来我改成了更宽松的规则,允许中间出现0个以上空格,并做了字符级别的修正,才稳定下来。
除了文本层面的解析,我还让系统提取了题目中的图片和公式区域。方法是找非文本区域的图像轮廓,这些轮廓如果属于一个题目框内且面积足够大,就认为是题图或公式区,单独保存。这个功能虽然在第一版用得比较少,但一旦要在错题本里展示原题原貌时,作用就出来了。
3. 实操过程与核心环节实现
3.1 环境搭建与依赖安装
老规矩,先上环境清单。我的开发机器是普通配置(8核CPU、16G内存、无独立GPU),在纯CPU环境下也完全能跑通。如果你有GPU,速度会快很多,但不必强求。
用conda创建虚拟环境,Python版本指定3.9,安装核心依赖:
conda create -n exam_parser python=3.9 -y conda activate exam_parser pip install opencv-python opencv-contrib-python pip install paddlepaddle paddleocr pip install flask flask-cors pip install pandas openpyxl pip install numpy pillowPaddleOCR的安装需要额外注意版本匹配。paddlepaddle和paddleocr之间有版本对应关系,我装的是paddlepaddle 2.4.x + paddleocr 2.6.x,这套组合在CPU环境下表现稳定。如果你装最新的paddleocr 3.x,很多API变了,网上现成教程大部分不匹配,劝你先装2.6。装完可以先跑一个小验证,看能不能识别一张简单的中文图片,确认环境没问题再进入下一步。
3.2 预处理与版面分析核心代码实现
这部分直接上代码,连同注释一起贴出来。
import cv2 import numpy as np def preprocess_image(image_path): # 读取原始图像 img = cv2.imread(image_path) img = resize_long_edge(img, 2000) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 用高斯模糊去噪 blur = cv2.GaussianBlur(gray, (5, 5), 0) # 自适应二值化,blockSize=51,C=5 binary = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 51, 5) # 倾斜校正 corrected, angle = correct_skew(binary, img) return corrected, binary, angle def correct_skew(binary, original): # 查找所有文字区域的轮廓 contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 合并所有轮廓形成一个整体,用于估计整体倾斜角 all_points = np.vstack(contours) rect = cv2.minAreaRect(all_points) angle = rect[-1] # 修正角度范围 if angle < -45: angle = -(90 + angle) else: angle = -angle # 获取旋转矩阵 h, w = original.shape[:2] matrix = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) corrected = cv2.warpAffine(original, matrix, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) return corrected, angle def split_question_regions(binary_image, min_area=800): # 膨胀,让同一行文字连成块 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 7)) dilated = cv2.dilate(binary_image, kernel, iterations=1) contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) regions = [] for cnt in contours: area = cv2.contourArea(cnt) if area < min_area: continue x, y, w, h = cv2.boundingRect(cnt) regions.append((x, y, w, h)) # 按垂直方向排序,然后合并垂直距离过近的区域 regions.sort(key=lambda r: r[1]) merged = [] for region in regions: if merged and abs(region[1] - merged[-1][1]) < 25: px, py, pw, ph = merged[-1] x1 = min(px, region[0]) y1 = min(py, region[1]) x2 = max(px + pw, region[0] + region[2]) y2 = max(py + ph, region[1] + region[3]) merged[-1] = (x1, y1, x2 - x1, y2 - y1) else: merged.append(region) return merged这段代码的核心逻辑说穿了并不复杂,核心就是两步走:膨胀连字、轮廓找块。但这两步的参数对结果影响非常大,尤其是膨胀核的尺寸。如果试卷文字字号大,核可以大一点;字号小,核要小一点。我在调试的时候经常来回改这个参数,后来做成一个可配置的配置文件,跑不同试卷前先用一张样例图试几个参数组合,找到舒服的值再批量跑。
透视变换的代码由于篇幅原因只展示思路:检测A4纸四角,用getPerspectiveTransform做变换。如果你需要处理“斜着拍”的照片,这一步绝对不能省。
3.3 OCR识别与题目结构化输出
OCR这块直接调用PaddleOCR,我封装了一个函数,方便后面所有场景复用。
from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False) def recognize_text(region_img): result = ocr.ocr(region_img, cls=True) text_lines = [] if not result or not result[0]: return text_lines for line in result[0]: box = line[0] text = line[1][0] conf = line[1][1] # box是四个顶点坐标,计算中心点用于排序 xs = [p[0] for p in box] ys = [p[1] for p in box] cx = sum(xs) / 4 cy = sum(ys) / 4 text_lines.append({ "text": text, "conf": conf, "cx": cx, "cy": cy, "box": box }) # 按阅读顺序排序:先按行(cy相近视为同一行),再按列 text_lines.sort(key=lambda t: (round(t["cy"] / 30), t["cx"])) return text_lines def parse_question(text_lines): question = {"type": None, "stem": "", "options": {}, "answer_blank": None, "raw_texts": text_lines} for item in text_lines: text = item["text"].strip() # 匹配选项 opt_match = re.match(r"^([A-D])[.、]?\s*(.+)", text) if opt_match: option_key = opt_match.group(1) option_value = opt_match.group(2) question["options"][option_key] = option_value if question["type"] is None: question["type"] = "choice" continue # 匹配题号 num_match = re.match(r"^(\d+)[.、]\s*(.+)", text) if num_match: question["stem"] = num_match.group(2) continue # 如果没有题号且题干为空,把当前行当作题干的一部分 if not question["stem"]: question["stem"] = text else: question["stem"] += text # 识别选项数量:少于3个选项时,可能不是选择题 if len(question["options"]) >= 3: question["type"] = "choice" elif "____" in question["stem"] or "( )" in question["stem"]: question["type"] = "fill_blank" else: question["type"] = "subjective" return question在写这段代码时有一个心得要分享:OCR返回的置信度一定要用。有时候一句文本被识别得乱七八糟,置信度只有0.5,那基本可以判定是错识别或者背景噪声。我会把它单独标成“低置信度文本”,后续人工抽查时优先看这些位置。这个过滤策略节省了我大量的人工校对时间。
3.4 批量处理与结果导出
单个试卷的完整跑通只是一个开始,实际项目里往往要批量处理几十上百张试卷。我写了一个批量调度脚本,用多进程并行处理。Python的multiprocessing在图像处理场景下表现很好,我开了4个worker进程,单张试卷平均耗时从25秒降到了8秒左右。因为有GIL的存在,直接用threading做计算密集任务效果很差,这里一定要用多进程。
处理完的结果统一存入一个JSON文件,并且用pandas生成Excel表格:
def export_to_excel(all_results, output_path="result.xlsx"): rows = [] for exam_name, questions in all_results.items(): for q in questions: rows.append({ "试卷": exam_name, "题号": q.get("number"), "题型": q.get("type"), "题干": q.get("stem"), "选项A": q.get("options", {}).get("A"), "选项B": q.get("options", {}).get("B"), "选项C": q.get("options", {}).get("C"), "选项D": q.get("options", {}).get("D"), "OCR置信度": round(q.get("avg_conf", 0), 4) }) df = pd.DataFrame(rows) df.to_excel(output_path, index=False)生成的Excel就是老师们最喜欢的错题汇总表。再配合知识点的映射规则,我还可以统计出每道题对应到哪个章节、哪个知识点,生成“知识点薄弱雷达图”所需的原始数据。这个能力放到教学评估里,价值远大于单纯的文字识别。
4. 常见问题与排查技巧实录
项目做出来之后,我在不同场景下跑了上百张试卷照片,积累了非常多的问题。这些问题如果只看教程是遇不到的,大概率要自己踩一遍坑。我把它们整理成一份速查表,并在后面详细展开几个高发问题。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 倾斜校正后图片边缘出现黑边 | 旋转后画布未裁剪 | 校正后裁剪掉边缘黑边区域 |
| 两行文字粘在一起,切块不准 | 膨胀核过大 | 减小膨胀核或减少膨胀迭代次数 |
| 选择题选项识别出3个,少了1个 | OCR漏识别或选项换行断行 | 增加选项字符级修正逻辑,合并相邻行 |
| OCR识别速度极慢 | 图像尺寸过大 | 将长边缩放到2000像素以内 |
| 高亮笔涂过的地方识别出乱码 | 高亮颜色干扰二值化 | 增加颜色过滤,去除高饱和背景 |
| 答案区域的“____”被识别成乱字符 | 下划线在二值化后断裂 | 在下划线区域做水平方向闭运算 |
4.1 倾斜校正后的黑边陷阱
这是我最常遇到的问题。用仿射变换做旋转校正之后,图像四个角常常出现三角形的黑边/白边区域。这些区域会污染后续的版面分析,甚至被当成题目区域。解决方法是:在旋转矩阵里设置borderMode为BORDER_REPLICATE,用边缘像素填充,这样至少不会出现纯黑色区域;更有效的做法是旋转后计算有效区域并做中心裁剪。
实际项目中我建议两步都做:先用BORDER_REPLICATE避免纯黑边,再按检测到的纸张轮廓做一次裁剪。这样既能修正倾斜,又能去掉多余背景。
4.2 高亮笔涂改导致的识别灾难
学生试卷上经常有荧光笔涂过的痕迹,红色、绿色、黄色都有。这类高饱和背景对OCR的干扰极大,会让文字区域变成一块纯色块。我的解决方案是在预处理阶段加一个“颜色过滤”步骤:先将图像转到HSV色彩空间,然后构建一个颜色掩膜,把高饱和度的像素替换成白色背景,或者直接置为背景色。
这个操作在OpenCV里不到20行代码,但效果拔群。要注意的是,这样过滤可能会误伤题目里红色的批改痕迹。所以我的参数是只过滤饱和度极高且面积较大的色块,而不是所有带颜色的像素。
4.3 公式识别的边界能力
试卷里经常有数学公式,比如分数、根号、上下标。PaddleOCR的通用模型对简单的线性公式能识别,但对复杂的二维结构(比如真正的分式、根号内嵌套分式)就会识别得很烂。我在这个项目里做的是“保守策略”:检测到公式区域时,不强行做公式语义识别,而是把该区域保存成一张公式图片,放在结构化数据的“公式图片”字段里。虽然还是一个图像,但至少不会变成一串乱码反而误导使用者。
如果未来要扩展,可以单独接入公式识别模型,但要在“识别准确率”和“项目迭代成本”之间好好权衡。当前保守策略最大的好处就是不会提供一个错误答案。
4.4 多栏排版试卷的处理难题
还有一种很常见的情况是试卷是两栏排版,尤其是英语试卷。我的最初版面分析算法是按照从上到下、从左到右的顺序合并区域的,遇到双栏会乱套:把右栏第一题的文本接到左栏第二题的后面。
这个问题我用了“分栏检测”来解决:先做垂直投影,统计每列像素的分布,找到两栏之间的空白通道,然后分别在左栏和右栏里独立执行版面分析。这一步逻辑不复杂,关键在于先检测分栏,再解析题目。有了分栏检测,双栏试卷的题目顺序就恢复成了自然阅读顺序。
4.5 低置信度文本的筛查
PaddleOCR会返回每行文本的置信度,这个数值在0到1之间。我的经验阈值是0.75,低于这个值时有很大概率是误识别。把所有低置信度文本导出一份清单,做人工复核时就看这个清单,效率比从头到尾检查高得多。实际上,我后来把低置信度文本按题目区域汇总,只展示对应题目图片的人工校对界面,整个流程从技术视角转到产品视角,体验完全不一样。
5. 扩展方向与实战体会
5.1 从解析到教学:知识图谱的接入
当系统能稳定输出“试卷名称 + 题号 + 题干 + 选项 + 答案位置 + 文本置信度”之后,进一步的价值是接教学知识图谱。我能拿到每道题的文字内容后,用关键词匹配或简单的句向量模型,把题目映射到预设的知识点库中,比如“一元二次方程判别式”“三角函数图像变换”等等。有了题目和知识点的对应关系,就能自动生成每个学生的薄弱点分析。这个扩展真正把“智能试卷解析”从工具属性升级成了教学辅助属性,前景更广。
5.2 轻量化部署:把系统塞进小程序
我最初把系统部署在服务器上,通过Web API供外部访问。后来发现很多老师的使用场景是在手机上拍照、马上解析,所以我把Flask服务部署成轻量API,具体路径是:手机拍照上传 → 服务器预处理与识别 → 返回结构化结果 → 前端展示错题列表。再往后可以配合小程序做一个更完整的体验:拍一张照,几秒后就能看到所有题的答案和解析结果。这个方案对服务器带宽和算力的要求不高,单机就能抗住班级级别的并发量。
5.3 关于数据质量与标注的一些经验
最后说一说数据。整个项目最耗时、最磨人的部分不是算法,而是数据整理和验收。我前前后后人工标注了两百多张试卷图片的题目区域,整理出三千多道题。中途一度因为标注格式问题导致训练数据不统一,被迫返工,那种挫败感我到现在还记得。如果你是刚开始做,建议从一开始就定义一个严格的数据格式模板,每个字段都写清楚含义和示例,后面能省非常多的时间。
另外,测试时一定要覆盖多种拍摄条件:正对拍摄、斜拍、有手指遮挡、有阴影、教室灯光偏暖、题目旁边有学生乱涂乱画……这些真实场景比标准测试集更能暴露问题。我自己的测试集里,至少有20%的图片是有瑕疵的,跑完这批图片还能保持90%以上的可用率,系统才算真正达标。
5.4 最后的几点实在建议
- 先跑通最小闭环,再追求精度。哪怕第一版只有“上传图片 → 切分 → OCR → 拼结果”这四步,也要让它端到端跑起来,不要一开始就沉迷于调参。
- 在代码里多留中间产物输出。每次处理都保存预处理图、切分图、OCR可视化图,这些调试图是定位问题的最佳线索。
- 性能优化留到功能稳定之后再做。早期如果单个任务跑10秒,就直接等待,不要浪费时间优化。等系统能出结果了,再考虑并行、缓存、模型量化。
- 注意系统设计时留好人工复核接口。让机器把好做的题和不好做的题分开,人类只在低置信度区域做确认,人机协作的组合效率远超纯自动化。
这个项目做到现在,我的感受是:智能试卷解析真正难的不是OCR代码本身,而是如何把脏乱差的现实世界图像变成干净可靠的训练数据和处理流程。你处理过一百张真实拍出来的试卷之后,才会对“文本检测、版面分析、置信度校准”这些词有切身感觉。希望我这篇总结能帮你少踩一些坑,也欢迎你做完后回来说说你的处理方案。