Java App (Activity) -> JNI (OcrEngine.java) -> C++ (OcrLite.cpp, DbNet.cpp, ...)a# chineseocr_lite 纯 C++ 移植指南:从 Android Java 层到原生 aarch64 可执行文件
声明:本文是作者作为 Rust 开发者的 C++ 初学项目记录,旨在将 chineseocr_lite 的 Android 版本去掉 Java 层后,编译为可在 aarch64 设备上直接运行的原生可执行文件。文中包含大量试错过程,适合同样从其他语言转入 C++ 的初学者参考。
一、项目背景与目标
1.1 为什么做这个?
chineseocr_lite 是一个轻量级中文 OCR 项目,基于 ncnn 和 OpenCV 实现。其 Android 版本通过 JNI 桥接 Java 层与 C++ 原生代码,结构如下:
Java App (Activity) -> JNI (OcrEngine.java) -> C++ (OcrLite.cpp, DbNet.cpp, ...)
目标:去掉所有 Java/JNI 层,将核心 C++ 代码编译为独立的 main 可执行文件,通过 adb shell 直接在 Android 设备上运行,支持 aarch64 真机及多种模拟器架构。
1.2 技术栈
| 组件 | 用途 | 版本 |
|---|---|---|
| ncnn | 神经网络推理框架 | 官方预编译包 |
| opencv-mobile | 轻量级图像处理库 | 3.4.15-android |
| CMake | 构建系统 | >= 3.10 |
| Android NDK | 交叉编译工具链 | r28.2 |
| Ninja | 构建生成器 | NDK 内置 |
二、核心问题与解决方案
2.1 去掉 Java 层后的入口改造
原代码通过 JNI_OnLoad 初始化 OCR 引擎,Java_com_..._detect 接收 Bitmap 对象。
改造后使用标准 main(int argc, char* argv[]) 入口,支持命令行参数:
int main(int argc, char* argv[]) {
// 图片路径为必需参数
char* imagePath = argv[1];
// 其余参数可选,带默认值
int numThreads = 4;
float boxScoreThresh = 0.6f;
float boxThresh = 0.3f;
float unClipRatio = 2.0f;
bool doAngle = false;
bool mostAngle = true;
int maxSideLen = 1024;
int padding = 0;
// 解析 --key value 格式的命令行选项
// ...
}
关键改动:
- 移除所有
JNIEXPORT、JNIEnv*、jobject等 JNI 类型 - 将
bitmapToMat替换为自定义的readImg函数,读取自定义.raw格式图片 - 日志系统从 Android 的
LOGE/LOGI替换为std::cout
2.2 自定义图片输入格式
由于去掉了 Java 层的 Bitmap 解码,需要自行处理图片输入。采用简单的二进制 .raw 格式:
| 字节偏移 | 长度 | 内容 |
|---|---|---|
| 0-3 | 4 bytes | 宽度 (int32, 小端序) |
| 4-7 | 4 bytes | 高度 (int32, 小端序) |
| 8-15 | 8 bytes | 保留字段(跳过) |
| 16+ | widthheight4 | RGBA 像素数据 |
读取实现(C++):
cv::Mat readImg(const std::string& filePath) {
std::ifstream file(filePath, std::ios::binary);
int32_t width = 0, height = 0;
file.read(reinterpret_cast<char*>(&width), sizeof(width));
file.read(reinterpret_cast<char*>(&height), sizeof(height));
file.seekg(8, std::ios::cur); // 跳过保留字段
// 读取像素数据并转换为 cv::Mat
std::vector<uint8_t> pixels(dataSize);
file.read(reinterpret_cast<char*>(pixels.data()), dataSize);
cv::Mat img(height, width, CV_8UC4, pixels.data());
return img.clone(); // 深拷贝,避免局部 vector 释放后悬空
}
注意:OCR 模型需要 RGB 三通道输入,因此读取后必须执行颜色转换:
cv::Mat imgRGB;
cv::cvtColor(img, imgRGB, cv::COLOR_RGBA2RGB);
2.3 多 ABI 自由切换的 CMake 配置
Android 设备与模拟器架构各异,需要一套 CMake 配置支持四种 ABI:
# 默认 ARM64,可通过 -DANDROID_ABI=... 覆盖
if(NOT ANDROID_ABI)
set(ANDROID_ABI arm64-v8a)
endif()
# 根据 ABI 动态切换 ncnn 和 OpenCV 库路径
set(ncnn_DIR "${CMAKE_SOURCE_DIR}/lib/ncnn/${ANDROID_ABI}/lib/cmake/ncnn")
set(OpenCV_DIR "${CMAKE_SOURCE_DIR}/lib/opencv-mobile-3.4.15-android/sdk/native/jni/${ANDROID_ABI}")
支持的 ABI:
| ABI | 适用场景 | 编译命令 |
|---|---|---|
arm64-v8a | 真机 64 位(默认) | cmake .. -DCMAKE_BUILD_TYPE=Release |
armeabi-v7a | 真机 32 位 | cmake .. -DANDROID_ABI=armeabi-v7a |
x86 | 模拟器 32 位 | cmake .. -DANDROID_ABI=x86 |
x86_64 | 模拟器 64 位(速度最快) | cmake .. -DANDROID_ABI=x86_64 |
关键教训:每次切换 ABI 必须彻底删除 build 目录,否则 CMake 缓存会导致交叉编译器选择错误(如误选 Visual Studio 的 MSVC)。
2.4 性能优化:从 14 秒到 380 毫秒
问题 1:Debug 导致速度偏慢
现象:真机运行耗时 14.6 秒,其中 DbNet 5.8 秒、CrnnNet 8 秒。
原因:未指定 CMAKE_BUILD_TYPE=Release,编译器默认使用 -O0(无优化)并生成调试符号。
解决:在 CMakeLists.txt 中强制启用 Release 优化:
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG" CACHE STRING "" FORCE)
set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG" CACHE STRING "" FORCE)
问题 2:模拟器 ARM 指令翻译极慢
现象:MuMu 模拟器上运行 arm64 版本耗时 7.9 秒,真机仅 380ms。
原因:
猜测模拟器通过软件模拟 ARM 指令,ncnn 的 NEON/SIMD 优化完全失效。二更,未找到实际原因,因为在模拟器上用安卓调用还是很快的,只用shell调用就很慢。三更,通过观察top得到猜测。安卓应用调用的开启了VULKAN,使用gpu加速,cpu使用率不高。而shell调用完全用cpu,使用率拉满。adb logcat | grep -iE "vulkan|gpu|icd|mali|adreno"使用这个命令确认了在使用shell调用时没用到vulkan,而安卓应用的方式使用了vulkan- 原因已找到,移步这篇文章在 Android 模拟器 Shell 下运行 ncnn 推理的性能排查记录编译C++程序后,在x86模拟器Shell中 - 掘金
最终性能对比
| 场景 | 耗时 | 备注 |
|---|---|---|
| 真机 aarch64 (Release) | ~380ms | 理想状态 |
| 模拟器 arm64 (指令模拟) | ~7.9s | 避免使用 |
2.5 常见编译错误与解决
| 错误 | 原因 | 解决 |
|---|---|---|
fatal error: 'net.h' file not found | ncnn 头文件路径未加入 | target_link_libraries(main ncnn) 自动传递 include 路径 |
undefined symbol: OcrLite::detect | 只编译了 main.cpp,遗漏其他源文件 | file(GLOB_RECURSE ALL_SOURCES src/*.cpp) 自动收集 |
incomplete type "std::__ndk1::ifstream" | 缺少 #include <fstream> | 添加头文件 |
more than one operator "-" matches | std::streampos 与 int 运算歧义 | 显式转换为 std::streamsize |
CANNOT LINK EXECUTABLE: libicu.so not found | x86_64 的 OpenCV 库引入了系统 harfbuzz 依赖 | 使用官方裁剪版 opencv-mobile |
| CMake 使用 Visual Studio 编译器 | 未指定 NDK toolchain 或 build 目录未清理 | 添加 -DCMAKE_TOOLCHAIN_FILE=... 并删除 build |
三、构建与运行
3.1 一键构建脚本(Windows)
创建 build.bat,支持菜单选择 ABI 和 Vulkan 选项:
@echo off
echo 1. arm64-v8a
echo 2. armeabi-v7a
echo 3. x86
echo 4. x86_64
set /p choice="Select (1-4): "
set NDK_PATH=C:\Users\...\ndk\28.2.13676358
set TOOLCHAIN=%NDK_PATH%\build\cmake\android.toolchain.cmake
rmdir /s /q build 2>nul
mkdir build && cd build
cmake .. ^
-G "Ninja" ^
-DCMAKE_TOOLCHAIN_FILE="%TOOLCHAIN%" ^
-DANDROID_ABI=%ABI% ^
-DANDROID_PLATFORM=android-30 ^
-DCMAKE_BUILD_TYPE=Release ^
-DOCR_LITE_VULKAN=OFF
cmake --build .
3.2 运行示例
# 推送可执行文件和资源
adb push build/main /sdcard/Download/
adb push model/ /sdcard/Download/model/
adb push test.raw /sdcard/Download/
# 执行 OCR
adb shell /sdcard/Download/main /sdcard/Download/test.raw --num-threads 4
# 输出示例
=== OCR Detection Options ===
num-threads : 4
box-score-thresh : 0.6
box-thresh : 0.3
unclip-ratio : 2.0
do-angle : false
most-angle : true
max-side-len : 1024
padding : 0
=============================
use time: 380.5
ocr result: 温馨提示
抵制不良游戏,拒绝盗版游戏
...
四、VSCode 开发环境配置
4.1 IntelliSense 配置
推荐使用 compile_commands.json 自动同步 CMake 构建参数,避免手动维护头文件路径:
{
"configurations": [
{
"name": "Android (compileCommands)",
"compileCommands": "${workspaceFolder}/build/compile_commands.json"
}
]
}
生成命令:
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Release -DANDROID_ABI=arm64-v8a
4.2 构建任务
在 .vscode/tasks.json 中定义多 ABI 构建任务,通过 Ctrl+Shift+B 快速切换。
五、关键代码改动清单
| 文件 | 改动内容 |
|---|---|
src/main.cpp | 新增标准 main 入口,命令行参数解析,图片读取 |
include/OcrLite.h / src/OcrLite.cpp | 移除 JNI 相关声明,日志改为 std::cout |
src/AngleNet.cpp | LOGE -> std::cout,可选关闭角度检测 |
src/CrnnNet.cpp | LOGE -> std::cout,去掉 softmax 加速识别 |
src/DbNet.cpp | 同理替换日志系统 |
CMakeLists.txt | 多 ABI 动态切换,Release 优化,静态链接标准库 |
build.bat | 一键多 ABI 编译脚本 |
六、踩坑总结(给初学者的建议)
- C++ 头文件必须显式包含:
std::ifstream需要<fstream>,否则报incomplete type。 - 动态内存管理:
cv::Mat借用外部指针时,必须clone()或确保数据生命周期覆盖 Mat 使用期。 - CMake 缓存很顽固:切换 ABI 或 Release/Debug 时,务必删除 build 目录重新配置。
- Release 不是默认的:不指定
-DCMAKE_BUILD_TYPE=Release就是 Debug,性能差 10 倍。 - 模拟器架构要匹配:在 x86 模拟器上跑 ARM 程序会触发指令翻译,慢到不可接受。
- 日志输出也是性能杀手:循环内的
std::cout同步 I/O 会显著拖慢推理,生产环境务必关闭。
七、参考与致谢
- chineseocr_lite - 原项目作者
- ncnn - 腾讯开源的高性能神经网络推理框架
- opencv-mobile - 轻量级 OpenCV 裁剪版
- Android NDK - 原生开发工具包
项目地址
https://github.com/Marcus737/chinese-ocr-cmd
免责声明:本文为个人学习记录,代码质量未经过生产环境验证。OCR 模型文件及字典版权归原项目所有,仅供学习研究使用。