備上跑 ppocr-4:文字旋轉(zhuǎn)識別 + opencv-mobile 中文顯示,mnn/ncnn 雙版本配置骨架)
1. 斑馬手持終端上跑 ppocr-4旋轉(zhuǎn)文字識別與中文顯示到底難在哪在斑馬這類 Android 手持終端上做 OCR和服務(wù)器端完全是兩碼事。設(shè)備算力有限、內(nèi)存緊張、屏幕小還要在工業(yè)場景里識別貼歪的標(biāo)簽、豎排的貨架牌、旋轉(zhuǎn)過的單據(jù)。ppocr-4 相比 v3 在檢測和識別精度上有提升但直接把 Paddle 模型搬到端側(cè)會遇到三個繞不開的問題模型格式要轉(zhuǎn)成 mnn 或 ncnn 才能高效推理文字方向分類cls模塊很多開源端側(cè)框架默認(rèn)沒接識別出來的中文要用 opencv-mobile 畫到圖上而 opencv-mobile 默認(rèn)不帶 freetype 中文渲染。這篇就圍繞這三個問題展開。目標(biāo)很明確同一套 C 代碼骨架通過配置切換 mnn 和 ncnn 兩個推理后端跑通 ppocr-4 的檢測、方向分類、識別三段流程并且用 opencv-mobile 把中文結(jié)果正確渲染出來。適合已經(jīng)在做端側(cè) OCR、手里有斑馬或類似 Android 工業(yè)手持設(shè)備的開發(fā)者。下面給的 config.toml 和 settings.json 骨架可以直接抄模型路徑按你的實際目錄改。先說清楚整體鏈路。ppocr-4 端側(cè)推理分三步det 檢測文本框cls 判斷每個框是否旋轉(zhuǎn) 180 度rec 識別文字內(nèi)容。很多端側(cè) demo 只做了 detrec遇到倒置文字就識別成亂碼cls 必須補上。推理后端 mnn 和 ncnn 各有優(yōu)劣mnn 對 Android 更友好、算子覆蓋全ncnn 體積小、Vulkan 加速成熟。用配置驅(qū)動切換代碼里只保留一套前后處理邏輯后端差異封裝在推理接口層。2. 前置準(zhǔn)備模型轉(zhuǎn)換、opencv-mobile 與 TaoToken 接入模型轉(zhuǎn)換是第一步。ppocr-4 的 det、cls、rec 三個模型都要從 Paddle 格式轉(zhuǎn)出來。det 和 cls 轉(zhuǎn) mnn/ncnn 比較直接rec 因為帶 CRNN 結(jié)構(gòu)轉(zhuǎn)換時注意輸入輸出節(jié)點名稱。轉(zhuǎn)換腳本網(wǎng)上有現(xiàn)成的核心是保證輸入 shape 和歸一化參數(shù)一致。轉(zhuǎn)完后你會得到類似 det.mnn、cls.mnn、rec.mnn 和對應(yīng)的 .param/.binncnn。opencv-mobile 用會長維護(hù)的版本Android 端直接引 sdk/native/jni 即可。中文渲染這塊opencv-mobile 默認(rèn)的 putText 不支持中文需要自己接 freetype 或者用預(yù)渲染字庫。我采用的是 freetype 方案把中文字體文件放進(jìn) assets初始化時加載渲染時按 UTF-8 逐字繪制。推理后端和模型管理這塊如果你不想在每臺設(shè)備上手動同步模型文件可以用 TaoToken 做統(tǒng)一的模型分發(fā)和 API 調(diào)用管理。它的控制臺可以管理多個項目的密鑰模型對話接口適合做識別結(jié)果的二次校驗。接入文檔在 https://taotoken.net/api API Keys 在 https://taotoken.net/console/api-keys 申請。對于需要長期跑編碼和 Agent 任務(wù)的場景Coding Plan 更劃算入口在 https://taotoken.net/coding-plan 。模型對話調(diào)試可以直接用 https://taotoken.net/model-chat 。注意TaoToken 在這里的角色是模型分發(fā)和 API 管理不是推理后端。端側(cè)推理仍然在設(shè)備本地用 mnn/ncnn 完成TaoToken 負(fù)責(zé)的是模型版本同步和云端校驗接口。3. 可復(fù)制配置config.toml 與 settings.json 雙版本骨架配置分兩層。config.toml 管推理后端和模型路徑settings.json 管預(yù)處理參數(shù)和渲染選項。這樣切換 mnn/ncnn 只改一個字段不用動代碼。先看 config.toml[engine] # 可選值: mnn 或 ncnn backend mnn num_threads 4 use_vulkan false [model.det] path models/ppocr4/det.mnn input_shape [1, 3, 640, 640] mean [0.485, 0.456, 0.406] std [0.229, 0.224, 0.225] thresh 0.3 box_thresh 0.6 unclip_ratio 1.5 [model.cls] path models/ppocr4/cls.mnn input_shape [1, 3, 192, 48] mean [0.5, 0.5, 0.5] std [0.5, 0.5, 0.5] cls_thresh 0.9 [model.rec] path models/ppocr4/rec.mnn input_shape [1, 3, 48, 320] mean [0.5, 0.5, 0.5] std [0.5, 0.5, 0.5] char_dict models/ppocr4/ppocr_keys_v1.txt [render] font_path fonts/simhei.ttf font_size 24 text_color [0, 255, 0] box_color [255, 0, 0]ncnn 版本只需要改 backend 和模型后綴[engine] backend ncnn num_threads 4 use_vulkan true [model.det] path models/ppocr4/det.param weights models/ppocr4/det.bin # 其余參數(shù)與 mnn 版本一致再看 settings.json管的是運行時行為和自檢開關(guān){ preprocess: { det_limit_side_len: 640, rec_batch_num: 6, use_rotate_detection: true }, cls: { enable: true, rotate_threshold: 0.9, angles: [0, 180] }, render: { draw_box: true, draw_text: true, chinese_support: true, font_cache_size: 128 }, debug: { save_intermediate: false, log_level: info, verify_rotation: true } }關(guān)鍵字段說明use_rotate_detection控制是否啟用 cls 分支angles定義分類角度ppocr-4 的 cls 模型輸出 0 和 180 兩類。verify_rotation打開后會在日志里打印每個框的旋轉(zhuǎn)判定結(jié)果方便自檢。chinese_support打開后渲染走 freetype 路徑關(guān)閉則退回 opencv-mobile 原生 putText。C 側(cè)讀取配置的代碼骨架#include toml.hpp #include nlohmann/json.hpp struct EngineConfig { std::string backend; int num_threads; bool use_vulkan; }; EngineConfig loadConfig(const std::string toml_path) { auto data toml::parse(toml_path); EngineConfig cfg; cfg.backend toml::findstd::string(data, engine, backend); cfg.num_threads toml::findint(data, engine, num_threads); cfg.use_vulkan toml::findbool(data, engine, use_vulkan); return cfg; }推理接口層用工廠模式根據(jù) backend 字段返回 mnn 或 ncnn 的實現(xiàn)class InferenceEngine { public: virtual ~InferenceEngine() default; virtual bool loadModel(const std::string path) 0; virtual std::vectorfloat forward(const std::vectorfloat input, const std::vectorint shape) 0; }; std::unique_ptrInferenceEngine createEngine(const EngineConfig cfg) { if (cfg.backend mnn) { return std::make_uniqueMnnEngine(cfg.num_threads); } else if (cfg.backend ncnn) { return std::make_uniqueNcnnEngine(cfg.num_threads, cfg.use_vulkan); } return nullptr; }這樣 det、cls、rec 三個模型都通過同一個接口加載和推理前后處理代碼完全復(fù)用。4. 驗證請求與成功結(jié)果旋轉(zhuǎn)角度自檢 中文渲染自檢配置寫好后先做兩個自檢動作確認(rèn) cls 和中文渲染都正常工作。旋轉(zhuǎn)角度自檢準(zhǔn)備一張包含正置和倒置文字的測試圖跑一遍完整流程打開verify_rotation后日志會輸出每個檢測框的 cls 得分和判定角度。預(yù)期結(jié)果是倒置文字的框被判定為 180 度并自動旋轉(zhuǎn)回來識別結(jié)果正確。如果 cls 得分低于閾值檢查 cls 模型的輸入歸一化參數(shù)是否和訓(xùn)練時一致。void verifyRotation(const std::vectorTextBox boxes, const std::vectorfloat cls_scores) { for (size_t i 0; i boxes.size(); i) { float score cls_scores[i]; int angle score 0.9f ? 180 : 0; LOGI(Box %zu: cls_score%.4f, angle%d, i, score, angle); if (angle 180) { rotateBox180(boxes[i]); } } }中文渲染自檢加載字體后在空白圖上繪制一段中文保存成 png用設(shè)備或電腦打開確認(rèn)沒有亂碼和方框。freetype 渲染的核心是按 UTF-8 解碼后逐字獲取 glyphvoid drawChineseText(cv::Mat img, const std::string text, cv::Point org, cv::Scalar color, int font_size) { FT_Library ft; FT_Init_FreeType(ft); FT_Face face; FT_New_Face(ft, fonts/simhei.ttf, 0, face); FT_Set_Pixel_Sizes(face, 0, font_size); int x org.x; for (size_t i 0; i text.size();) { unsigned long code decodeUTF8(text, i); FT_Load_Char(face, code, FT_LOAD_RENDER); FT_GlyphSlot g face-glyph; for (int row 0; row g-bitmap.rows; row) { for (int col 0; col g-bitmap.width; col) { int px x g-bitmap_left col; int py org.y - g-bitmap_top row; if (px 0 px img.cols py 0 py img.rows) { img.atcv::Vec3b(py, px) cv::Vec3b(color[0], color[1], color[2]); } } } x g-advance.x 6; } FT_Done_Face(face); FT_Done_FreeType(ft); }成功結(jié)果應(yīng)該是測試圖上所有文字框被正確繪制倒置文字識別結(jié)果正確中文顯示無亂碼。如果 mnn 和 ncnn 兩個后端跑出來的識別結(jié)果一致說明配置切換邏輯沒問題。5. 本篇常見錯排查cls 模型加載失敗或輸出維度不對。ppocr-4 的 cls 模型輸出是 2 類有些轉(zhuǎn)換腳本會輸出成單類。檢查轉(zhuǎn)換時的輸出節(jié)點確保 shape 是 [1, 2]。mnn 版本用 Netron 打開看輸出層ncnn 版本看 .param 最后一層。中文渲染出現(xiàn)方框或亂碼。三個原因字體文件沒打包進(jìn) assets、UTF-8 解碼函數(shù)寫錯、freetype 沒鏈接。先確認(rèn)字體路徑在設(shè)備上可讀再檢查 decodeUTF8 對多字節(jié)字符的處理。opencv-mobile 本身不帶 freetype需要在 CMakeLists 里顯式鏈接。mnn 和 ncnn 結(jié)果不一致。常見于 rec 模型的輸入寬度不同。mnn 版本可能默認(rèn) 320ncnn 版本 640導(dǎo)致識別結(jié)果有差異。統(tǒng)一 input_shape 后重新轉(zhuǎn)換。另外 ncnn 開 Vulkan 后浮點精度略有差異對識別結(jié)果影響很小但如果 cls 得分卡在閾值附近建議關(guān)掉 Vulkan 對比。旋轉(zhuǎn)檢測誤判。cls 閾值設(shè)太低會把正置文字判成 180 度。默認(rèn) 0.9 比較穩(wěn)如果場景里文字方向單一可以調(diào)到 0.95。另外 det 檢測框如果本身是斜的cls 只處理 180 度旋轉(zhuǎn)斜框需要額外的角度回歸ppocr-4 的 det 不直接輸出角度這塊要自己加。CMake 鏈接順序問題。ncnn 和 opencv-mobile 同時鏈接時注意 ncnn 放在 opencv 前面否則會出現(xiàn)符號沖突。Android 端還要加-static-openmp否則 OpenMP 運行時可能找不到。6. 接入與后續(xù)模型分發(fā)和編碼任務(wù)的分流建議端側(cè)推理跑通后模型版本管理和結(jié)果校驗可以接到 TaoToken 上。API Keys 在 https://taotoken.net/console/api-keys 申請接入文檔在 https://taotoken.net/api 。識別結(jié)果需要二次校驗時用模型對話接口 https://taotoken.net/model-chat 做語義糾錯。如果你后續(xù)要做長期的編碼和 Agent 任務(wù)Coding Plan 的入口在 https://taotoken.net/coding-plan 控制臺在 https://taotoken.net/console 。整套代碼骨架的核心思路就是配置驅(qū)動加接口抽象mnn 和 ncnn 的差異被隔離在引擎實現(xiàn)層det/cls/rec 的前后處理完全共享。cls 補上后旋轉(zhuǎn)文字識別就完整了opencv-mobile 接 freetype 解決中文顯示。兩個自檢動作建議每次換模型或換設(shè)備都跑一遍能省掉大量排查時間。