《大话文渊慧典》:外二篇-Tesseract+OpenCV自制OCR,我写了三百个if-else,最后代码成了玄学

📅 2026/7/29 3:24:01 👁️ 阅读次数 📝 编程学习
《大话文渊慧典》:外二篇-Tesseract+OpenCV自制OCR,我写了三百个if-else,最后代码成了玄学

——小菜:“大胖老师,咱们为什么不自己用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规则。有人问大胖老师:“你们自己写的那些规则呢?怎么不用了?”大胖老师指了指窗外的垃圾桶:“都在那儿呢,和那三个月的青春一起。”

小菜笑了,给他续了一杯枸杞水。

本文为注水技术版,您看看即可,不必当真,写此文字就是图一乐:-)