——小菜“大胖老师咱们为什么不自己用Tesseract加OpenCV搞一套OCR开源免费还显得咱技术牛。”——大胖老师“你这想法跟我2021年一模一样。那一年我写了三百个if-else最后代码跑起来靠的不是逻辑是缘分。”——小菜“缘分”——大胖老师“对就是那种‘今天能跑通明天不知道为什么就崩了后天重启一下又好了’的缘分。程序员管这叫玄学我管这叫‘Tesseract综合征’。”一、那个雄心勃勃的下午2021年初秋大胖老师刚被那套两万八的国际大厂OCR伤透了心又被云OCR的计费规则气得够呛。他坐在实验室里对着天花板发了一下午呆忽然一拍桌子“小菜咱自己搞Tesseract是开源OCR的祖师爷OpenCV是图像处理的神器两个加一起还搞不定几页竖排繁体”小菜当时正是对技术充满信仰的年纪一听这话热血沸腾当晚就去GitHub把Tesseract的源码克隆下来又装上了OpenCV。师徒俩泡上枸杞茶开始了为期三个月的“自研OCR”苦旅。起初一切都很顺利。Tesseract的安装出奇简单一行apt-get搞定。OpenCV更是老朋友了几行代码就能把图片读进来、灰度化、二值化。大胖老师从网上找了一个号称“中文古籍优化”的Tesseract语言包替换掉默认的中文模型然后丢进一张光绪刻本的扫描件。几分钟后结果出来了。识别准确率大约45%。近一半的字是错的剩下一半里还有不少顺序混乱。但大胖老师很兴奋“你看至少认出来了45%说明路子对剩下55%咱们优化优化不就上去了吗”当时的他还不知道这“优化优化”的过程将是一场漫长而痛苦的深渊。二、第一道坎竖排的“方向性迷失”Tesseract对竖排中文的支持几乎为零。这是他们遇到的第一个大坑。Tesseract的设计假设非常朴素文字是横排的。它的内部检测引擎会尝试寻找文本行默认是从左到右的水平行。你把一页竖排古籍丢进去它会把每一列当成一个“行”但读取顺序是水平从左到右——于是右起第一列的内容被当成了第一行第二列是第二行……输出的文字阅读顺序全乱。大胖老师想了个“聪明”的办法用OpenCV先把整张图片旋转90度把竖排变横排喂给Tesseract识别完再旋转回来。听起来完美实际上呢古籍的版面往往混合方向正文竖排天头可能有横排眉批地脚可能有竖排页码中间还可能有横排的印章。全局旋转90度竖排正了横排却歪了。结果是按下葫芦浮起瓢。小菜又提出能不能先检测每一块文字的方向分别旋转于是他们开始了痛苦的“版面分割”之路。这是第二个坑也是最深的一个。三、第二道坎if-else地狱的入口古籍的版面分割说难听点就是一场规则编写噩梦。大胖老师采用的策略是用OpenCV的投影法把图片二值化后统计每一列像素的密度高密度的位置是文字低密度的是栏间距。通过寻找密度谷底可以把页面切成若干列。然后再对每一列做水平投影切出行。最后根据位置关系判断是正文、夹注还是眉批。“这逻辑很清楚嘛”当时的大胖老师信心满满。他写了大概一百行代码拿第一张测试图跑了一下效果不错正文列切得挺准注文也单独识别了。他得意地把结果给小菜看“看见没开源的力量”然后他换了第二张测试图。这是一页明万历刻本栏线模糊而且正文和注文之间的间距非常小投影法根本分不开。切出来的列有的把正文和注文粘在一起有的把一个字从中间劈成两半。大胖老师眉头一皱加了一条规则如果列宽度大于某个阈值说明里面可能夹了注文需要再做一次内部切分。他加了一个if-else。第三张测试图是清乾隆朱墨套印本红黑双色。二值化之后红色的批注和黑色的正文混在一起投影图糊成一团。大胖老师又加了一条先用颜色分离把红色通道单独提取出来分别处理。又是一个if-else。第四张测试图有印章印章被检测成了一大坨“文字块”投影密度极高。大胖老师加了一条如果某个块的像素密度异常高接近100%判定为印章跳过识别。又一个if。第五张测试图是民国石印本字极小行间距极窄投影法的谷底几乎找不到。大胖老师开始调阈值把“谷底”的判定条件从绝对像素数改成相对比例。几个参数一调前面几张图的切割效果又乱了。他开始用配置文件管理参数不同版式用不同预设。if-else开始嵌套像俄罗斯套娃一样一层套一层。小菜每周帮大胖老师统计代码量。第一个月核心逻辑三百行if-else有四十几个。第二个月代码膨胀到一千二百行if-else突破一百个。第三个月代码超过两千行其中if-else三百多个配置参数五六十项。代码文件名叫ancient_ocr.py但小菜私底下给它起了个外号Grand_If_Else_Master_3000。四、代码成了玄学那些无法解释的时刻随着if-else越来越多一个诡异的现象出现了代码开始表现出“非确定性行为”。同样的测试图片同样的参数今天跑的效果和昨天不一样。大胖老师一度怀疑是硬件问题换了内存条重装了系统问题依旧。后来他发现差异来自于一些几乎无法察觉的变量光线不同导致的扫描件色温偏移、纸张纹理的细微变化、甚至CPU浮点运算的舍入误差在某些极端情况下影响了像素分类结果。最经典的一次他写了一个函数用来检测栏线位置。函数在周三下午跑得完美周四早上却全线崩溃。他花了一整天debug最后发现是周三那天测试用的图片是从PDF里导出的PNG周四换了一批直接从扫描仪出的TIFF。TIFF的压缩算法导致边缘像素的灰度值差了1。就1。人的肉眼完全看不出区别但投影算法对边缘像素极其敏感谷底位置偏移了几个像素整个切割就错位了。他修改阈值TIFF好了PNG又不行了。最后他加了一条根据图像格式和文件来源自动选择阈值参数。又是两个if-else。小菜有一次半夜来实验室发现大胖老师正对着屏幕上的if-else代码念咒“如果列宽大于12且小于15且上一行是注文且下一页的页码是奇数那么……”小菜悄悄拍了张照片发到项目群里配文“大胖老师正在给代码开光。”大胖老师看到后没生气反而苦笑着说“你说对了。这代码现在跑起来靠的不是逻辑严密是运气。每次成功我都不知道下一次还能不能成功。这不就是玄学吗”五、压死骆驼的最后一根稻草换本书全塌真正让大胖老师放弃的是一个周末的测试。经过了三个月的打磨他的“Grand_If_Else_Master_3000”已经在十几页古籍上取得了不错的效果。他甚至整理了一份《古籍OCR预处理参数配置指南》准备在系里开个小型分享会。分享会前一天他想再测试一本新书。那是一套民国时期的《吴郡志》石印本版式跟他之前调优的那些清代刻本不太一样栏线用点线而非实线正文行间距更小注文用的是六号小字。他把第一页丢进系统结果出来栏线检测全错注文跟正文混成一锅粥识别准确率暴跌到30%以下。他打开代码试图找到该改哪个参数。但他发现这套版式和他之前写的所有规则都不匹配。如果要适配需要新加一整组if-else分支并且调参可能又会影响之前的书。他看着屏幕上密密麻麻的if-else忽然意识到一件事这不是在写程序这是在给每一本书手工定制一套规则。中国有几千种古籍版本每一种都可能需要专门的参数。这条路没有尽头。他默默关掉了代码编辑器取消了第二天的分享会。小菜问他怎么了他说了句至今被写在实验室墙上的话“如果用if-else就能解决古籍OCR那全国只需要一个程序员。可惜这个程序员已经被我干废了。”六、反思为什么if-else这条路走不通大胖老师后来在项目复盘会上郑重其事地总结了这次失败。“Tesseract的底层架构本质上还是传统的字符分割单字识别。它对版面的理解能力很弱严重依赖前处理。所以我们才会需要那么多OpenCV的预处理规则。但古籍版式的多样性是爆炸式的——不同的时代、不同的刻工、不同的纸张、不同的保存状态组合起来有无穷多的变体。用规则去穷举就像用渔网去捞大海总有漏的。”“更致命的是我们用的规则都是基于‘像素级特征’的——投影密度、连通域面积、边缘梯度。这些特征对环境极其敏感轻微的光照变化、纸张纹理、扫描噪点都会让特征偏移。所以你才会看到‘同样的图今天能跑明天不能跑’这种玄学现象。这不是代码的bug是方法论的失败。规则系统是脆弱的而古籍是强韧的——它经历了数百年风雨当然不怕你这几个if-else。”“真正的解法是让模型自己去学习什么是正文、什么是注文、什么是栏线。不是人写规则去教模型而是模型从大量数据里自己总结规律。这就是深度学习相比传统规则的优势——它不依赖像素级的硬阈值而是学习高层次的特征。PaddleOCR的PPStructure之所以能对付复杂的古籍版面不是因为它内置了一万条if-else而是因为它用了几万张标注过的古籍图像训练了一个版面检测模型。模型学会了‘看版’就像王大姐看了一辈子古籍瞄一眼就知道哪儿是正文、哪儿是注。这不是规则是经验。”七、尾声代码可以玄学但方向不能玄小菜在清理实验室旧文件时翻出了大胖老师当年那个“Grand_If_Else_Master_3000”的源码。他试着在最新的Ubuntu上跑了一下果然如预期般全线崩溃——库版本不兼容、路径硬编码失效、有几个依赖已经停止维护。他把代码归档到一个叫“历史的教训”的文件夹里然后新建了一个文件写下了这样一段话“你问我为什么不继续维护那套if-else代码了不是因为我写不动了是因为我想通了。代码可以是玄学但做事的逻辑不能玄。古籍数字化需要的是能应对万千变化的智能而不是三千个脆弱的规则。有些钱该让别人赚指开源社区有些模型该让机器自己学。人应该去做人该做的事——比如把王大姐教会怎么用新系统。”后来“文渊慧典”上线其中版面分析模块直接调用了PaddleOCR的PPStructure几乎没有一行自定义的if-else规则。有人问大胖老师“你们自己写的那些规则呢怎么不用了”大胖老师指了指窗外的垃圾桶“都在那儿呢和那三个月的青春一起。”小菜笑了给他续了一杯枸杞水。本文为注水技术版您看看即可不必当真写此文字就是图一乐-