Java 呼叫 PaddleDetection 模型

2023-02-20 15:03:02

文章地址

介紹

訓練好的模型要給業務呼叫,deepjavalibrary/djl:Java 中與引擎無關的深度學習框架 (github.com) 可以完成這件事,它支援使用 Java 呼叫 PyTorch、TensorFlow、MXNet、ONNX、PaddlePaddle 等引擎的模型(也支援部分引擎的模型構建和訓練),本文只介紹呼叫 PaddlePaddle 引擎的模型呼叫。

呼叫模型流程:

  1. 匯出模型(我更喜歡 ONNX 格式,它在 CPU 上推理也挺快的,可以快速測試,但有的運算元不支援匯出),確認模型輸入輸出
  2. 編寫 Java 載入模型以及處理輸入輸出的程式碼

PaddleDetection 模型匯出

匯出模型

Anaconda 設定一個 PaddleDetection 的環境,cpu 版本即可(paddlepaddle==2.2.2),下載 PaddleDetection 工程,修改工程中 configs/runtime.yml 的屬性 use_gpufalse

下面以 configs/pphuman/pedestrian_yolov3/pedestrian_yolov3_darknet.yml 為例介紹整個流程,匯出模型:

$ python tools/export_model.py -c configs/pphuman/pedestrian_yolov3/pedestrian_yolov3_darknet.yml -o weights=https://paddledet.bj.bcebos.com/models/pedestrian_yolov3_darknet.pdparams --output_dir pedestrian_yolov3_darknet

再轉換為 ONNX:

$ paddle2onnx --model_dir pedestrian_yolov3_darknet/pedestrian_yolov3_darknet --model_filename model.pdmodel --params_filename model.pdiparams --opset_version 11 --save_file pedestrianYolov3.onnx  --enable_onnx_checker True

確認輸入輸出

PaddleDetection 模型匯出教學 中檢視模型輸入輸出引數,再通過 Netorn 開啟前面匯出的 ONNX 模型詳細確認

Java 讀取模型及推理

依賴

<dependencies>
    <dependency>
        <groupId>ai.djl</groupId>
        <artifactId>api</artifactId>
    </dependency>
    <!--混合引擎,因為有的引擎 NDArray 不支援-->
    <dependency>
        <groupId>ai.djl.mxnet</groupId>
        <artifactId>mxnet-engine</artifactId>
    </dependency>
    <dependency>
        <groupId>ai.djl.onnxruntime</groupId>
        <artifactId>onnxruntime-engine</artifactId>
    </dependency>
    <dependency>
        <groupId>ai.djl</groupId>
        <artifactId>model-zoo</artifactId>
    </dependency>
    <!--使用 openpnp 的 opencv 加快圖片讀取-->
    <dependency>
        <groupId>ai.djl.opencv</groupId>
        <artifactId>opencv</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>ai.djl</groupId>
            <artifactId>bom</artifactId>
            <version>0.20.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

處理輸入輸出

確定輸入引數為圖片原形狀 im_shape、圖片(需要歸一化)image、比例 scale_factor,輸出為預測框和預測數量,引數詳細說明見前面提到的 PaddleDetection 模型匯出教學中的說明。

import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.output.BoundingBox;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.modality.cv.output.Rectangle;
import ai.djl.modality.cv.transform.Normalize;
import ai.djl.modality.cv.transform.Resize;
import ai.djl.modality.cv.transform.ToTensor;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.ndarray.types.DataType;
import ai.djl.translate.NoBatchifyTranslator;
import ai.djl.translate.Pipeline;
import ai.djl.translate.TranslatorContext;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// 非批次輸入輸出應實現 NoBatchifyTranslator 介面,而不是 Translator
public class PedestrianTranslator implements NoBatchifyTranslator<Image, DetectedObjects> {
    private final Pipeline pipeline;
    private final float threshold;
    private final List<String> classes;
    private final float imageWidth = 608f;
    private final float imageHeight = 608f;

    public PedestrianTranslator(float threshold) {
        // 定義圖片預處理過程
        pipeline = new Pipeline();
        pipeline.add(new Resize((int) imageWidth, (int) imageHeight)) // resize 為模型圖片輸入格式,變成 608 * 608 * 3,HWC
                .add(new ToTensor()) // HWC -> CHW
                .add(new Normalize(new float[]{0.485f, 0.456f, 0.406f}, new float[]{0.229f, 0.224f, 0.225f})) // 歸一化
                .add(array -> array.expandDims(0)); // CHW -> NCHW
        // 預測閾值
        this.threshold = threshold;
        // 類別
        classes = Collections.singletonList("pedestrian");
    }

    @Override
    public NDList processInput(TranslatorContext ctx, Image input) {
        // 記憶體管理器,負責 NDArray 的記憶體回收
        NDManager manager = ctx.getNDManager();
        // 通過建構函式定義好的管道把圖片轉換到模型需要的圖片格式。NDList 是一個集合,與 List<NDArray> 類似
        NDList ndList = pipeline.transform(new NDList(input.toNDArray(manager, Image.Flag.COLOR)));
        // 新增原圖尺寸引數
        ndList.add(0, manager.create(new float[]{input.getHeight(), input.getWidth()}).expandDims(0));
        // 新增原圖片尺寸與輸入圖片尺寸的比值
        ndList.add(manager.create(new float[]{input.getHeight() / 608f, input.getWidth() / 608f}).expandDims(0));
        return ndList;
    }

    @Override
    public DetectedObjects processOutput(TranslatorContext ctx, NDList list) {
        // 獲取第一個引數預測結果,第二個預測數量沒什麼用
        NDArray result = list.get(0);
        /*
        result demo:
        ND: (3, 6) cpu() float32
        [[  0.    ,   0.9759,  10.0805, 276.1631, 298.1623, 586.246 ],
         [  0.    ,   0.955 , 486.306 , 221.0572, 585.966 , 480.4897],
         [  0.    ,   0.8031, 295.0543, 206.104 , 395.3066, 485.3789],
        ]
         */
        // 獲取類別
        int[] classIndices = result.get(":, 0").toType(DataType.INT32, true).flatten().toIntArray();
        // 獲取置信度
        double[] probs = result.get(":, 1").toType(DataType.FLOAT64, true).toDoubleArray();
        // 獲取預測的目標數量
        int detected = Math.toIntExact(probs.length);

        // 獲取矩形框左上角 x 座標比例(第 2 列)
        NDArray xMin = result.get(":, 2:3").clip(0, imageWidth).div(imageWidth);
        // 獲取矩形框左上角 y 座標比例(第 3 列)
        NDArray yMin = result.get(":, 3:4").clip(0, imageHeight).div(imageHeight);
        // 獲取矩形框右上角 x 座標比例(第 4 列)
        NDArray xMax = result.get(":, 4:5").clip(0, imageWidth).div(imageWidth);
        // 獲取矩形框右上角 y 座標比例(第 5 列)
        NDArray yMax = result.get(":, 5:6").clip(0, imageHeight).div(imageHeight);

        // 轉為可以直接繪製的資料,分別是矩形框左上角的 x 和 y 座標、矩形框的寬和高,均為比例
        float[] boxX = xMin.toFloatArray();
        float[] boxY = yMin.toFloatArray();
        float[] boxWidth = xMax.sub(xMin).toFloatArray();
        float[] boxHeight = yMax.sub(yMin).toFloatArray();

        // 封裝成 DetectedObjects 物件輸出
        List<String> retClasses = new ArrayList<>(detected);
        List<Double> retProbs = new ArrayList<>(detected);
        List<BoundingBox> retBB = new ArrayList<>(detected);
        for (int i = 0; i < detected; i++) {
            // 類別不存在或者置信度低於預測閾值則跳過
            if (classIndices[i] < 0 || probs[i] < threshold) {
                continue;
            }
            retClasses.add(classes.get(0));
            retProbs.add(probs[i]);
            retBB.add(new Rectangle(boxX[i], boxY[i], boxWidth[i], boxHeight[i]));
        }
        return new DetectedObjects(retClasses, retProbs, retBB);
    }
}

這裡涉及的 NDArray 操作比較多,使用官方實現的 Transform 和 Pipeline 可以簡化程式碼,不過手動調 NDImageUtils 更清晰。簡單說幾個 API:

  1. expandDims:增加維度,比如 Pipeline 的一個 Transform Lambda 將 CHW 前面加一個維度變成 NCHW
  2. get:檢視 NDIndex API(方法註釋上均有程式碼樣例說明)、百度 numpy 索引切片或 NDArray 教學,搞懂 :,
  3. clip:限制數值,數值越界就取該方法傳入的值

載入模型

import ai.djl.MalformedModelException;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.training.util.ProgressBar;

import java.io.IOException;
import java.nio.file.Paths;

public class Models {

    public static ZooModel<Image, DetectedObjects> getModel() throws ModelNotFoundException, MalformedModelException, IOException {
        return Criteria.builder()
                .optEngine("OnnxRuntime") // 選擇引擎
                .setTypes(Image.class, DetectedObjects.class) // 設定輸入輸出
                .optModelPath(Paths.get("D:\\Repository\\Github\\PaddleDetection\\pedestrian_yolov3_darknet.onnx")) // 設定模型地址。Jar 包、Zip 包根據 API 自行設定
                .optProgress(new ProgressBar()) // 進度條
                .optTranslator(new PedestrianTranslator(.5f)) // 預設的轉換器,不是執行緒安全的
                .build().loadModel();
    }
}

推理

import ai.djl.Device;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.modality.cv.output.DetectedObjects;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.TranslateException;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Inference {
    public static void main(String[] args) throws IOException, MalformedModelException, TranslateException, ModelNotFoundException {
        String imageFilePath = "C:\\Users\\DELL\\Desktop\\2.png";

        // 載入模型
        try (ZooModel<Image, DetectedObjects> model = Models.getModel()) {
            // 新建一個推理,使用 GPU
            try (Predictor<Image, DetectedObjects> predictor = model.newPredictor(Device.gpu())) {
                Image image = ImageFactory.getInstance().fromFile(Paths.get(imageFilePath));
                // 推理
                DetectedObjects result = predictor.predict(image);
                // 繪製矩形框
                image.drawBoundingBoxes(result);
                image.save(Files.newOutputStream(Paths.get("output.png")), "png");
            }
        }
    }
}

CPU GPU 設定

沒有設定 cuda 的話自動下載 CPU 所需的檔案,有 cuda 的話會自動尋找匹配 cuda 版本的檔案,目前官網上的 cuda 版本是 10.2 和 11.2。

也可以通過設定 jar 來指定 CPU 還是 GPU,以 ONNX 為例(詳見DJL Hybrid engines ONNX):

<dependency>
    <groupId>ai.djl.onnxruntime</groupId>
    <artifactId>onnxruntime-engine</artifactId>
    <version>0.20.0</version>
    <scope>runtime</scope>
    <exclusions>
        <exclusion>
            <groupId>com.microsoft.onnxruntime</groupId>
            <artifactId>onnxruntime</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.microsoft.onnxruntime</groupId>
    <artifactId>onnxruntime_gpu</artifactId>
    <version>1.13.1</version>
    <scope>runtime</scope>
</dependency>

注意

  1. 最需要知道的是匯出的模型的輸入和輸出,否則不知道怎麼寫 Translator
  2. DJL 執行所需的檔案挺大的,它會在第一次執行時下載,網路卡流量在動就等會吧(在 /${HOME}/.djl.ai/ 下)
  3. 通常第一次推理比較慢,建議預熱一次
  4. 多執行緒建議每個執行緒一個 Predictor

Jupyter Notebook

附上可以直接執行的 notebook:d2l/paddledetection.ipynb at master · hligaty/d2l (github.com)。Maven 下載依賴比較慢,建議手動下載依賴放到 /${HOME}/.ivy2/cache/ 下。

參考與推薦

PaddleDetection 安裝

PaddleDetection 模型匯出教學

PaddleDetection 模型匯出為 ONNX 格式教學

DJL 引擎

AIAS_人工智慧加速器|Java SDK|中臺|套件

PaddleOCR 的 Java 高效能部署

frankfliu/IJava:用於執行Java程式碼的Jupyter核心