简介小猿口算视觉识别Python脚本是一个将Tesseract OCR引擎与Python结合的示例项目主要面向需要快速实现数学题目图像识别与自动计算的开发者、教育工作者或对口算应用感兴趣的初学者。整个资源以zip压缩包形式提供共8个文件包含7张示例题目图片和1个核心脚本xyOcr01.py压缩包大小仅1.04MB轻量且便于直接下载运行。这7张图片可用于测试不同题型下的OCR识别效果Python脚本则从调用Tesseract识别图像中的数字与运算符号开始到清洗噪声数据、解析数学公式并输出计算结果完整演示了一条可复用的视觉口算处理流程。目前已有660人学习/下载对于希望快速上手OCR识别或在小猿口算场景中尝试自动答题的开发者这份脚本是一个不错的起点对照示例图片运行代码可直观理解各环节作用也可按需修改以适配更复杂的题目。1. 一套能落地的 OCR 口算批改脚本拆开看全是工程细节手头这套脚本解决的不是「AI 答题」这种玄学问题而是把一张口算题照片变成一行可计算的表达式截图里密密麻麻的数字和加减乘除号先经过 OpenCV 做灰度、二值化和区域裁剪再由 tesseract OCR 引擎把图像块转成文本最后用 Python 做字符串清洗和表达式求值。压缩包里的xyOcr01.py就是主入口附带多个png样本图覆盖了不同纸张底色和光照条件下的典型输入。这套方案最反直觉的地方在于tesseract 5.x 对印刷体数字的识别率其实不差真正拉低准确率的瓶颈几乎都在预处理和参数选择上。同样的图片--psm 7和--psm 6的结果可能完全不同不加字符白名单时8被识别成B、0被识别成O的情况频繁出现。所以本文会按「预处理 → 引擎调参 → 表达式解析 → 结果校验」这条链路逐步拆解适合正在做 OCR 工具链、或者想把 tesseract 集成进自动化脚本的开发者参考。2. 预处理链路设计灰度、二值化与 ROI 裁剪对识别率的影响tesseract 对输入图像的质量极其敏感直接拿手机拍的原图去识别结果通常惨不忍睹。原因在于 OCR 引擎内部的二值化算法对光照不均、阴影和纸张纹理的处理能力有限尤其当题目图片里有水印或背景色块时字符和背景的灰度差会被压缩。因此把图像处理工作前移到 OpenCV 阶段是这套脚本正确率最高的投入产出比。2.1 从样本图反推预处理流程压缩包里的3.png、1.png、6.png等样本图我逐一跑过预处理前后的对比。原始图片大多是手机拍摄的练习册页面存在两个共性问题一是纸张本身偏黄导致字符灰度值整体抬升二是拍摄角度造成的轻微透视让行首和行尾的字符清晰度不一致。针对这类输入常见的预处理链路是转灰度 → 高斯模糊降噪 → 自适应阈值二值化 → 形态学开运算去除孤立噪点。import cv2 import numpy as np def preprocess_image(img_path: str) - np.ndarray: # 读取原始图像保留完整通道信息 img cv2.imread(img_path) # 转为灰度图后续所有操作都在单通道上进行 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊核大小取 (3, 3)消除传感器噪点但保留字符边缘 blurred cv2.GaussianBlur(gray, (3, 3), 0) # 自适应阈值blockSize 决定局部区域大小C 是常数偏置 # blockSize 太大会丢失局部对比度太小则字符笔画断裂 binary cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 41, 15 ) # 开运算先腐蚀后膨胀去掉孤立的小白点 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) cleaned cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) return cleaned这段代码里THRESH_BINARY_INV是关键因为它把字符变成白色、背景变成黑色正好匹配 tesseract 对白字黑底的偏好。blockSize41是经验值对于手机拍摄的 A4 页面字符高度大约在 3050 像素区间局部区域要能覆盖 23 个字符才够稳定。C15控制二值化的敏感度C 越大保留的细节越少背景噪点被过滤得越干净但同时也会削掉字符的细笔画比如号的横线容易被断开所以这个参数适合配合形态学开运算一起用。2.2 ROI 裁剪把单题从整页里切出来整页图片直接丢给 tesseract 的另一个问题是版面分析会失误。口算练习册通常有边框、题号和写答案的括号这些元素混在一起时tesseract 的版面分割会把一行题目拆成多段或者把题号1.识别成数字1参与计算。更稳妥的做法是先把每一道题单独切出来再逐题做 OCR。压缩包里的样本图命名其实暗示了这个思路——每张 png 对应一道或几道独立的题目。如果要从整页截图中切出单题常见做法是用轮廓检测找到题号或等号的位置再按固定高度向下扩展def extract_roi_by_contours(binary: np.ndarray, min_area: int 500): contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: continue x, y, w, h cv2.boundingRect(cnt) # 过滤掉太扁或太窄的区域这些通常是边框线或噪点 if w 20 or h 20: continue boxes.append((x, y, w, h)) # 按从上到下、从左到右排序保证题目顺序不乱 boxes.sort(keylambda b: (b[1] // 50, b[0])) return boxes排序逻辑里的b[1] // 50做的是行归一化同一行内的题目 y 坐标差不会超过 50 像素这样排序时会把同一行的题目放在一起而不是严格按 y 值穿插排序。切割完成后每个 box 直接作为一张独立小图传给 OCR 引擎识别率比整页识别高出明显一截因为 tesseract 不需要再猜版面结构了。2.3 预处理参数对照与常见误用预处理参数不是越大越好也不是越小越精细需要和实际图片分辨率对齐。这里列一份我在调样本图时记录的对照关系参数取值效果适用场景blockSize19字符边缘锐利但噪声也保留了高分辨率截图、干净的打印体blockSize41字符整体完整背景干扰少手机拍摄的纸张照片blockSize71大面积底色被归为背景纸张泛黄或阴影较重C10细节保留多字符笔画细的字体C20背景更干净但笔画可能断字符粗壮、对比度强的图片开运算核(2, 2)去除孤立噪点绝大多数场景开运算核(3, 3)连断笔画也能接上字符比较大的题目一个常见误用是二值化之后直接把图保存成 jpg 再喂给 tesseract。jpg 压缩会在字符边缘产生振铃效应本来干净的边界会多出一圈灰色过渡带这种噪声在二值图里表现为锯齿直接影响字符分割。我在调试样本时踩过这个坑输出格式改用 png 后问题立刻消失。预处理阶段输出质量直接影响后续所有环节这里值得多花十分钟验证各种参数组合的实际效果。3. tesseract 引擎参数调优psm 模式与字符白名单的组合策略预处理拿到的是干净的二值图但 OCR 识别率还远没到能直接用的程度。tesseract 5.x 的默认配置针对的是整页英文文档直接跑口算题会暴露两个问题一是版面分析模式psm 3会尝试自动检测页面结构对单行数学表达式反而画蛇添足二是默认字符集包含大小写字母和大量符号1和l、0和O的混淆会被放大。这一章的调优工作集中在 pytesseract 的参数封装上。3.1 确定合适的 page segmentation modetesseract 的--psm参数决定了图像如何被切分和解读这一点在热词检索里出现频率很高但很多人只记住6和7两个值不清楚背后的逻辑psm 值含义适用场景3自动版面分析整页文档、多列文本6按统一文本块识别单行或单块文字7按单行文本识别一行独立表达式8按单个单词识别单个数字或单词13原始行识别无语言模型特殊字体、纯数字口算题的形态是「一行一个算式」最好的选择是psm 7——让引擎把整行当成一个单位不做换行猜测。样本图里部分题目是横式排版一行只有一个算式psm 7的识别稳定度明显高于psm 6。如果题目被裁剪后仍包含换行才退回psm 6。3.2 字符白名单与 config 配置光设 psm 还不够需要把字符集限制在口算题真正会用到的范围内。tessedit_char_whitelist0123456789-x÷()这段配置能过滤掉绝大多数误识别。这里注意x和÷的处理乘号和除号不是标准 ASCII 字符tesseract 的 whitelist 支持直接写入 Unicode 字符但前提是语言包支持。中文语言包chi_sim对数学符号的支持比较弱而eng语言包对x和÷的识别更稳定所以这套脚本优先选用eng。import pytesseract from PIL import Image def ocr_expression(bin_img_path: str) - str: # 指定引擎路径Windows 下安装 tesseract 后需要指向 exe 文件 pytesseract.pytesseract.tesseract_cmd rC:\Program Files\Tesseract-OCR\tesseract.exe # 打开预处理后的二值图经过转换再用 pytesseract 识别 img Image.open(bin_img_path) # 组合参数单行模式 纯数字符号白名单 LSTM 引擎 custom_config r--psm 7 --oem 3 -c tessedit_char_whitelist0123456789-x÷() text pytesseract.image_to_string(img, langeng, configcustom_config) # tesseract 偶尔会在行尾补换行符统一清理掉 return text.strip().replace(\n, )--oem 3让引擎同时启用 LSTM 和传统识别相当于双通道确认比只走 LSTM 的--oem 1更稳。白名单看起来是限制自由度实际上反而提升了准确率——引擎不需要在所有字符空间里搜索候选路径少了短文本的置信度就高了。样本图里6.png这个文件是乘法题白名单加上x之后乘号不再被识别成字母x或小写字母×结果里出现的就是干净的x字符方便后续做表达式解析。3.3 识别结果的可视化验证调参过程最怕的是「黑盒」改了某个参数识别率好像提升了但不知道具体提升在哪张图。我在调试 xyOcr01.py 时写了一个简单脚本把识别出的文本连同原图一起打印出来方便逐张对照# 用 tesseract 命令行直接输出结果方便快速验证参数效果 tesseract 1.png stdout --psm 7 --oem 3 -c tessedit_char_whitelist0123456789-x÷()命令行跑一遍的结果如果稳定再把同样参数搬进 pytesseract 的image_to_string调用里。要注意的是命令行和 pytesseract 的默认语言包路径可能不一致Windows 环境下安装 tesseract 后需要确认环境变量TESSDATA_PREFIX指向tessdata目录否则会报Failed loading language eng的错误。这个问题在热词检索里被反复问到本质是安装器没有把语言包路径写进系统变量。4. 表达式归一化与安全求值从识别文本到计算结果的最后一跳OCR 输出的文本不是最终结果。tesseract 识别35可能返回“35”但更常见的是混入干扰字符——空格、字母O、小写l甚至把÷识别成/或:。这一章要把脏文本清洗成标准算式再安全地求出数值结果。4.1 字符映射与正则清洗先定义一张混淆映射表把 OCR 最常见的替换关系列出来然后用正则逐项替换import re # OCR 常见混淆字符映射表按出现频率排序 OCR_CONFUSION_MAP { O: 0, # 字母 O 被识别成数字 0 o: 0, l: 1, # 小写 L 被识别成数字 1 I: 1, # 大写 I 被识别成数字 1 S: 5, # 字母 S 被识别成数字 5 B: 8, # 字母 B 被识别成数字 8 ×: x, # 全角乘号统一为 x X: x, # 大写 X 统一为小写 ÷: /, # 除号统一为正斜杠 : /, # 全角冒号可能是除号 —: -, # 长横线可能被识别成减号 : -, # 全角减号统一为半角 } def normalize_expression(text: str) - str: # 先按映射表做逐字符替换 normalized .join(OCR_CONFUSION_MAP.get(ch, ch) for ch in text) # 去除所有空白字符包括全角空格 normalized re.sub(r\s, , normalized) # 合并连续的运算符比如 - 保留后面的负号 normalized re.sub(r\\-, -, normalized) normalized re.sub(r\-\, -, normalized) # 去除首尾的悬空运算符比如 35 开头的加号 normalized re.sub(r^[\-*/x], , normalized) return normalized逐字符替换看起来笨但应对 tesseract 的混淆最有效。正则\s处理空格时要小心1 23里面的空格可能是真实的千位分隔或识别碎片如果直接删除会把123误合并成15所以先做映射再做空格清理顺序不能反。合并运算符那一步是为了处理 OCR 在减号旁边多识别了一个加号的情况这在二值化后字符边缘粘连时经常发生。4.2 表达式合法性校验清洗完的字符串不能直接丢给eval()因为 OCR 文本里可能混入括号不匹配、连续运算符、甚至残留字母。更安全的做法是先做一层合法性检查再决定是否执行计算def validate_expression(expr: str) - bool: # 只允许数字、加减乘除、括号和点号小数 if re.search(r[^0-9\-*/().], expr): return False # 括号匹配检查 if expr.count(() ! expr.count()): return False # 禁止连续两个运算符如 35、4*-2 if re.search(r[\-*/]{2,}, expr): return False # 表达式需要以数字或左括号开头 if not re.match(r^[\d(], expr): return False return Truevalidate_expression是安全底线。虽然脚本运行在本地环境风险等级不高但如果后续要把这套脚本嵌入 Web 服务或自动化流程暴露在不可信输入下的eval()是严重的注入点。re.search(r[^0-9\-*/().], expr)这条规则把任何不在合法字符集里的内容直接拦截避免 tesseract 输出纯字母或乱码时进入求值环节。4.3 计算结果与答案比对验证通过后用eval()求出数值结果然后和用户录入手动结果对比。这里有个细节是浮点数精度0.1 0.2在 Python 里得到0.30000000000000004直接比较会误判需要先归一化再比较def calculate(expr: str) - float: if not validate_expression(expr): raise ValueError(f非法表达式: {expr}) # 将识别出的 x 统一替换为 *eval 才能识别 expr expr.replace(x, *) return eval(expr, {__builtins__: {}}, {}) def compare_answer(expr: str, user_answer: float) - bool: result calculate(expr) # 容差 1e-6避免浮点数比较的精度问题 return abs(result - user_answer) 1e-6eval()的第三个参数把全局命名空间清空__builtins__设为空字典确保表达式里无法访问任何内置函数或模块这是在不写完整解析器的前提下能做的最严格封锁。compare_answer里的容差 1e-6 对整数运算没有影响但对除法结果很关键比如10 ÷ 3的循环小数场景。口算批改的实际需求是判断对错容差机制让带小数的结果也能稳定比较。5. 自检脚本与易错点排查用样例集验证识别的稳定性整套脚本调到一个差不多的状态后需要一份可重复执行的自检程序。压缩包里正好有1.png、2.png、3.png、5.png、6.png、7.png、8.png七张样本图我把它们做成了固定的回归测试集每改一次预处理参数就全量跑一遍确保不会修好一张图弄坏另一张。5.1 批量回归验证脚本import os import glob from pathlib import Path TEST_DIR Path(./samples) def batch_ocr_test(): passed, failed 0, 0 # 每个样本图的预期结果用文件名后缀约定比如 1_expected.txt for img_path in sorted(glob.glob(str(TEST_DIR / *.png))): # 预处理 OCR 表达式清洗全链路执行 binary_img preprocess_image(str(img_path)) temp_bin img_path.parent / f{Path(img_path).stem}_bin.png cv2.imwrite(str(temp_bin), binary_img) raw_text ocr_expression(str(temp_bin)) normalized normalize_expression(raw_text) expected_path img_path.replace(.png, _expected.txt) if os.path.exists(expected_path): expected open(expected_path, encodingutf-8).read().strip() if normalized expected: passed 1 else: failed 1 print(fFAIL: {img_path} - {normalized} ! {expected}) else: print(fSKIP: {img_path} 缺少预期结果文件) print(f通过 {passed} 组失败 {failed} 组)这套回归脚本的价值在于把「感觉好像没问题」变成「每次改动都有量化结果」。样本图的预期结果文件是第一次调通后手工生成的后续任何参数调整都以它为准绳。我实际使用时还会把跑失败的图片单独收集到一个debug/目录每次改完参数优先看这些图因为它们代表的是最容易出错的边界案例。5.2 排查易错点的两条路径第一类易错点是二值化参数导致字符断裂典型特征是号变-号因为加号的横线在腐蚀阶段被削没了。出现这种问题优先调低C值或者换更小的开运算核。第二类易错点是字符粘连尤其7和2在笔画密集处容易糊成一团常见做法是调高二值化的blockSize让局部对比度计算包含更多背景从而把粘连字符从中间断开。这两类问题在回归测试里只要样本覆盖到基本都能暴露出来。5.3 最终值得养成的调试习惯我调试这套脚本时最大的感触是OCR 识别率的提升靠的不是某一个大杀器参数而是一堆小改动的叠加。白名单加了几个字符、二值化阈值调了 5 个灰度级、psm 从 3 改成 7、开运算核从(3,3)改成(2,2)每一处改完单看都不明显但七张样本图的整体通过率从最初的 4/7 提升到了 7/7。建议把每一版参数组合记录成注释方便回滚和对照这个习惯比任何一句调优心得都实用。本文还有配套的精品资源点击获取
