ChineseOCR Lite Android 纯C++版 - 初学者学习项目

23 阅读7分钟

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 格式的命令行选项
    // ...
}

关键改动

  • 移除所有 JNIEXPORTJNIEnv*jobject 等 JNI 类型
  • bitmapToMat 替换为自定义的 readImg 函数,读取自定义 .raw 格式图片
  • 日志系统从 Android 的 LOGE/LOGI 替换为 std::cout

2.2 自定义图片输入格式

由于去掉了 Java 层的 Bitmap 解码,需要自行处理图片输入。采用简单的二进制 .raw 格式:

字节偏移长度内容
0-34 bytes宽度 (int32, 小端序)
4-74 bytes高度 (int32, 小端序)
8-158 bytes保留字段(跳过)
16+widthheight4RGBA 像素数据

读取实现(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 foundncnn 头文件路径未加入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 "-" matchesstd::streamposint 运算歧义显式转换为 std::streamsize
CANNOT LINK EXECUTABLE: libicu.so not foundx86_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.cppLOGE -> std::cout,可选关闭角度检测
src/CrnnNet.cppLOGE -> std::cout去掉 softmax 加速识别
src/DbNet.cpp同理替换日志系统
CMakeLists.txt多 ABI 动态切换,Release 优化,静态链接标准库
build.bat一键多 ABI 编译脚本

六、踩坑总结(给初学者的建议)

  1. C++ 头文件必须显式包含std::ifstream 需要 <fstream>,否则报 incomplete type
  2. 动态内存管理cv::Mat 借用外部指针时,必须 clone() 或确保数据生命周期覆盖 Mat 使用期。
  3. CMake 缓存很顽固:切换 ABI 或 Release/Debug 时,务必删除 build 目录重新配置。
  4. Release 不是默认的:不指定 -DCMAKE_BUILD_TYPE=Release 就是 Debug,性能差 10 倍。
  5. 模拟器架构要匹配:在 x86 模拟器上跑 ARM 程序会触发指令翻译,慢到不可接受。
  6. 日志输出也是性能杀手:循环内的 std::cout 同步 I/O 会显著拖慢推理,生产环境务必关闭。

七、参考与致谢


项目地址

https://github.com/Marcus737/chinese-ocr-cmd

免责声明:本文为个人学习记录,代码质量未经过生产环境验证。OCR 模型文件及字典版权归原项目所有,仅供学习研究使用。