1、写在前面
读过前面文章的小伙伴一定知道我筹划 Android 版本的 OnnxOCR 已经有一段时间了,而且那时候是 PP-OCRv5 刚刚发布。但项目写着写着,v6 就发布了。以我对 Android 版本的定位,它最好能够让用户简单使用,支持多个模型在PC上可以,但是移动终端上还是有些繁琐,所以我最后决定只支持 PP-OCRv6。
另外,移动端更在意体积和速度,tiny 模型本来就是为此而生;再把 v5 / v6 两套默认参数、字典和后处理长期并行维护,性价比低了些。C# 桌面端 OnnxOCRSharp 已经把 v6 流水线跑顺了,Android 端直接对齐这一套就挺不错。
前面两篇前奏也不是白写的:
《OnnxOCRAndroid的前奏1》:用官方 MobileNet V2 示例,把 OnnxRuntime Android 的依赖引入、Session、预处理、资源释放摸了一遍 《OpenCV 5 来了》:同样的分类任务换成 OpenCV 5 DNN,同时把官方 Maven 包 org.opencv:opencv:5.0.0在真机上跑通
推理用 OnnxRuntime、图像处理用 OpenCV——这和 OnnxOCRSharp 的分工一致。今天这篇,就是把两边拼起来,正式开源 OnnxOCRAndroid。
Demo 已经支持从相册选图,也支持直接拍照识别。核心引擎拆成了独立的 onnxocr-core 库模块,以后别的 App 也可以本地依赖它。
2、我的成果物
onnxocr-core | |
app | |
det.onnxrec.onnx + 字典,打进 Demo assets | |
还是采用跟C#端一样的方式,先做出一个能在真机稳定跑通的离线 OCR的MVP,后续继续加能力(方向分类、NNAPI、发布到 Maven 之类),不断完善,不断优化,敏捷开发。
3、核心技术栈
onnxruntime-android:1.18.0 | ||
org.opencv:opencv:5.0.0 | ||
关键依赖在 onnxocr-core 里:
implementation("com.microsoft.onnxruntime:onnxruntime-android:1.18.0")
api("org.opencv:opencv:5.0.0")
这里有个设计:
OnnxRuntime 用 implementation:推理细节封在库里,业务 App 一般碰不到 OrtSessionOpenCV 用 api:对外接口是TextSystem.run(Mat),调用方需要自己持有Mat、做bitmapToMat,所以类型必须透传出去
和 C# 端 Microsoft.ML.OnnxRuntime + OpenCvSharp4 是同一思路,只是 Android 的打包方式换成了 Gradle。
4、项目结构
onnxocr-android/
├── app/ # Demo 应用
│ └── src/main/
│ ├── assets/models/ppocrv6_tiny/
│ │ ├── det/det.onnx
│ │ ├── rec/rec.onnx
│ │ └── ppocrv6_tiny_dict.txt
│ └── java/.../MainActivity.kt
└── onnxocr-core/ # OCR 核心类库
└── src/main/java/com/linc/onnxocr/
├── config/ # OcrOptions(v6 默认参数)
├── detection/ # 检测 + DB 后处理
├── recognition/ # 识别 + CTC 解码
├── imaging/ # 裁剪、排序、嵌套框过滤
├── inference/ # OnnxSessionFactory
├── model/ # OcrResult / TextLine
└── pipeline/ # TextSystem 编排
分层尽量对齐 OnnxOCRSharp:算法在 Core,Demo 只负责拿图、展示、复制。模型文件放在 app 的 assets 里——库保持瘦,模型按业务需要自行打包或下载。
5、整体流水线
和桌面端 v6 一样,默认是两阶段(检测 + 识别):
Bitmap / 拍照图
↓
decode(最长边≤1600,EXIF 校正)→ Mat
↓
TextSystem.run()
├─ RGBA/GRAY → BGR
├─ TextDetector(det.onnx)→ DbPostProcess
├─ BoxSorter(从上到下、从左到右)
├─ ImageCropper(透视裁剪;竖排高/宽≥1.5 则转 90°)
├─ TextRecognizer(rec.onnx,按宽度比批推理)→ CTC
└─ dropScore 过滤
↓
OcrResult(文本 + 置信度 + 四边形框 + 总耗时)
对外真正要记住的类就两个:OcrOptions 和 TextSystem。
6、让我们跑通 Demo
6.1 环境
Android Studio Hedgehog(2023.1.1)或更新 Kotlin 1.9.20 / AGP 8.5.0 真机
源码地址见文末。打开工程根目录即可。
6.2 同步、编译、安装
Android Studio → Open → 选择 onnxocr-android/等待 Gradle Sync(首次会拉 onnxruntime-android和opencv:5.0.0)连接手机,开启 USB 调试,Run app
避坑(国内网络):Gradle 包装器下载慢的话,可以把 gradle-wrapper.properties 的 distributionUrl 换成国内镜像,例如腾讯云:
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.5-bin.zip
6.3 首次启动复制模型到手机
Demo 首次启动会在后台:
OpenCVLoader.initLocal()初始化 OpenCV 5把 assets 里的三个模型文件拷到 filesDirOcrOptions.createDefault(context)指向这些绝对路径创建 TextSystem
有个小细节:app 模块给 .onnx 关了压缩——
androidResources {
noCompress += "onnx"
}
这样 AssetManager.openFd 能拿到准确长度,拷贝时才能判断「本地缓存是否过期」。模型升级后,体积对不上就会自动重拷。
6.4 开始识别吧
点「选择图片」→ 从相册选择 或 拍照 预览出现后点「开始识别」 左侧叠加红色检测框,右侧看置信度和文本 「复制全部」把结果丢进剪贴板
拍照走系统相机 + FileProvider,照片落在缓存目录,再走和相册同一套解码:最长边控制到 1600、读 EXIF 旋转。
7、核心代码
7.1 默认配置:对齐 pp-ocrv6
OcrOptions :主体参数对齐 OnnxOCRSharp 的 ApplyPpOcrV6Defaults,但长边软限制在手机上改成了 960(桌面 C# 默认是 4000):
dataclassOcrOptions(
val detModelPath: String,
val recModelPath: String,
val dictPath: String,
// PP-OCRv6: short side >= 736 (limit_type=min).
// Soft max-side cap for mobile (desktop C# uses 4000).
val detLimitSideLen: Int = 736,
val detLimitType: String = "min",
val detMaxSideLimit: Int = 960,
val detDbThresh: Float = 0.2f,
val detDbBoxThresh: Float = 0.4f,
val detDbUnclipRatio: Float = 1.4f,
val detDbMaxCandidates: Int = 3000,
val dropScore: Float = 0.5f,
val recBatchNum: Int = 6,
val recImageShape: String = "3, 48, 320",
val numThreads: Int = 4
)
detLimitType | min | |
detLimitSideLen | ||
detMaxSideLimit | 960 | |
detDbThresh | ||
detDbBoxThresh | ||
detDbUnclipRatio | ||
recImageShape | 3, 48, 320 | |
numThreads |
createDefault(context) 只负责拼 filesDir 路径,算法参数全吃默认值——够大多数 Demo / 嵌入场景了。
7.2 TextSystem:编排器
funrun(image: Mat): OcrResult {
val bgr = toBgr(image) // Utils.bitmapToMat 出来是 RGBA
try {
val boxes = detector.detect(bgr)
if (boxes.isEmpty()) return OcrResult(/* empty */)
val sortedBoxes = BoxSorter.sort(boxes)
val crops = sortedBoxes.map { ImageCropper.crop(bgr, it, options.detBoxType) }
try {
val recResults = recognizer.recognize(crops)
// score >= dropScore 才进结果
return OcrResult(lines = lines, elapsed = ...)
} finally {
crops.forEach { it.release() }
}
} finally {
if (bgr !== image) bgr.release()
}
}
几点和桌面端一致、也和前奏里强调过的资源习惯一致:
通道先转成 BGR,再喂检测——C# 端读图也是 BGR,不能随便改成 RGB,否则和训练预处理对不上 裁剪出来的 Mat必须release(),OpenCV 不走 Java GCTextSystem实现AutoCloseable,页面销毁时记得close()
7.3 调用方只要这几行
val options = OcrOptions.createDefault(context)
val textSystem = TextSystem(context, options)
val mat = Mat()
Utils.bitmapToMat(bitmap, mat)
val result = textSystem.run(mat)
for (line in result.lines) {
println("${line.text}${line.score}")
}
mat.release()
textSystem.close()
自定义模型时,把三个绝对路径传进 OcrOptions 即可,不必走 assets 拷贝。
7.4 Demo 侧的后台初始化
privatefuninitOpenCvAndOcr() {
Thread {
val loaded = try {
OpenCVLoader.initLocal()
} catch (_: NoSuchMethodError) {
OpenCVLoader.initDebug()
}
// ...
copyModelsIfNeeded()
val options = OcrOptions.createDefault(this)
textSystem = TextSystem(this, options)
runOnUiThread { tvStats.text = "OCR 引擎已就绪 (PP-OCRv6 tiny)" }
}.start()
}
OpenCV 5 推荐 initLocal(),不再依赖当年的 OpenCV Manager。识别本身也扔到后台线程,避免卡 UI——手机端这点比桌面更敏感。
8、踩过的坑(尤其是 OpenCV 5)
8.1 boxPoints 写法不对,会「检测全空」
这是 Android 移植里最坑、也最值得单独写一节的地方。
一开始按桌面端的直觉,用 Geometry.boxPoints 再 MatOfPoint2f.toArray() 取四个角点。在 OpenCV 5 上会出现一种很阴间的现象:推理有输出、轮廓也找到了,但最终框数永远是 0。
原因在 DbPostProcess:
boxPoints写出的是 4×2 的CV_32FC1Mat;再当成MatOfPoint2f去toArray(),total()会变成 8,凭空多出四个(0,0)点,框分数被打穿,等于没检测到。
正确姿势是直接用 RotatedRect.points():
privatefunrotatedRectPoints(rect: RotatedRect): Array<Point> {
val pts = arrayOf(Point(), Point(), Point(), Point())
rect.points(pts)
return pts
}
如果你也在 Android / OpenCV 5 上搬 DB 后处理,建议先拿一张简单中文图验证「至少能出框」,再做参数调优。
8.2 轮廓只用 EXTERNAL
findContours 用了 RETR_EXTERNAL,避免孔洞轮廓在手机端碎成多余小框。桌面端可以更宽松,移动端我更偏向少而稳。
8.3 Unclip 做了简化
C# 端优先走 Clipper 多边形膨胀,Android 这边用了更轻的几何扩展。效果在常见文档/菜单/截图场景够用;极端弯曲文本以后再加强也不迟。
9、来看看效果
不同机型差异会很大,我本地真机上的使用请看视频:
评论区给出你的意见,用陆光的话说,就是“追番,拜托啦!”。
10、开源地址
# 源码(Gitee)
https://gitee.com/lincyang/onnxocr-android
# 上游 C# 版
https://github.com/lincyang/OnnxOCRSharp
# 网盘下载APK
公众号聊天输入:onnxocr-android
工程打开即可编译;模型已随 Demo assets 提供。如果想直接下载APK,就对公众号发关键字"onnxocr-android"获取链接,直接安装即可。
欢迎关注公众号「程序员Linc」获取更多技术文章和项目更新!
参考
onnxocr-android 源码:https://gitee.com/lincyang/onnxocr-android OnnxOCRSharp:https://github.com/lincyang/OnnxOCRSharp OnnxOCR:https://github.com/jingsongliujing/OnnxOCR PP-OCRv6 魔塔集合:https://www.modelscope.cn/collections/PaddlePaddle/PP-OCRv6 ONNX Runtime Android:https://onnxruntime.ai/docs/tutorials/mobile/deploy-android.html OpenCV 5 Android Maven: org.opencv:opencv:5.0.0
夜雨聆风