PaddleOCR PP-OCRv6 Android 部署实战:基于 ONNX Runtime 的 OCR SDK 与 Demo 全解析
本文基于 PaddleOCR 官方仓库 deploy/ppocr-android 示例项目,系统讲解 PP-OCRv6 在 Android 端的 ONNX Runtime 推理部署方案。内容包括环境准备、模型文件布局、SDK 的两种集成方式(源码依赖 / AAR)、完整 API 参考与配置参数,以及检测/识别流水线的源码级实现原理和自动化性能测试方法。读完后你将能够把该 SDK 直接集成进自己的 Android 应用,完成端到端的文字检测与识别,并对各阶段耗时进行定位分析。
项目概览:SDK 与 Demo 分离架构
deploy/ppocr-android 是 PaddleOCR v6 的 Android 部署示例,使用 ONNX Runtime 完成移动端 OCR 推理。项目采用 SDK 与 Demo 分离 的架构:ppocr-sdk 是一个标准的 Android Library,可以独立集成到任意第三方应用;app 则是一个基于 MVVM + Jetpack Compose 的演示应用,展示 SDK 的完整用法。
核心特性:
- 端到端的文字检测 + 识别流水线(Det → Rec)
- 支持 PP-OCRv6 系列 ONNX 模型
- 细粒度性能计时(检测/识别阶段分别拆分出预处理、推理、后处理耗时)
- MVVM + Jetpack Compose 演示应用
- AAR 集成支持
实际目录结构(对照仓库中 deploy/ppocr-android 的真实文件):
ppocr-android/
├── ppocr-sdk/ # OCR SDK(Android Library)
│ ├── src/main/
│ │ ├── assets/models/ # 模型文件目录(需自行放入)
│ │ │ ├── det/ # 检测模型:inference.onnx
│ │ │ └── rec/ # 识别模型:inference.onnx、inference.yml
│ │ └── java/com/paddle/ocr/
│ │ ├── PaddleOCR.kt # [公开 API] SDK 入口
│ │ ├── PaddleOCRConfig.kt # [公开 API] 推理配置
│ │ ├── EngineConfig.kt # [公开 API] 引擎配置(线程数)
│ │ ├── engine/ # OCREngine / DetectionEngine /
│ │ │ # RecognitionEngine / ORTSessionManager
│ │ ├── preprocess/ # DetPreprocessor / RecPreprocessor
│ │ ├── postprocess/ # DBPostProcessor / CTCDecoder /
│ │ │ # PolygonUnclip / QuadTextCrop 等
│ │ ├── model/ # OCRResult / OCRRunResult / OCRError ...
│ │ └── util/ # OpenCVUtils / YamlUtils / BitmapUtils
│ └── build.gradle.kts
├── app/ # Demo App(Compose + MVVM)
│ ├── src/main/java/com/paddle/ocr/demo/
│ │ ├── OCRApplication.kt # 应用启动时初始化 SDK
│ │ ├── MainActivity.kt
│ │ └── ui/ # Compose UI(HomeScreen、组件等)
│ └── build.gradle.kts
├── gradle/libs.versions.toml # 版本目录(依赖版本统一管理)
├── run_benchmark.sh # 性能测试脚本
└── settings.gradle.kts
从源码结构看,SDK 内部按职责分为四层:engine(会话管理与检测/识别引擎)、preprocess(图像预处理)、postprocess(DB 后处理与 CTC 解码)、model(结果与错误模型),入口 PaddleOCR.kt 是唯一需要对外暴露的 facade。
环境要求
| 依赖 | 版本 |
|---|---|
| Android Studio | Ladybug (2024.2+) |
| JDK | 17 |
| Kotlin | 2.1.0 |
| minSdk | 26(Android 8.0) |
| ONNX Runtime | 1.21.1 |
| OpenCV | 4.5.3(com.quickbirdstudios:opencv) |
| compileSdk / targetSdk | 35 |
以上版本与仓库中的 版本目录 完全一致:onnxruntime = "1.21.1"、opencv = "4.5.3"、coroutines = "1.9.0"、kotlin = "2.1.0"、AGP 8.7.3。SDK 模块 build.gradle.kts 中 minSdk = 26、compileSdk = 35、JVM target 为 17,Demo 模块 app/build.gradle.kts 的 targetSdk = 35。
快速开始
1. 获取代码
获取 PaddleOCR 仓库后进入示例目录:
git clone https://gitcode.com/GitHub_Trending/pa/PaddleOCR.git
cd PaddleOCR/deploy/ppocr-android
2. 准备模型文件
项目支持以下 ONNX 推理模型(可从 PaddlePaddle 官方模型仓库获取对应的 *_onnx_infer 推理包):
| 模型 | 说明 |
|---|---|
| PP-OCRv6_small | small 规格的检测 + 识别 ONNX 模型 |
| PP-OCRv6_tiny | tiny 规格的检测 + 识别 ONNX 模型,体积更小 |
| PP-OCRv5_mobile | PP-OCRv5 mobile 检测 + 识别 ONNX 模型 |
下载并解压后,将文件放到 ppocr-sdk/src/main/assets/models/:
- 检测模型:
inference.onnx放入models/det/ - 识别模型:
inference.onnx与inference.yml放入models/rec/
模型为什么必须放在 assets 里?从源码看,ORTSessionManager.kt 的 readModelAsset 通过 context.assets.open(assetPath) 读取模型字节流,缺文件时会抛出 OCRError.ModelNotFound;识别模型的 inference.yml 则被 ModelConfig.kt 解析,从中提取 PostProcess: 段下的 character_dict: 字符列表(末尾自动补空格字符)供 CTC 解码使用。因此 yml 与 onnx 必须同时提供。
3. 构建并运行
# 构建 Debug APK
./gradlew :app:assembleDebug
# 安装到设备
./gradlew :app:installDebug
也可以直接在 Android Studio 中 Run。Demo 的初始化逻辑在 OCRApplication.kt:Application.onCreate() 中先调用 OpenCVUtils.init() 加载 OpenCV 原生库,成功后在 IO 协程中执行 PaddleOCR.create(),并通过 StateFlow<ModelState>(Loading / Ready / Error 三态)驱动 Compose UI;加载失败时 UI 提供 retryLoadModels() 重试入口。
4. 体验 Demo
- 打开 "PP-OCRv6 Demo" 应用
- 等待模型加载完成
- 点击 "Select from Gallery" 选择一张图片
- 查看识别结果与分阶段耗时统计(UI 中的 TimingBar 组件展示各阶段毫秒数)
SDK 集成
方式一:源码依赖
-
将
ppocr-sdk/拷贝到你的项目根目录 -
在
settings.gradle.kts中加入:include(":ppocr-sdk") -
在 app 模块的
build.gradle.kts中加入:implementation(project(":ppocr-sdk"))
方式二:AAR 依赖
# 构建 AAR
./gradlew :ppocr-sdk:assembleRelease
产物位于 ppocr-sdk/build/outputs/aar/ppocr-sdk-release.aar。将 AAR 放入宿主工程的 libs/ 目录,并在 app 模块的 build.gradle.kts 中声明:
dependencies {
implementation(files("libs/ppocr-sdk-release.aar"))
// AAR 不会传递依赖,需要手动补齐
implementation("com.microsoft.onnxruntime:onnxruntime-android:1.21.1")
implementation("com.quickbirdstudios:opencv:4.5.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}
注意 SDK 模块在 build.gradle.kts 中配置了 consumerProguardFiles("proguard-rules.pro"),消费方 release 构建时会自动携带其混淆规则(见下文工程注意事项)。
API 参考
创建实例
PaddleOCR 提供三个 suspend 工厂重载(见 PaddleOCR.kt),内部统一通过 withContext(Dispatchers.IO) 执行,并使用 applicationContext 避免持有 Activity 引用:
// 默认配置(使用 assets 默认模型路径)
val ocr = PaddleOCR.create(context)
// 自定义配置
val ocr = PaddleOCR.create(
context = context,
config = PaddleOCRConfig(
detThresh = 0.3f,
detBoxThresh = 0.6f,
recScoreThresh = 0.0f,
recBatchSize = 1,
),
engineConfig = EngineConfig(numThreads = 4),
)
// 自定义模型路径(模型不在默认 assets 位置时)
val ocr = PaddleOCR.create(
context = context,
config = config,
engineConfig = EngineConfig(numThreads = 4),
detModelAssetPath = "models/det/inference.onnx",
recModelAssetPath = "models/rec/inference.onnx",
recConfigAssetPath = "models/rec/inference.yml",
)
执行 OCR
// 传入 Bitmap
val result = ocr.recognize(bitmap)
// 传入图片字节(推荐,与 Python 端管线行为一致)
val result = ocr.recognize(imageBytes)
// 读取结果
result.results.forEach { item ->
println("Text: ${item.text}, Confidence: ${item.confidence}")
println("Box: ${item.box.points}")
}
println("Detection: ${result.detectionTimeMs}ms, Recognition: ${result.recognitionTimeMs}ms")
两种入参最终都汇聚到同一个引擎执行入口:Bitmap 经 BitmapUtils.bitmapToBGRMat 转 Mat,字节流经 BitmapUtils.imdecodeBGR 解码(解码失败或空输入会抛出 OCRError.InvalidImage)。实例还暴露 coldLoadTimeMs 属性,可直接读取模型冷加载耗时。
释放资源
ocr.release()
配置参数
PaddleOCRConfig 完整定义(与 PaddleOCRConfig.kt 一致):
data class PaddleOCRConfig(
val detImgMode: String = "BGR", // 检测输入颜色模式(BGR / RGB)
val detLimitSideLen: Int = 64, // 检测侧长限制
val detLimitType: String = "min", // 限制策略(min / max / none)
val detMaxSideLimit: Int = 4000, // 最大侧长上限
val detThresh: Float = 0.3f, // 概率图二值化阈值
val detBoxThresh: Float = 0.6f, // 检测框置信度阈值
val detUnclipRatio: Float = 1.5f, // 检测框扩张(unclip)比例
val detMaxCandidates: Int = 3000, // 最大候选框数量
val detUseDilation: Boolean = false, // 是否对概率图做膨胀
val detScoreMode: String = "fast", // 打分模式(fast / slow)
val detBoxType: String = "quad", // 检测框类型(仅支持 quad)
val recScoreThresh: Float = 0.0f, // 识别置信度阈值(低于则丢弃)
val recBatchSize: Int = 1, // 识别批大小
)
参数与源码行为的对应关系(有助于调参):
detLimitSideLen / detLimitType / detMaxSideLimit / detImgMode:传入DetPreprocessor。默认limit_type=min意味着以较短边对齐 64,再按 32 的倍数缩放,且较长边不超过detMaxSideLimit(4000),保证大图不爆内存。detThresh / detBoxThresh / detUnclipRatio / detMaxCandidates / detUseDilation / detScoreMode / detBoxType:全部作用于DBPostProcessor(DB 算法后处理,下节详述)。其中box_type目前仅支持"quad",源码中有require(boxType == "quad")校验。recScoreThresh:在 OCREngine.kt 中对每条识别结果过滤,confidence < recScoreThresh的行不会进入最终结果;默认 0.0 表示全部保留。recBatchSize:识别裁剪图按此大小分批送入识别引擎(coerceAtLeast(1)保证至少为 1);当为 1 时结果会额外记录逐行识别耗时perLineRecMs,便于定位慢行。EngineConfig(numThreads = 4):设置 ONNX Runtime 会话的setIntraOpNumThreads,并启用OptLevel.ALL_OPT全量图优化(见 ORTSessionManager.kt)。
结果模型
data class OCRRunResult(
val results: List<OCRResult>, // 识别结果列表
val detectionTimeMs: Long, // 检测总耗时
val recognitionTimeMs: Long, // 识别总耗时
val totalTimeMs: Long, // 总耗时
val lineCount: Int, // 识别行数
// 详细分段计时
val detPreprocessMs: Long = 0, // 检测-预处理
val detInferenceMs: Long = 0, // 检测-推理
val detPostprocessMs: Long = 0, // 检测-后处理
val recPreprocessMs: Long = 0, // 识别-预处理
val recInferenceMs: Long = 0, // 识别-推理
val recPostprocessMs: Long = 0, // 识别-后处理
val pipelineOverheadMs: Long = 0, // 管线自身开销(总耗时 - 检测 - 识别)
val coldLoadTimeMs: Long = 0, // 模型冷加载耗时
// 输入张量形状
val detInputShape: List<Int> = emptyList(),
val recInputShapes: List<List<Int>> = emptyList(),
// 逐行识别耗时(仅当 recBatchSize == 1 时填充)
val perLineRecMs: List<Long> = emptyList(),
)
data class OCRResult(
val box: OCRBox, // 检测框四点坐标
val text: String, // 识别文本
val confidence: Float, // 置信度
)
完整的字段定义见 OCRRunResult.kt。异常则统一收敛为 OCRError.kt 中的 sealed 类:ModelNotFound、ModelLoadFailed、ConfigParseFailed、InvalidImage、InferenceFailed、DecodeError,便于上层做精确的错误处理。
推理流水线源码剖析
OCREngine 主流程
OCREngine.kt 的 run 方法体现了完整管线:
- 检测:
DetectionEngine.detect()输出候选框列表;若无框则直接返回空结果(此时recognitionTimeMs = 0)。 - 排序:
BoxSorter.sortInReadingOrder()将四边形框按阅读顺序排列。 - 裁剪与识别:按
recBatchSize分批调用QuadTextCrop.crop()从原图裁出文本区域,送入RecognitionEngine.recognize()做 CTC 解码;每行结果按recScoreThresh过滤后汇入allResults。 - 计时汇总:
pipelineOverhead = 总耗时 - 检测耗时 - 识别耗时,即为排序、裁剪、对象构造等管线自身开销。
所有 Mat 均在 try/finally 中 release(),避免原生内存泄漏——这是移动端 OpenCV 编程的关键实践。
检测阶段:预处理与 DB 后处理
DetPreprocessor.kt 的执行步骤:
- 按
detImgMode选择保留 BGR 或转为 RGB; resizeToMultipleOf32按limitSideLen / limitType / maxSideLimit缩放,输出尺寸保证为 32 的倍数(与 DB 网络多次下采样对齐);- 归一化:
x * (1/255)后减 mean(0.485, 0.456, 0.406)、除 std(0.229, 0.224, 0.225)(ImageNet 统计量); - 转成 CHW 排布的
FloatArray,形状(1, 3, H, W)。
DBPostProcessor.kt 实现了 DB 算法的经典后处理链:
- 将概率图按
detThresh做THRESH_BINARY二值化; detUseDilation = true时用 2×2 核做膨胀(缓解字符断行);RETR_LIST + CHAIN_APPROX_SIMPLE查找轮廓,最多取detMaxCandidates个候选;- 每个候选取
minAreaRect,短边小于 3px 的丢弃(MIN_SIZE_BEFORE_UNCLIP); - 打分:
score_mode=fast时按四个顶点取值,slow时沿轮廓逐点取值后平均;分数低于detBoxThresh的丢弃; PolygonUnclip.unclip(points, unclipRatio)按扩张比例放大多边形后重新取最小外接矩形,短边仍小于 5px 的丢弃;- 坐标按
originalW/pW、originalH/pH缩放回原图尺寸,输出四边形框。
识别阶段:定高裁剪 + CTC 解码
RecPreprocessor.kt 对每批裁剪图:BGR→RGB → 等比缩放到固定高度 48(宽度上限 3200)→ 归一化 (x / 127.5) - 1 → 右侧 padding 到批内最大宽度 → 组成长度为 N 的张量 (N, 3, 48, maxW)。
识别后处理是 CTCDecoder.decode(output, shape, characterList):字符表来自识别模型 inference.yml 中 PostProcess.character_dict 的解析结果(ModelConfig.kt 手写了一个轻量 YAML 列表解析器,只依赖 character_dict: 段,避免了引入完整 YAML 库)。这也解释了为什么模型文件必须同时包含 inference.yml。
会话管理
ORTSessionManager.kt 统一管理 OrtEnvironment 与两个 OrtSession:模型加载失败时做资源回滚(识别会话创建失败会关闭已创建的检测会话);推理时动态读取 session 的第一个输入/输出名,不硬编码张量名;coldLoadTimeMs 在 loadModels 全程计时,release() 依次关闭两个 session 并置空。
性能测试
项目内置了基于 Android Instrumentation 的自动化基准测试 OCRBenchmarkTest.kt,配套参考图片 ppocr-sdk/src/androidTest/res/raw/android_ocr_benchmark_reference.png,并通过 run_benchmark.sh 一键驱动:
# 运行基准测试(迭代次数 默认50,预热 默认30)
./run_benchmark.sh
./run_benchmark.sh 10 3 # 10 次正式测量 + 3 次预热
脚本内部执行 :ppocr-sdk:connectedAndroidTest,只跑 OCRBenchmarkTest#testLatencyBenchmark,参数经 -Pandroid.testInstrumentationRunnerArguments.warmup / .iterations 注入;结果从 adb logcat -s System.out 中按 OCRBenchmark 标签过滤输出。测试还支持 rec_batch_size 参数对比批识别收益。输出示例(表格中的数值来自文档示例,实际数值取决于你的设备):
╔═════════════════════════════════════════════════════════════════════════╗
║ PP-OCRv6 Speed Benchmark Results ║
╠═════════════════════════════════════════════════════════════════════════╣
║ Device: GM1900 | OS: Android 9 | Lines: 5 ║
║ Cold load: 158ms | Warmup: 3 | Measured: 10 ║
╠═════════════════════════════════════════════════════════════════════════╣
| Stage | Mean ms | Stdev | P90 | Min ms|
|-----------------------------+----------+----------+----------+----------|
| Total pipeline | 420.40 | 6.37 | 427 | 413 |
| Detection (total) | 348.70 | 4.67 | 356 | 343 |
| Preprocess | 33.30 | 2.90 | 36 | 28 |
| Inference | 311.00 | 2.93 | 315 | 304 |
| Postprocess | 4.40 | 0.49 | 5 | 4 |
| Recognition (total) | 66.20 | 3.16 | 68 | 64 |
| Preprocess | 3.00 | 0.89 | 4 | 2 |
| Inference | 60.60 | 3.14 | 63 | 58 |
| Postprocess | 2.60 | 0.92 | 4 | 1 |
| Pipeline overhead | 5.50 | 0.50 | 6 | 5 |
╚═════════════════════════════════════════════════════════════════════════╝
这套统计指标(Mean / Stdev / P90 / Min)与 OCRRunResult 的分段计时字段一一对应:因为 SDK 本身就把预处理、推理、后处理、管线开销拆开了,基准测试只是对其做多次采样统计。
工程注意事项
- OpenCV 初始化顺序:必须在
PaddleOCR.create()之前调用OpenCVUtils.init(context),否则预处理/后处理依赖的 OpenCV 原生函数不可用(Demo 中见 OCRApplication.kt 的顺序)。 - 协程调用:
create()、recognize()、release()均为suspend函数,内部已切到Dispatchers.IO,调用方只需处于协程作用域内。 - 内存管理:应用退出或不再需要时调用
release()关闭 ONNX 会话;Demo 在Application.onTerminate()中做了释放。 - ProGuard 规则:proguard-rules.pro 中保留了
com.paddle.ocr.**与ai.onnxruntime.**两个包,AAR 方式集成时通过 consumer rules 自动生效;源码集成且宿主开启混淆时应确保同样的 keep 规则。 - 模型路径前提:默认模型路径
models/det/inference.onnx、models/rec/inference.onnx、models/rec/inference.yml均相对于 assets 根目录;自定义路径必须仍位于 assets 内(SDK 通过AssetManager读取)。
小结
deploy/ppocr-android 展示了一个可投产的移动端 OCR 集成形态:SDK 侧用 ONNX Runtime + OpenCV 完整复刻了 PP-OCR 的检测(DB)与识别(CTC)链路,参数与 PaddleOCR Python 端配置一一对应;API 层通过协程 + sealed 错误模型保证线程安全与错误可辨识;性能侧则提供从分段计时到自动化基准测试的完整工具链。对于需要在 Android 端本地运行 PP-OCRv6 的场景,直接以 ppocr-sdk 为集成起点,再按需调整 PaddleOCRConfig 中的检测阈值与识别批大小即可。
主要参考文件:
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python310
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46467
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20043
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java33951