简介:面向 Qt 桌面应用开发者的 Windows 64 位 Tesseract OCR 编译包,适合需要在 C++/Qt 项目中本地集成文字识别能力、又不愿自行处理第三方库编译与依赖链接的开发者,也可用于离线环境下的 OCR 功能验证。压缩包共 916 个文件,约 39.32MB,包含大量 h 头文件、dll 动态库、lib 导入库,以及 cmake 配置、pc 描述文件、可执行工具、训练相关文件和调试符号等,基本覆盖开发接入、构建配置、运行调试与问题定位所需材料。已有 1126 人学习,说明该编译版本在 Qt OCR 场景中具有一定参考价值。借助其中的头文件、库文件与 CMake/pkg-config 配置,读者可较快完成 Qt 工程的环境接入、链接参数设置与基础识别功能验证,并减少从源码编译 Tesseract 及其依赖时的排错成本,适合有一定 C++/Qt 基础、希望把 OCR 能力嵌入桌面应用的开发者按需取用。
1. 一个压缩包省掉的三天编译
前阵子有人给我看一个 Qt 写的票据识别小工具,在他机器上跑得好好的,拷到客户那边双击就闪退。查了半天不是代码问题,是他自己拼的 Tesseract 库 32 位和 64 位混着用。换成一套 Windows 64 位现成编译版之后,改了两行.pro,十分钟就跑通了。Qt OCR 这件事,门槛从来不在 API——Init、SetImage、GetUTF8Text三个调用谁都会写,真正耗时间的是前面那段编译链:Leptonica、libpng/zlib、训练数据、MSVC 与 MinGW 的 ABI,任何一环对不上,你拿到的就是链接错误或者满屏乱码。这份资源解决的就是最费时间的那一段:把 Tesseract 和它的依赖在 Windows 64 位下预先编好,你只管接进工程、调参、上线。适合两类人:一是在桌面端做离线文字识别的 Qt 开发者,二是被 vcpkg 和 cmake 反复折磨、只想先验证效果的人。
2. 先看清包里有什么:依赖链、ABI 与 64 位文件清单
拿到一个预编译包,最忌讳的就是直接往工程里一塞然后编译。先花十分钟认清它是什么形态的东西,后面能省掉一半的排查时间。
2.1 Tesseract 不是一个人在战斗
Tesseract 本体负责版面分析和 LSTM 推理,读图、二值化、连通域、形态学这些图像操作全部委托给 Leptonica。而 Leptonica 要读 PNG/JPEG/TIFF,又得链上 libpng、libjpeg-turbo、zlib、libtiff 这一串。自己从源码编的时候,十次失败里有八次不是 Tesseract 源码的问题,而是 Leptonica 找不到某个编解码库,或者 zlib 头文件和 lib 版本对不上。
这决定了一个能直接用的 64 位包里,tesseract5.dll和leptonica-*.dll必须是同一套编译参数产出的,缺一个或者版本错位,典型表现就是Init返回 0 看着成功,SetImage之后直接崩,或者读 PNG 返回空 Pix。
提示:如果你的图片本来就用
QImage加载,其实可以完全绕开 Leptonica 的解码路径,把内存像素直接喂给SetImage。这时候那些 png/jpeg DLL 只在链接期需要,运行期不会触发。图片格式交给 Qt 处理,Tesseract 只吃裸像素,这条边界划清楚,部署能少踩不少坑。
2.2 MSVC 与 MinGW:选错一次,链接期全是红字
Windows 上的 Qt 官方包历来是两条线:MSVC 版和 MinGW 版。你链接的库必须和 Qt 同属一个编译器家族,MinGW 的libtesseract.dll.a拿到 MSVC 工程里必然报 LNK2038 或一堆无法解析的外部符号。
更隐蔽的是同一家族内部的版本差异。MSVC 这边,Debug 构建默认_ITERATOR_DEBUG_LEVEL=2,Release 是 0,用一个 Debug 程序去链 Release 库,编译期可能只是警告,运行期直接堆损坏。MinGW 这边,GCC 8.1 和 11.2 编出来的 C++ 库在异常处理和 RTTI 的细节上并不完全兼容,跨版本链接经常是能编过、跑起来崩在某个std::string析构上。运行库同理,/MD和/MT混用会导致堆被两个 CRT 各管一半,释放时直接爆炸。
选型建议很朴素:确认你 Qt 套件的编译器版本,然后要一份用同一版本编出来的库;MSVC 用户优先走 Release 配置统一,MinGW 用户别跨大版本。
2.3 一个能用的包应该包含哪些文件
预编译版的价值在于目录结构清晰,你打开压缩包对照下面这张表看一眼,就知道它是不是完整的。
| 目录 | 内容 | 出现在哪个阶段 |
|---|---|---|
include/tesseract/ | baseapi.h、capi.h、renderer.h等 | 编译期 |
include/leptonica/ | allheaders.h、pix.h等 | 编译期(用 Pix 时) |
lib/ | tesseract5.lib(MSVC)或libtesseract5.dll.a(MinGW) | 链接期 |
bin/ | tesseract5.dll、leptonica-*.dll | 运行期 |
tessdata/ | chi_sim.traineddata、eng.traineddata、osd.traineddata | 运行期 |
osd.traineddata最容易被忽略,它负责方向和文字脚本检测,一旦你用了PSM_AUTO_OSD而没有这个文件,Init会直接失败。语言包的版本也要和主库的版本对上,Tesseract 4 和 5 的 traineddata 格式不同,拿 4.x 的chi_sim.traineddata喂给 5.x 的库,报的是 "Error opening data file",看着像路径问题,其实是版本不匹配。
注意:上面表格里的 DLL 名字带版本号后缀,具体叫什么以包内实际文件为准,别照着抄到我这里的示例字符串去写死。
2.4 静态库还是动态库,先定发布方式
静态链接听起来省事,实际在 Windows 上并不轻松:库本身得用/MT编,Leptonica 依赖的那一串编解码库也得是静态的,任何一个环节漏了就得照样带 DLL,最后 exe 体积翻倍还查不出问题来源。而语言包永远要外挂,tessdata没办法塞进 exe 里让 Tesseract 直接读。
所以实际项目里我会选动态库:把bin/下的 DLL 和tessdata/目录跟着 exe 一起发出去,升级语言包只要替换一个文件,换库也不用重编整个工程。代价是安装目录多几个文件,但这部分工作一次做完就能固化进构建脚本。
3. 接进 Qt 工程:qmake 与 CMake 两套配置和第一次识别
库选好了,接下来是落地。这一步的目标很明确:编译通过、链接通过、运行期能找到 DLL 和语言包,三者缺一不可。
3.1 目录摆放:把 3rdparty 钉在工程根目录
我习惯在工程根目录建一个3rdparty/tesseract/,把包里的include、lib、bin、tessdata原样搬进去。好处是所有路径都能用相对工程根目录的写法,换机器、换盘符、进 CI 都不用改配置。
千万不要把 DLL 放在源码目录然后靠系统 PATH 找。开发机上可能因为你之前手动加过环境变量而正常,一打包就原形毕露。运行期找 DLL 的顺序是 exe 所在目录优先,所以构建产物目录里必须有一份。
3.2 qmake 写法:INCLUDEPATH、LIBS 与运行期拷贝
老工程大多还是 qmake,配置文件直接写这几段:
QT += core gui CONFIG += c++17 TESS_ROOT = $$PWD/3rdparty/tesseract # 编译期:头文件路径 INCLUDEPATH += $$TESS_ROOT/include DEPENDPATH += $$TESS_ROOT/include # 链接期:导入库,名字按包内实际的 .lib / .dll.a 改 LIBS += -L$$TESS_ROOT/lib -ltesseract5 -lleptonica-1.82.0 # 运行期:构建完成后把 DLL 同步到输出目录,省掉手动拷贝漏文件 DEST_DIR = $$OUT_PWD/$$DESTDIR QMAKE_POST_LINK += $$quote(cmd /c copy /Y "$$shell_path($$TESS_ROOT/bin/*.dll)" "$$shell_path($$DEST_DIR)")-l后面跟的是去掉前缀和后缀的库名:MSVC 下tesseract5.lib写成-ltesseract5,MinGW 下libtesseract5.dll.a也写-ltesseract5,qmake 会自动补前缀。shell_path()不能省,路径里有空格时没有它copy会直接失败,而且失败信息藏在构建日志里很难发现。QMAKE_POST_LINK只在链接成功后执行,所以第一次全新构建如果链接就挂了,DLL 不会拷过去,别以为是拷贝命令写错。
3.3 CMake 写法:IMPORTED 目标比裸路径可靠
Qt 6 新工程基本都用 CMake 了。推荐用IMPORTED目标而不是直接往target_link_libraries里塞路径,这样头文件目录会跟着目标走,将来换库只改一处。
set(TESS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/tesseract") add_library(tesseract::lib SHARED IMPORTED) set_target_properties(tesseract::lib PROPERTIES IMPORTED_IMPLIB "${TESS_ROOT}/lib/tesseract5.lib" # MSVC 用 IMPORTED_LOCATION "${TESS_ROOT}/bin/tesseract5.dll" INTERFACE_INCLUDE_DIRECTORIES "${TESS_ROOT}/include") add_library(lept::lib SHARED IMPORTED) set_target_properties(lept::lib PROPERTIES IMPORTED_IMPLIB "${TESS_ROOT}/lib/leptonica-1.82.0.lib" IMPORTED_LOCATION "${TESS_ROOT}/bin/leptonica-1.82.0.dll" INTERFACE_INCLUDE_DIRECTORIES "${TESS_ROOT}/include") target_link_libraries(YourApp PRIVATE tesseract::lib lept::lib) # 构建后把运行期文件搬到 exe 旁边 add_custom_command(TARGET YourApp POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${TESS_ROOT}/bin" "$<TARGET_FILE_DIR:YourApp>" COMMAND ${CMAKE_COMMAND} -E copy_directory "${TESS_ROOT}/tessdata" "$<TARGET_FILE_DIR:YourApp>/tessdata")MinGW 下把IMPORTED_IMPLIB换成对应的.dll.a即可,IMPORTED_LOCATION仍然指向 DLL。$<TARGET_FILE_DIR:YourApp>是生成器表达式,多配置生成器(Debug/Release)下会自动落到正确子目录,比手写${CMAKE_BINARY_DIR}/Debug靠谱得多。
3.4 五行代码跑通第一次识别
配置通了,先用一个最小可运行程序验证链路,别急着往业务代码里塞。
#include <QCoreApplication> #include <QImage> #include <QDebug> #include <tesseract/baseapi.h> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); tesseract::TessBaseAPI api; // datapath 直接传 tessdata 目录的绝对路径,绕开环境变量的版本差异 if (api.Init("D:/work/ocr/3rdparty/tesseract/tessdata", "chi_sim+eng", tesseract::OEM_LSTM_ONLY) != 0) { qWarning() << "Init failed"; return -1; } api.SetPageSegMode(tesseract::PSM_AUTO); // 自动版面分析,通用性最好 api.SetSourceResolution(300); // 告诉引擎图像分辨率,别让它猜 QImage img("D:/work/ocr/test/ticket.png"); if (img.isNull()) return -2; img = img.convertToFormat(QImage::Format_Grayscale8); // 单通道灰度最省事 // 1 表示每像素 1 字节,bytesPerLine 必须用 Qt 给的值,不能用 width api.SetImage(img.constBits(), img.width(), img.height(), 1, img.bytesPerLine()); char *utf8 = api.GetUTF8Text(); qDebug().noquote() << QString::fromUtf8(utf8); delete[] utf8; // 必须 delete[],不是 free,也不是 delete api.End(); // 释放内部模型,长生命周期对象要显式调用 return 0; }几个参数值得单独说。OEM_LSTM_ONLY表示只用 LSTM 引擎,Tesseract 5 的默认语言包就是 LSTM 格式,用这个最匹配。PSM_AUTO让引擎自己做版面分析,适合不确定版式的图,速度慢一点但通用。SetSourceResolution(300)如果不设,日志里会打印 "Estimating resolution as XXX",估算错了小字号文本的识别率会掉一大截。GetUTF8Text返回的缓冲区由调用方释放,Tesseract 内部用的是new char[],所以必须delete[]。
3.5 打包发布:windeployqt 之后还要补什么
windeployqt只处理 Qt 自己的 DLL 和插件,Tesseract 的东西它一概不管。发布目录里必须额外确认三样:tesseract5.dll、leptonica-*.dll、整个tessdata目录。
路径拼接也有讲究。用QCoreApplication::applicationDirPath() + "/tessdata"拿到绝对路径再传给Init,不要用"./tessdata"。Windows 上相对路径是按进程当前工作目录解析的,用户从开始菜单快捷方式启动、或者被别的程序拉起来时,工作目录可能完全不是你想象的那个,结果就是开发机上好好的,装到客户机器上识别全空。
4. QImage 送进 TessBaseAPI:像素格式、生命周期与预处理
这一段是整个集成里最容易出玄学问题的地方,图片明明看得见,识别出来是空的、乱的、缺一半的,八成都在这里。
4.1 SetImage 的重载与三个必填参数
主要的重载是这个:
void SetImage(const unsigned char* imagedata, int width, int height, int bytes_per_pixel, int bytes_per_line);bytes_per_pixel只接受 1、3、4,分别对应灰度、RGB、RGBA。传 2 或者传 8 会直接报错或者给你一个错误结果。bytes_per_line是每行的字节跨度,很多人顺手写成width,然后就翻车了——Qt 的QImage按 4 字节对齐扫描行,一个宽度 1001 的 8 位灰度图,bytesPerLine()返回 1004,你传 1001,从第二行开始每一行都错位三个字节,识别结果就是一堆碎片。永远用img.bytesPerLine(),不要自己算。
4.2 灰度直通:最稳的一条转换路径
QImage::Format_Grayscale8是 Qt 5.5 之后一直有的格式,内存布局就是一个字节一个像素,和 Tesseract 期望的 1 bpp 灰度完全对得上,不需要任何转换开销。
QImage prepare(const QImage &src) { // 先转灰度,再拷贝一份,切断隐式共享,避免别处修改原图导致数据变化 QImage gray = src.convertToFormat(QImage::Format_Grayscale8); return gray.copy(); }copy()这一步看着多余,实际能挡掉一类很难查的崩溃:QImage是隐式共享的,convertToFormat返回的对象可能和别的QImage共享同一块缓冲区。你在一个线程里SetImage,另一个线程改了原图触发 detach,constBits()指向的内存就被释放了,识别过程中读到野指针。加上copy()就彻底独立了,代价是一次内存拷贝,对大图也就几毫秒。
生命周期还有一条:SetImage只是记录指针,真正读像素是在Recognise或GetUTF8Text里。所以从SetImage到拿到结果之间,那张QImage必须一直活着。
4.3 需要 Pix 时怎么转
有些场景绕不开 Leptonica,比如你要用pixScale、pixDeskew做倾斜校正,或者调pixConvertTo1做二值化。转换有两条路。
第一条省事:把QImage编码成 PNG 内存块,再让 Leptonica 读。
#include <QBuffer> #include <leptonica/allheaders.h> Pix *qimageToPix(const QImage &src) { QByteArray ba; QBuffer buf(&ba); buf.open(QIODevice::WriteOnly); src.save(&buf, "PNG"); // 多一次编码开销 return pixReadMemPng(reinterpret_cast<const l_uint8 *>(ba.constData()), ba.size()); }第二条快:手工逐行拷贝到 8 位 Pix。
Pix *qimageToPixFast(const QImage &src) { QImage gray = src.convertToFormat(QImage::Format_Grayscale8); Pix *pix = pixCreate(gray.width(), gray.height(), 8); for (int y = 0; y < gray.height(); ++y) { l_uint32 *line = pixGetData(pix) + y * pixGetWpl(pix); const uint8_t *s = gray.constScanLine(y); for (int x = 0; x < gray.width(); ++x) { SET_DATA_BYTE(line, x, s[x]); } } return pix; }注意pixGetWpl返回的是每行多少个 32 位字,不是字节数,所以是pixGetData(pix) + y * pixGetWpl(pix),直接按字节加会错位。返回的Pix*用完必须pixDestroy(&pix),Leptonica 的对象全是 C 风格手动管理,漏一次就是几 MB。
如果能用第一种就用第一种,pixReadMemPng走的是完整的解码路径,不会因为位深或对齐出岔子;只有在批量处理成千上万张图、编码开销明显时才值得换第二种。
4.4 预处理做到什么程度就够
Tesseract 内部会做 Otsu 二值化、去噪、连通域分析,所以很多教程教的那套"先灰度、再高斯模糊、再自适应二值化、再开运算"未必是好事,处理过头反而把笔画细节磨没了。我的经验是按图的来源分三种情况。
屏幕截图、扫描件这类白底黑字、对比度高的图,什么预处理都别做,直接灰度送进去。手机拍的纸质文档,先做一次对比度拉伸或者直方图均衡,把阴影压掉。小字号文本——字符高度小于 20 像素的那种——先放大两倍,用Qt::FastTransformation(二值图用快速插值,平滑插值会糊掉边缘),放大之后 Tesseract 的小字符分类器才有足够像素可用。
倾斜也是个大问题,超过 3 到 5 度,行分割就会出错。轻度倾斜用pixDeskew能救,重度倾斜得先做霍夫变换找直线,成本就上去了,通常更划算的是在采集端解决。
提示:不要用
QImage::scaled缩放到你"觉得合适"的尺寸,然后不告诉引擎分辨率。缩放后一定跟着SetSourceResolution(),让引擎知道它看到的 40 像素高字符实际对应多大的物理尺寸,否则内部那些基于 DPI 的阈值全是错的。
4.5 中文与 psm/oem:参数怎么设
中文识别和英文最大差别在字符集和分词。chi_sim是简体,chi_sim_vert是竖排,chi_tra是繁体。组合语言用加号,主语言写在前面,比如chi_sim+eng,识别中英混排的名片、票据很实用,代价是速度会慢三成左右,纯英文场景别这么写。
版面模式(psm)对结果影响比很多人想象的大:
| psm 值 | 含义 | 典型场景 |
|---|---|---|
PSM_AUTO(3) | 全自动分页 | 不确定版式,通用 |
PSM_SINGLE_BLOCK(6) | 假定单一块 | 规整的证件、单栏文档 |
PSM_SINGLE_LINE(7) | 单行文本 | 号码、标题 |
PSM_SINGLE_WORD(8) | 单词 | 单个字段校验 |
PSM_SPARSE_TEXT(11) | 稀疏文本 | 界面截图、散落标签 |
引擎模式(oem)在 Tesseract 5 里基本只有OEM_LSTM_ONLY和OEM_DEFAULT两种有意义,OEM_TESSERACT_ONLY需要 legacy 格式的语言包,而官方tessdata仓库里 5.x 的语言包早就不带 legacy 数据了,写这个值只会在Init时报错。
还有几个常用变量,用SetVariable设置:preserve_interword_spaces设成1能保留英文单词间的多空格,对表格类文本做后续解析很有用;debug_file设成一个不存在的路径或者系统的空设备,可以避免当前目录莫名多出一个tesseract.log,这个日志在客户机器上写不出来时还可能拖慢速度。
5. 避坑与排查:五条踩过的坑,每条都有解
下面这几条都是我在实际项目里撞过的,共同特点是错误信息完全指不到真正的原因,只能靠对照现象一条条排。
5.1 Init 返回 0,识别结果全是空串
现象:程序跑起来不报错,Init返回值是 0,SetImage也执行了,GetUTF8Text返回空字符串或者几个乱码字符。
原因:九成是datapath传错。最常见的错法是把路径写成了.../tessdata/tessdata——解压时多套了一层目录,或者把父目录传进去让引擎自己拼tessdata。Tesseract 各版本对TESSDATA_PREFIX到底指向父目录还是tessdata目录本身处理不一致,靠环境变量就是在赌版本。
解决:只用Init的第一个参数传绝对路径,并且这个路径直接就是包含chi_sim.traineddata的目录。启动时打一行日志把路径和QFileInfo::exists()的结果输出,出问题一眼就能看出来。另外确认语言包文件名全小写,Windows 虽然不区分大小写,但引擎拼文件名时是按语言名.traineddata严格拼的。
5.2 开发机能跑,拷到别的机器 0xc000007b 或直接闪退
现象:本机调试一切正常,打包发给别人,双击没反应,或者弹一个"应用程序无法正常启动 0xc000007b"。
原因:0xc000007b 是位数不匹配的经典错误,通常是包里混进了 32 位 DLL,或者 Qt 是 64 位而 Tesseract 是 32 位。闪退则多是缺leptonica-*.dll或缺 MinGW 运行库(libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll)。
解决:先用dumpbin /headers xxx.dll看 machine 字段,x64 的会显示8664,x86 是14C。然后用windeployqt补齐 Qt 侧依赖,再手工核对 Tesseract 那一组。MinGW 工程可以把三个运行库一起拷进输出目录,一次性解决。别指望在客户机上装个运行库合集,那是最不可控的做法。
5.3 tessedit_char_whitelist 设了跟没设一样
现象:只想识别数字,设置了api.SetVariable("tessedit_char_whitelist", "0123456789"),结果还是识别出一堆汉字和字母。
原因:白名单机制在 LSTM 引擎下基本失效,它是为老的 legacy 引擎设计的。Tesseract 4 之后默认走 LSTM,这个变量只影响非常有限的路径,社区里反馈多年也没什么变化。
解决:两条路。一是在识别后做后处理,用正则把非目标字符过滤掉,配合置信度阈值效果足够好;二是如果要走真正的白名单,得用 legacy 引擎加对应的语言包,但 5.x 的官方语言包已经不带 legacy 数据了,等于要自己额外准备一份。实际项目里我基本都走后处理,一行正则的事,比折腾引擎模式省心。
5.4 跑一万张图后内存涨到几个 G
现象:单张识别没问题,批量跑几百上千张之后内存持续上涨,最后卡死或者崩在GetUTF8Text内部。
原因:三类泄漏。GetUTF8Text返回的char*忘了delete[];Pix*忘了pixDestroy;多线程共用一个TessBaseAPI实例。
解决:char*一律delete[],用std::unique_ptr<char, decltype(&free)>不行,必须是数组删除。Pix*用 RAII 包一层std::unique_ptr<Pix, decltype(&pixDestroy)>,规则是创建它的地方负责销毁。线程这块要记牢:TessBaseAPI不是线程安全的,一个实例只能在一个线程里用,但可以每个线程各持一个实例,Init的几百毫秒开销在批量场景里摊薄到可以忽略。别为了省内存搞实例池,那是在给自己埋雷。
5.5 中文漏字、标点乱飞
现象:一段中文识别出来,常用字基本对,但数字和标点经常错,偶尔整行丢失。
原因:按概率从高到低排——没设SetSourceResolution,引擎按估算值工作;psm选错,多栏排版用了PSM_SINGLE_BLOCK;组合语言写成eng+chi_sim,英文在前导致中文被当成噪声;图片本身字符高度太小,低于 20 像素。
解决:按顺序改。先设分辨率,再根据版面换psm(多栏、散落文本用PSM_AUTO或PSM_SPARSE_TEXT),主语言放前面,最后再把小图放大两倍重试。中文标点错误率本来就高,官方语言包对全角标点的训练数据有限,真要精确到标点,得自己拿业务语料微调,那是另一个量级的工程。
6. 批量识别流水线:用置信度把结果筛一遍
单张识别跑通之后,真正决定这套东西能不能上生产的是批量场景下的结果可信度。我的做法是给每个识别结果配一个置信度,低于阈值的走人工复核队列,而不是直接丢弃——丢弃会让你永远不知道识别率的真实分布。
#include <tesseract/baseapi.h> struct OcrWord { QString text; float conf; }; QVector<OcrWord> recogniseWithConfidence(tesseract::TessBaseAPI &api) { QVector<OcrWord> out; api.Recognise(nullptr); // 先跑推理,GetIterator 才有效 tesseract::ResultIterator *it = api.GetIterator(); // 归 api 所有,不要 delete if (!it) return out; do { const char *word = it->GetUTF8Text(tesseract::RIL_WORD); if (!word) continue; const float conf = it->Confidence(tesseract::RIL_WORD); out.append({QString::fromUtf8(word).trimmed(), conf}); delete[] word; // 每个词都要单独释放 } while (it->Next(tesseract::RIL_WORD)); return out; }阈值不要拍脑袋定。我会先拿两三百张真实样本跑一遍,把conf分布打出来看:干净的扫描件大部分词在 85 以上,手机拍摄的集中在 60 到 80 之间,低于 50 的基本是噪声和分割错误。阈值定在分布的低谷位置,比定在某个整数上合理得多。这份统计同时还能告诉你问题出在图还是出在参数——如果整批图的置信度都偏低,先怀疑分辨率和预处理;如果只是某几行低,那多半是版面分割的问题。
还有一个我自己加的习惯:在批量流水线里,每个TessBaseAPI实例处理完固定张数(我一般设 500)就End()再重新Init。理论上不该有状态残留,但长跑任务里我确实遇到过某些极端图片把内部状态带歪、后续识别率下降的情况,重启实例是最便宜的后悔药。从那以后我每次上线批量任务,都会先强制跑一遍"Init 成功 + 置信度分布 + 前 50 张抽样人工核对"这三步,再放开全量。希望帮到你。
本文还有配套的精品资源,点击获取