C++到Python全搞定,教你如何為FastDeploy貢獻程式碼

語言: CN / TW / HK

大家好!今天為大家帶來的是一篇經驗帖文。本次分享的主人公是黑客鬆比賽參賽者鄭必城,他將為大家帶來比賽專案“No.80瑞芯微RK3588:通過Paddle2ONNX打通5個飛槳模型的部署中如何為FastDeploy”任務中的一些心得體會,快來看看他是如何為FastDeploy貢獻程式碼的吧!


RKNPU2是瑞芯微Rockchip推出的針對RK356X/RK3588/RV1103/RV1106的C++推理工具。在參加黑客鬆比賽時,FastDeploy倉庫[1]還沒有整合RKNPU2的引擎。開發者需要使用RKNPU2從頭編寫程式碼。在參加完黑客鬆之後,我為FastDeploy倉庫貢獻了RKNPU2的後端推理引擎的程式碼,現在能直接使用FastDeploy快速開發基於RKNPU2的程式碼。本次教程將以貢獻SCRFD模型[2]為例,教你如何給FastDeploy貢獻程式碼。

FastDeploy簡介

FastDeploy是一款全場景、易用靈活、極致高效的AI推理部署工具,提供開箱即用的雲邊端部署體驗,支援超過150+文字、計算機視覺、語音和跨模態模型,並實現端到端的推理效能優化。其應用於影象分類、物體檢測、影象分割、人臉檢測、人臉識別、關鍵點檢測、摳圖、OCR、NLP、TTS等任務,滿足開發者多場景、多硬體、多平臺的產業部署需求。同時,FastDeploy集成了多種後端推理引擎,其中就包括RKNPU2。開發者能夠快速基於現有的模型以及後端來進行開發。

很多開發者可能會有疑惑,為什麼Rockchip提供了RKNPU2rknn-toolkit2這兩個分別面向C++和Python的推理引擎,我們還要使用FastDeploy進行開發呢?簡單來說,RKNPU2和rknn-toolkit2是推理引擎,它們側重於推理;FastDeploy是推理部署工具側重於部署。給RKNPU2輸入一張圖片,會得到一串數字。給FastDeploy輸入一張圖片,會直接得到經過後處理後的圖片。這樣就能大大減少開發者在專案落地過程中的一些困難。

貢獻步驟

給FastDeploy貢獻程式碼,我一般按以下步驟進行,當然你可以根據自己的能力制定自己的開發步驟。

圖片.png

由上圖所示,給FastDeploy貢獻程式碼的步驟一般為編寫C++程式碼、編寫C++ example、編寫Python程式碼、編寫Python example程式碼、編寫文件、提交PR。

貢獻程式碼指南

下面我以貢獻SCRFD模型為例子,給大家詳細介紹每個貢獻環節中的注意事項。

轉換模型

不管你是在FastDeploy上開發C++還是Python的程式碼,轉換模型都是你首先需要完成的任務。通常情況下,轉換模型的工具一般使用rknn-toolkit2,但是這個工具API比較多,用起來較為複雜。為了讓大家能夠更快速的轉換模型,在FastDeploy中,我已經編寫了轉換模型的程式碼並且提供了詳細的文件。詳情請檢視FastDeploy RKNPU2模型轉換文件。這裡為了縮短篇幅,直接給出模型轉換的配置檔案以及模型轉換的文件。大家可以參考這幾個文件轉換自己的模型。

編寫C++程式碼

上文提到,SCRFD的C++程式碼需要在fastdeploy/vision/facedet/contrib這個目錄下編寫,因此我建立了scrfd.hscrfd.cc這兩個檔案,實現模型具體程式碼。這裡要注意與常見的檔案命名形式不同,scrfd.cc這個C++程式碼檔案的字尾不是 .cpp而是 .cc ,如果scrfd.cc改為scrfd.cpp將無法成功編譯!

  • 編寫scrfd.h

scrfd.h裡定義了SCRFD模型的一些基本引數以及需要重定義的函式。其中定義的SCRFD模型需要繼承FastDeployModel這個公共的模型類,為的是繼承FastDeploy的一些公共特性。如下面的程式碼所示,在標頭檔案中,我們需要重寫FastDeployModel中的以下幾個函式,包括Initialize、Preprocess、Postprocess、Predict、ModelName。分別對應初始化、預處理、後處理、預測、模型名稱。如果你需要完整詳細的程式碼,請訪問下方連結。

#pragma once
#include <unordered_map>
#include "fastdeploy/fastdeploy_model.h"
#include "fastdeploy/vision/common/processors/transform.h"
#include "fastdeploy/vision/common/result.h"
namespace fastdeploy {
namespace vision {
namespace facedet {
class FASTDEPLOY_DECL SCRFD : public FastDeployModel {
 public:
  SCRFD(const std::string& model_file, const std::string& params_file = "",
        const RuntimeOption& custom_option = RuntimeOption(), const ModelFormat& model_format = ModelFormat::ONNX);
  std::string ModelName() const { return "scrfd"; }
  virtual bool Predict(cv::Mat* im, FaceDetectionResult* result, float conf_threshold = 0.25f, float nms_iou_threshold = 0.4f);
 private:
  bool Initialize();
  bool Preprocess(Mat* mat, FDTensor* output, std::map<std::string, std::array<float, 2>>* im_info);
  bool Postprocess(std::vector<FDTensor>& infer_result, FaceDetectionResult* result, const std::map<std::string, std::array<float, 2>>& im_info, float conf_threshold, float nms_iou_threshold);
};
}  // namespace facedet
}  // namespace vision
}  // namespace fastdeploy
  • 編寫scrfd.cc

scrfd.cc負責對在scrfd.h中宣告的函式進行了實現。在編寫預處理的過程中要注意,RKNPU2目前僅支援NHWC格式的輸入資料,因此必須遮蔽Permute操作。我這裡使用disable_permute_ 變數控制Permute操作。此外由於FastDeploy採用的是RKNPU2的零拷貝流程來實現後端的處理和運算,因此可以考慮將Normalize操作放在NPU上來做,提升速度,我這裡使用disable_normalize_ 變數控制Normalize的開關。如果需要詳細的程式碼,請訪問以下連結。

#include "fastdeploy/vision/facedet/contrib/scrfd.h"
#include "fastdeploy/utils/perf.h"
#include "fastdeploy/vision/utils/utils.h"
namespace fastdeploy {
namespace vision {
namespace facedet {
bool SCRFD::Preprocess(Mat* mat, FDTensor* output, std::map<std::string, std::array<float, 2>>* im_info) {
  return true;
}

bool SCRFD::Postprocess(std::vector<FDTensor>& infer_result, FaceDetectionResult* result, const std::map<std::string, std::array<float, 2>>& im_info, float conf_threshold, float nms_iou_threshold) {
  return true;
}

bool SCRFD::Predict(cv::Mat* im, FaceDetectionResult* result, float conf_threshold, float nms_iou_threshold) {
  return true;
}
}  // namespace facedet
}  // namespace vision
}  // namespace fastdeploy
  • 在vision.h中新增我們的模型

我們編寫完scrfd的程式碼之後,我們還需要讓FastDeploy知道我們已經編寫了scrfd程式碼,因此我們需要在fastdeploy/vision.h檔案中補充scrfd.h標頭檔案的路徑。

編譯FastDeploy C++ SDK

編寫完C++程式碼後,我們需要編譯C++版本的FastDeploy。一是為了測試我們編寫的程式碼是否有程式上的漏洞,二是為了後續編寫example可以連結FastDeploy編譯出來的動態庫。編譯的細節詳情請參考FastDeploy C++程式碼編譯指南。

這裡直接給出編譯時的命令:

git clone https://github.com/PaddlePaddle/FastDeploy.git
cd FastDeploy
mkdir build && cd build
cmake ..  -DENABLE_ORT_BACKEND=ON \
          -DENABLE_RKNPU2_BACKEND=ON \
          -DENABLE_VISION=ON \
          -DRKNN2_TARGET_SOC=RK3588 \
          -DCMAKE_INSTALL_PREFIX=${PWD}/fastdeploy-0.0.3
make -j8
make install

編寫C++ example程式碼

為了除錯我們已經完成的C++程式碼,以及方便使用者使用,在編寫完上述程式碼之後,我們需要編寫對應example的程式碼來驗證我們的想法是否正確。在編寫C++ example時,目錄下的檔案一般由infer_model_name.cc以及CMakeLists.txt組成。在CMakeLists.txt中需要對不同的infer_model_name.cc生成不同的infer_model_name程式。

  • 編寫infer.cc

infer.cc主要負責呼叫FastDeploy的C++程式碼來對SCRFD進行測試。在上文中,我們提到vision.h可以讓fastdeploy知道我們已經編寫了SCRFD模型。因此在編寫example時,我們只需要包含vision.h,即可讓程式知道,我們已經聲明瞭FastDeploy所有已經實現的視覺模型。針對RKNPU的測試,其流程一般為初始化模型,然後根據轉換模型時的配置決定是否需要disable_normalize和disable_permute,隨後輸入測試圖片,呼叫Predict函式進行處理,最後使用對應的視覺化函式進行視覺化。

#include <iostream>
#include <string>
#include "fastdeploy/vision.h"
void RKNPU2Infer(const std::string& model_dir, const std::string& image_file) {
  auto model = fastdeploy::vision::facedet::SCRFD(model_file, params_file, option, format);
  model.Initialized();
  model.DisableNormalize();
  model.DisablePermute();
  auto im = cv::imread(image_file);
  fastdeploy::vision::FaceDetectionResult res;
  model.Predict(&im, &res)
  auto vis_im = fastdeploy::vision::VisFaceDetection(im, res);
  cv::imwrite("infer_rknn.jpg", vis_im);
  std::cout << "Visualized result saved in ./infer_rknn.jpg" << std::endl;
}

int main(int argc, char* argv[]) {
  if (argc < 3) {
    std::cout
        << "Usage: infer_demo path/to/model_dir path/to/image run_option, "
           "e.g ./infer_model ./picodet_model_dir ./test.jpeg"
        << std::endl;
    return -1;
  }

  RKNPU2Infer(argv[1], argv[2]);
  return 0;
}
  • 編寫CMakeLists.txt

編寫完C++ example的程式碼後,我們還需要編寫CMakeLists.txtCMakeLists.txt相當於編譯時的配置檔案,負責連結infer_model_name.cc和FastDeploy的動態庫,並且把模型推理需要用到的東西整合在install目錄下。

CMAKE_MINIMUM_REQUIRED(VERSION 3.10)
project(rknpu_test)
set(CMAKE_CXX_STANDARD 14)
# 指定下載解壓後的fastdeploy庫路徑
set(FASTDEPLOY_INSTALL_DIR "thirdpartys/fastdeploy-0.7.0")
include(${FASTDEPLOY_INSTALL_DIR}/FastDeployConfig.cmake)
include_directories(${FastDeploy_INCLUDE_DIRS})
add_executable(rknpu_test infer.cc)
target_link_libraries(rknpu_test ${FastDeploy_LIBS})

編寫Python程式碼

Python程式碼的編寫主要包括pybind檔案的編寫以及py本體檔案的編寫。上文提到,在FastDeploy中,python程式碼通過呼叫pybind暴露出的C++ API來進行工作,因此我們首先需要編寫pybind.cc。

  • 編寫scrfd_pybind.cc

pybind.cc主要負責提供可用的API給Python呼叫。scrfd_pybind.cc中對SCRFD C++的程式碼進行了暴露,程式碼如下:

#include "fastdeploy/pybind/main.h"
namespace fastdeploy {
void BindSCRFD(pybind11::module& m) {
  // Bind SCRFD
  pybind11::class_<vision::facedet::SCRFD, FastDeployModel>(m, "SCRFD")
      .def(pybind11::init<std::string, std::string, RuntimeOption,
                          ModelFormat>())
      .def("predict",
           [](vision::facedet::SCRFD& self, pybind11::array& data,
              float conf_threshold, float nms_iou_threshold) {
             auto mat = PyArrayToCvMat(data);
             vision::FaceDetectionResult res;
             self.Predict(&mat, &res, conf_threshold, nms_iou_threshold);
             return res;
           })
      .def("disable_normalize",&vision::facedet::SCRFD::DisableNormalize)
      .def("disable_permute",&vision::facedet::SCRFD::DisablePermute);
}
}  // namespace fastdeploy
  • 在facedet_pybind.cc中新增宣告

和在vision.h檔案中新增宣告一樣,在編寫完pybind程式碼之後,我們還需要在fastdeploy/vision/facedet/facedet_pybind.cc中新增宣告。目的是告訴編譯器我們已經編寫了pybind的程式碼,並且在編譯Python時請把我們的程式碼加上。核心程式碼如下:

#include "fastdeploy/pybind/main.h"
namespace fastdeploy {
void BindSCRFD(pybind11::module& m);
void BindFaceDet(pybind11::module& m) {
  auto facedet_module = m.def_submodule("facedet", "Face detection models.");
  BindSCRFD(facedet_module);
}
}
  • 編寫scrfd.py

編寫完pybind.cc後,我們還需要編寫對應的py檔案呼叫pybind暴露出來的C++ API。程式碼如下

from __future__ import absolute_import
import logging
from .... import FastDeployModel, ModelFormat
from .... import c_lib_wrap as C
class SCRFD(FastDeployModel):
    def __init__(self,
                 model_file,
                 params_file="",
                 runtime_option=None,
                 model_format=ModelFormat.ONNX):
        super(SCRFD, self).__init__(runtime_option)

        self._model = C.vision.facedet.SCRFD(model_file, params_file, self._runtime_option, model_format)
        assert self.initialized, "SCRFD initialize failed."

    def predict(self, input_image, conf_threshold=0.7, nms_iou_threshold=0.3):
        return self._model.predict(input_image, conf_threshold, nms_iou_threshold)

編譯FastDeploy Python SDK

編寫example之前我們肯定需要編譯Python版本的FastDeploy程式碼,請參考FastDeploy RKNPU2編譯指南編譯Python版本的FastDeploy。

這裡給出我經常使用的編譯命令:

cd FastDeploy
cd python
export ENABLE_ORT_BACKEND=ON
export ENABLE_RKNPU2_BACKEND=ON
export ENABLE_VISION=ON
export RKNN2_TARGET_SOC=RK3588
python3 setup.py build
python3 setup.py bdist_wheel
cd dist
pip3 install fastdeploy_python-0.0.0-cp39-cp39-linux_aarch64.whl

編寫Python example程式碼

為了除錯我們已經完成的Python程式碼,以及方便使用者使用,在編寫完上述scrfd程式碼之後,我們需要編寫對應example的程式碼來驗證我們的想法是否正確。在編寫Python example時,目錄下的檔案一般由infer_model_name.py組成。

  • 編寫infer.py

infer.py 主要負責呼叫FastDeploy的Python程式碼來對SCRFD的測試。與C++ example相似,針對RKNPU的測試,其流程一般為初始化模型,然後根據轉換模型時的配置決定是否需要disable_normalize和disable_permute,隨後輸入測試圖片,呼叫Predict函式進行處理,最後使用對應的視覺化函式進行視覺化。

import fastdeploy as fd
import cv2
import os
def parse_arguments():
    import argparse
    import ast
    parser = argparse.ArgumentParser()
    parser.add_argument("--model_file", required=True, help="Path of FaceDet model.")
    parser.add_argument("--image", type=str, required=True, help="Path of test image file.")
    return parser.parse_args()
def build_option(args):
    option = fd.RuntimeOption()
    option.use_rknpu2()
    return option
args = parse_arguments()
# 配置runtime,載入模型
runtime_option = build_option(args)
model_file = args.model_file
params_file = ""
model = fd.vision.facedet.SCRFD(model_file, params_file, runtime_option=runtime_option, model_format=fd.ModelFormat.RKNN)
model.disable_normalize()
model.disable_permute()
# 預測圖片分割結果
im = cv2.imread(args.image)
result = model.predict(im)
print(result)
# 視覺化結果
vis_im = fd.vision.vis_face_detection(im, result)
cv2.imwrite("visualized_result.jpg", vis_im)
print("Visualized result save in ./visualized_result.jpg")

編寫文件以及提交pr

請參考SCRFD example編寫模型的轉換文件、模型的cpp example執行文件、模型的python執行文件共三份文件,然後向FastDeploy的Github倉庫提交PR。待稽核過後,你的貢獻就會被記錄啦。

總結

飛槳做開源貢獻的體驗是無與倫比的,首先能夠快速實現程式設計能力提升,在貢獻程式碼的過程中,你會更加深刻的理解書本上的內容,掌握行業前沿的程式碼邏輯和程式設計規範。同時在開發過程中,你還會認識飛槳研發團隊的同學以及很多志同道合的好友,與他們共同創造一些有趣的成果,在修復bug的過程中體驗成就感。歡迎和我一起加入貢獻程式碼的行列。

參考文獻

[1]https://github.com/PaddlePaddle/FastDeploy

[2]Guo J , Deng J , Lattas A , et al. Sample and Computation Redistribution for Efficient Face Detection[J]. 2021.