天天看點

c#和java部署pytorch同僚識别兩個圖檔_手把手教你移動端AI應用開發(三)——部署環節關鍵代碼最詳解讀...

AI應用的核心包括以下兩大塊:如何開發一個模型、以及如何将模型部署到項目進行應用。

現在有許多關于AI的教程,比如如何進行目标檢測、圖像分類、NLP以及建構聊天機器人等,反複強調相同的幾點:

  1. 首先,使用像飛槳這樣的深度學習平台開發模型。
  2. 然後,将模型打包到網頁Paddle.js、移動端Paddle Lite、單機Paddle Inference、或者伺服器Paddle Servering。

如何開發一個模型,無論是學術論文還是工業實踐,相關的詳細講解随處可見;而如何實作第二點的細節,相關的講解卻很少。

本文将為大家詳細解讀将模型內建到移動端應用的核心代碼。其他部署詳解後續會陸續推出,敬請期待哦!

內建流程

對所有模型來說,将模型內建到移動端應用的流程是相同的:

c#和java部署pytorch同僚識别兩個圖檔_手把手教你移動端AI應用開發(三)——部署環節關鍵代碼最詳解讀...

內建流程分兩大階段:
  1. 模型訓練階段:主要解決模型訓練,利用标注資料訓練出對應的模型檔案。面向端側進行模型設計時,需要考慮模型大小和計算量。
  2. 模型部署階段:
    • 模型轉換:如果是Caffe, TensorFlow或ONNX平台訓練的模型,需要使用X2Paddle工具将模型轉換到飛槳的格式。本次使用的ocr模型是使用Paddle平台訓練的模型,是以不需要進行轉換。
    • (可選)模型壓縮:主要優化模型大小,借助PaddleSlim提供的剪枝、量化等手段降低模型大小,以便在端上使用。
    • 将模型部署到Paddle Lite。
    • 在終端上通過調用Paddle Lite提供的API接口(C++、Java、Python等API接口),完成推理相關的計算。

具體實作方法

移動端的AI應用開發具體實作,包含以下操作:

c#和java部署pytorch同僚識别兩個圖檔_手把手教你移動端AI應用開發(三)——部署環節關鍵代碼最詳解讀...

  1. 生成和優化模型。先經過模型訓練得到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

  2. 擷取Paddle Lite推理庫。Paddle Lite新版本釋出時已提供預編譯庫,是以無需進行手動編譯,直接下載下傳編譯好的推理庫檔案即可。
  3. 建構推理程式。使用前續步驟中編譯出來的推理庫、優化後模型檔案,首先經過模型初始化,配置模型位置、線程數等參數,然後進行圖像預處理,如圖形轉換、歸一化等處理,處理好以後就可以将資料輸入到模型中執行推理計算,并獲得推理結果。
Paddle Lite預測庫

Paddle Lite庫可以通過飛槳下載下傳,連結:

https://paddle-lite.readthedocs.io/zh/latest/user_guides/release_lib.html。

c#和java部署pytorch同僚識别兩個圖檔_手把手教你移動端AI應用開發(三)——部署環節關鍵代碼最詳解讀...

模型檔案

c#和java部署pytorch同僚識别兩個圖檔_手把手教你移動端AI應用開發(三)——部署環節關鍵代碼最詳解讀...

模型檔案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

  1. 預處理在Predictor.java中完成
  2. 推理在ocr_ppredictor.cpp中的_det_predictor
  3. 後處理在ocr_db_post_process.cpp裡
模型ch_rec_mv3_crnn_opt.nb

  1. 根據ocr_db_post_process.cpp結果,在ocr_crnn_process摳出多張含有文字的小圖
  2. 對每張小圖進行預處理,在preprocess.cpp裡完成
  3. 對每張小圖進行推理在ocr_ppredictor.cpp中的_rec_predictor。
  4. 所有小圖的結果,序列化成float,傳輸到java層
  5. 在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,類似于變量名)。

c#和java部署pytorch同僚識别兩個圖檔_手把手教你移動端AI應用開發(三)——部署環節關鍵代碼最詳解讀...

03

jniLibs(so方式內建C++代碼)

C++的檔案,最終都會編譯成so檔案,然後同java編譯dex檔案,一起打包成apk檔案。

我們也可以直接使用apk檔案裡編譯好的so檔案。

示例中的方式是從官方demo的apk檔案裡提取的so檔案。

c#和java部署pytorch同僚識别兩個圖檔_手把手教你移動端AI應用開發(三)——部署環節關鍵代碼最詳解讀...

04

build.gradle

app目錄下的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應用也沒那麼難呢?飛槳提供了很多的開源模型,有興趣的朋友可以參考本教程,發揮自己的想象力,開發更多有趣、有意義的應用哈。

繼續閱讀