AI應用的核心包括以下兩大塊:如何開發一個模型、以及如何将模型部署到項目進行應用。
現在有許多關于AI的教程,比如如何進行目标檢測、圖像分類、NLP以及建構聊天機器人等,反複強調相同的幾點:
- 首先,使用像飛槳這樣的深度學習平台開發模型。
- 然後,将模型打包到網頁Paddle.js、移動端Paddle Lite、單機Paddle Inference、或者伺服器Paddle Servering。
如何開發一個模型,無論是學術論文還是工業實踐,相關的詳細講解随處可見;而如何實作第二點的細節,相關的講解卻很少。
本文将為大家詳細解讀将模型內建到移動端應用的核心代碼。其他部署詳解後續會陸續推出,敬請期待哦!
內建流程
對所有模型來說,将模型內建到移動端應用的流程是相同的:
內建流程分兩大階段:- 模型訓練階段:主要解決模型訓練,利用标注資料訓練出對應的模型檔案。面向端側進行模型設計時,需要考慮模型大小和計算量。
- 模型部署階段:
-
- 模型轉換:如果是Caffe, TensorFlow或ONNX平台訓練的模型,需要使用X2Paddle工具将模型轉換到飛槳的格式。本次使用的ocr模型是使用Paddle平台訓練的模型,是以不需要進行轉換。
- (可選)模型壓縮:主要優化模型大小,借助PaddleSlim提供的剪枝、量化等手段降低模型大小,以便在端上使用。
- 将模型部署到Paddle Lite。
- 在終端上通過調用Paddle Lite提供的API接口(C++、Java、Python等API接口),完成推理相關的計算。
具體實作方法
移動端的AI應用開發具體實作,包含以下操作:
-
生成和優化模型。先經過模型訓練得到Paddle模型,該模型不能直接用于Paddle Lite部署,需先通過Paddle Lite的opt離線優化工具優化,然後得到Paddle Lite nb模型。如果是Caffe, TensorFlow或ONNX平台訓練的模型,需要使用X2Paddle工具将模型轉換到Paddle模型格式,再使用opt優化。X2Paddle使用方法:https://paddle-lite.readthedocs.io/zh/latest/user_guides/x2paddle.html
opt工具使用方法:https://paddle-lite.readthedocs.io/zh/latest/user_guides/model_optimize_tool.html
- 擷取Paddle Lite推理庫。Paddle Lite新版本釋出時已提供預編譯庫,是以無需進行手動編譯,直接下載下傳編譯好的推理庫檔案即可。
- 建構推理程式。使用前續步驟中編譯出來的推理庫、優化後模型檔案,首先經過模型初始化,配置模型位置、線程數等參數,然後進行圖像預處理,如圖形轉換、歸一化等處理,處理好以後就可以将資料輸入到模型中執行推理計算,并獲得推理結果。
Paddle Lite庫可以通過飛槳下載下傳,連結:
https://paddle-lite.readthedocs.io/zh/latest/user_guides/release_lib.html。
模型檔案
模型檔案assets包含了兩個深度學習模型,圖檔作為輸入,同時将模型導入Paddle Lite中,輸出即為檢測的結果,模型的作用如下:
1. ch_det_mv3_db_opt.nb:文字檢測的模型,輸入為圖像,輸出為文字的區域坐标
2. ch_rec_mv3_crnn_opt.nb:文字識别的模型,輸入的文字檢測的結果,輸出為文字識别結果
OCR的過程其實是兩個模型的串行工作過程,将文字檢測模型的輸出結果作為文字識别模型的輸入,最後輸出最終的結果。
這兩個模型,可以通過PaddleOCR github下載下傳:
優化前的模型下載下傳連結:https://github.com/PaddlePaddle/PaddleOCR/blob/develop/README_cn.md#%E4%B8%AD%E6%96%87ocr%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8
opt優化後的模型連結:https://github.com/PaddlePaddle/PaddleOCR/blob/develop/deploy/lite/readme.md#21-%E6%A8%A1%E5%9E%8B%E4%BC%98%E5%8C%96
推理程式代碼解讀
推理程式代碼目錄結構:
|-app # 程式module的主目錄
|-build # app子產品編譯輸出的檔案(包括最終生成的apk)
|-libs # 依賴庫
|-OpenCV # OpenCV庫
|-PaddleLite # PaddleLite庫,用于調用模型進行推理預測
|-src # app應用的源代碼目錄
|-src/main/assets # 模型檔案、測試圖檔
|-src/main/cpp # (C++源代碼方式)C++ 程式代碼目錄
|-src/main/java # java程式代碼目錄
|-src/main/jniLibs # (so方式)與cpp 目錄的操作二選一
|-src/main/res #存放app中顯示的圖形、文本、聲音等一些資源檔案
|-src/main/res/drawable # 各種位圖檔案(.png、.jpg等)和drawable類型的XML檔案
|-src/main/res/ # 布局檔案
|-src/main/AndroidManifest.xml # 項目的清單檔案(名稱、版本、SDK、權限等配置資訊)
|-build.gradle # 項目的gradle編譯檔案
其中,src是主要源代碼目錄,下文詳細逐一介紹。
01
C++ 程式代碼目錄(JNI調用C++自定義類)C++(cpp)程式代碼是移動端app的核心算法代碼。C++程式代碼的作用:向下調用OpenCV庫和Paddle Lite庫中的函數,來實作模型的推理預測功能(底層實作);向上提供接口給上層的功能應用層的java程式調用。
C++代碼目錄如下:
|-app/src/main/cpp
|-CMakeLists.txt # 重新編譯C++的源代碼和庫,生成能被本項目中的C++的程式所使用的庫
|- common.h # 常量定義和日志函數
|- native.cpp # 和java層互動的c++函數
|- native.h # jni的封裝函數
|- ocr_clipper.cpp # 檢測模型DB後處理用到的第三方庫
|- ocr_clipper.hpp
|- ocr_crnn_process.cpp # 識别模型CRNN預處理函數, 擷取OpenCV的Mat圖檔後再放到preprocess做DB模型的預處理
|- ocr_crnn_process.h
|- ocr_db_post_process.cpp #檢測模型DB後處理函數
|- ocr_db_post_process.h
|- ocr_predictor.cpp # OCR 模型預測函數
|- ocr_predictor.h
|- ppredictor.cpp # 準備模型預測所需要的初始化,加載模型,從網絡結果中擷取輸出等步驟
|- ppredictor.h
|- predictor_input.cpp # 輸入資料
|- predictor_input.h
|- predictor_output.cpp # 擷取預測結果的輸出結果資訊
|- predictor_output.h
|- preprocess.cpp # 圖檔預處理函數,用于檢測模型DB
|- preprocess.h
具體推理步驟如下所示: 1. 檢測預處理
2. 檢測模型預測-> 得到預測結果-> 檢測後處理-> 獲得檢測的文本框
3. 根據檢測文本框,從原圖中把檢測到的文本行剪切出來;
4. 将每個剪切出來的文本行,輸入給識别網絡預處理
5. 識别網絡預處理後,輸入給識别網絡預測
6. 識别網絡預測結果解析得到預測文本
代碼包括四個部分:1. 檢測模型預處理,後處理;
|- preprocess.cpp 識别模型CRNN預處理函數
|- preprocess.h
|- ocr_db_post_process.cpp 檢測模型DB後處理函數
|- ocr_db_post_process.h
2. 識别模型預處理
|- ocr_crnn_process.cpp 識别模型CRNN模型的預處理,結果是OpenCv的Mat,然後再放到preprocess.cpp做圖檔的預處理
|- ocr_crnn_process.h
3. 模型預測
|- ocr_predictor.cpp OCR 模型預測函數
|- ocr_predictor.h
4. 模型預測準備,包括模型初始化,給輸入資料配置設定記憶體等。
|- ppredictor.cpp 準備模型預測所需要的初始化,加載模型,從網絡結果中擷取輸出等步驟
|- ppredictor.h
|- predictor_input.cpp 輸入資料配置設定記憶體
|- predictor_input.h
|- predictor_output.cpp 擷取預測結果的輸出結果資訊
|- predictor_output.h
其中,
模型ch_det_mv3_db_opt.nb:
- 預處理在Predictor.java中完成
- 推理在ocr_ppredictor.cpp中的_det_predictor
- 後處理在ocr_db_post_process.cpp裡
:
- 根據ocr_db_post_process.cpp結果,在ocr_crnn_process摳出多張含有文字的小圖
- 對每張小圖進行預處理,在preprocess.cpp裡完成
- 對每張小圖進行推理在ocr_ppredictor.cpp中的_rec_predictor。
- 所有小圖的結果,序列化成float,傳輸到java層
- 在OCRPredictorNative.java 解析成最終結果
核心預測代碼:
https://github.com/PaddlePaddle/PaddleOCR/blob/7b201a385547152a015c27dc3d17e9c3ae12a5fb/deploy/android_demo/app/src/main/cpp/ocr_ppredictor.cpp#L67
std::vector<OCRPredictResult>
OCR_PPredictor::infer_ocr(const std::vector<int64_t> &dims, const float *input_data, int input_len,
int net_flag, cv::Mat &origin) {
// _det_predictor:檢測預測網絡
// 擷取輸出資料,并轉換為網絡預測支援的資料格式;
PredictorInput input = _det_predictor->get_first_input();
input.set_dims(dims);
input.set_data(input_data, input_len);
// 執行預測,得到檢測網絡預測的文本框
std::vector<PredictorOutput> results = _det_predictor->infer();
PredictorOutput &res = results.at(0);
// 對文本框做簡單的過濾
std::vector<std::vector<std::vector<int>>> filtered_box
= calc_filtered_boxes(res.get_float_data(), res.get_size(), (int) dims[2], (int) dims[3],
origin);
LOGI("Filter_box size %ld", filtered_box.size());
// 執行識别模型預測,并直接傳回識别模型預測結果
return infer_rec(filtered_box, origin);
}
std::vector<OCRPredictResult>
OCR_PPredictor::infer_rec(const std::vector<std::vector<std::vector<int>>> &boxes,
const cv::Mat &origin_img) {
//識别模型預處理參數
std::vector<float> mean = {0.5f, 0.5f, 0.5f};
std::vector<float> scale = {1 / 0.5f, 1 / 0.5f, 1 / 0.5f};
std::vector<int64_t> dims = {1, 3, 0, 0};
std::vector<OCRPredictResult> ocr_results;
PredictorInput input = _rec_predictor->get_first_input();
// 通過for訓練,每次讀取檢測模型預測的檢測框
for (auto bp = boxes.crbegin(); bp != boxes.crend(); ++bp) {
const std::vector<std::vector<int>> &box = *bp;
// 根據檢測框将檢測到的文本行剪切出來
cv::Mat crop_img = get_rotate_crop_image(origin_img, box);
float wh_ratio = float(crop_img.cols) / float(crop_img.rows);
// 識别模型預處理
cv::Mat input_image = crnn_resize_img(crop_img, wh_ratio);
input_image.convertTo(input_image, CV_32FC3, 1 / 255.0f);
const float *dimg = reinterpret_cast<const float *>(input_image.data);
int input_size = input_image.rows * input_image.cols;
dims[2] = input_image.rows;
dims[3] = input_image.cols;
input.set_dims(dims);
neon_mean_scale(dimg, input.get_mutable_float_data(), input_size, mean, scale);
// 執行識别模型預測
std::vector<PredictorOutput> results = _rec_predictor->infer();
OCRPredictResult res;
// 解析識别模型預測結果,得到預測的字的索引
res.word_index = postprocess_rec_word_index(results.at(0));
if (res.word_index.empty()) {
continue;
}
// 計算預測的文本行的置信度
res.score = postprocess_rec_score(results.at(1));
res.points = box;
ocr_results.emplace_back(std::move(res));
}
LOGI("ocr_results finished %lu", ocr_results.size());
// 傳回識别結果
return ocr_results;
}
02
java程式代碼目錄Java程式屬于上層功能應用的開發,主要工作是調用函數的接口:
- 讀取相冊中的圖像
- 建立Paddle Lite的預測對象Predictor
- 将模型檔案和圖像送入Predictor中進行推理預測
- 預測的結果送入OcrResultModel中,在輸出的圖像上繪制預測結果,并在APP上顯示
修改MiniActivity.java中的代碼:predictor.init( ):Predictor的初始化,配置一些預測的參數(輸入的尺寸、模型的路徑等);predictor.process( ):進行推理預測。
MiniActivity是入口的java檔案,相當于是APP的主函數、程式入口,其他.java檔案被它調用activity_mini.xml是MiniActivity對應的UI布局,是APP控件開發,定義了APP的各個控件的布局
本項目增加了三個控件:(控件在.java檔案中的調用通過其ID,類似于變量名)。
03
jniLibs(so方式內建C++代碼)C++的檔案,最終都會編譯成so檔案,然後同java編譯dex檔案,一起打包成apk檔案。
我們也可以直接使用apk檔案裡編譯好的so檔案。
示例中的方式是從官方demo的apk檔案裡提取的so檔案。
04
build.gradleapp目錄下的build.gradle檔案用來配置對應的APP。需要設定compileSdkVersion和targetSdkVersion的版本與前面軟體中配置的SDK相同。同時,添加abiFilters 'armeabi-v7a', 'arm64-v8a'指定編譯的平台,如果不指定就會預設編譯出所有平台的目标檔案,而我們的庫隻支援了arm-v7和arm-v8,運作時可能會報錯。
根目錄也就是Project下的build.gradle檔案用來配置整個Project,本次項目不需要修改。
補充說明1. 橙色的檔案夾都是build編譯生成的目标檔案(不用手動編輯)
2. libs是存放靜态庫或者動态庫(不用修改)
3. src/main/裡的java和cpp檔案夾存放app運作的源代碼,包括Java和C++的代碼(上層的應用開發使用Java,底層的具體實作使用C++,此項目中兩者都要開發)。
4. OpenCV庫可以通過OpenCV官網下載下傳,連結:https://opencv.org/releases/,本次用的是4.2 android。
親自實踐一把!看到這裡,是不是覺得開發一個移動端AI應用也沒那麼難呢?飛槳提供了很多的開源模型,有興趣的朋友可以參考本教程,發揮自己的想象力,開發更多有趣、有意義的應用哈。