Kerasの自作モデルをONNX形式モデルへと変換しRaspberry Pi 5 の AI Kit (Hailo8L) 向けのHEF形式にコンパイルして物体検出をする

前回の記事で **Raspberry Pi AI KitをRaspberry Pi 5無しでx86_64に接続しDockerから推論用のHEFファイルを転送して実行できること** を確認できました。今回の記事では、 **オリジナルの機械学習モデルをRaspberry Pi AI Kit上で実行する** 流れをご紹介いたします。Raspberry Pi 5 AI Kitの制御は、前回作成したDocker環境を利用しています。

https://qiita.com/xtrizeShino/items/63281c1e3ed14f9501e2

### 今回利用した機材とOS

**Core i5-14500 RAM64GB + 6TB NVMe**

**Ubuntu Desktop 24.04.1 LTS**

**Raspberry Pi 5 に対応したAI Kit + M.2搭載用ネジ**

https://www.amazon.co.jp/dp/B0D7HD8PMQ

### 参考にさせていただきました記事

https://www.nkozawa.com/blog/archives/8108

https://github.com/onnx/tensorflow-onnx/issues/2348

https://qiita.com/studio_haneya/items/be9bc7c56af44b7c1e0a

https://zenn.dev/kleamp1e/articles/202104-keras-onnx

# Kerasの学習済みResNetをONNXとして書き出す

まず、オリジナルの機械学習モデルをONNX形式として準備します。今回はオリジナルの機械学習モデルを定義する代わりにKerasの提供する`keras.applications.resnet`の`ResNet50`をimagenetデータセットにより事前学習したモデル`(weights='imagenet')`を利用しました。まず **以下のスクリプトによりONNXモデルを生成** します。ONNX生成時のダミーの入力画像には下記画像を利用しました。このモデルは、入力テンソルとして画像を(同時処理画像枚数x224x224x3)に変形したものをとり、出力テンソルとして(同時処理画像枚数x1000)のクラス分類をとります。

```python
import numpy as np
import tensorflow as tf
import tf2onnx
import onnx
import onnxruntime
import keras.utils as image
from keras.applications.resnet import preprocess_input
from keras.applications.resnet import ResNet50

input_image_path = 'dog.jpg'
image_size = 224

img = image.load_img(input_image_path, target_size=(image_size, image_size))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)

model = ResNet50(include_top=True, weights='imagenet')
onnx_model, _ = tf2onnx.convert.from_keras(model)
onnx.save(onnx_model, './ResNet50.onnx')
```

![dog.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/bf1e9827-f8f0-9ef8-d325-8215338ce41f.jpeg)

実行に必要なパッケージは以下の通りです。

```toml
[project]
name = "keras-work"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "keras2onnx>=1.7.0",
    "numpy==1.26.4",
    "onnx>=1.17.0",
    "onnxruntime>=1.20.1",
    "keras-preprocessing>=1.1.2",
    "keras-applications>=1.0.8",
    "tensorflow-gpu>=2.9.0",
    "keras>=2.9.0",
    "scipy==1.12.0",
    "scikit-learn==1.2.2",
    "tf2onnx>=1.16.1",
    "pillow>=11.0.0",
]
```

https://qiita.com/xtrizeShino/items/74c9d3f5991da4afb06b

#### PyTorchのモデルをONNX形式に書き出す場合

PyTorchにてONNX形式のモデルを入手する方法は以下の記事をご参照ください。

https://qiita.com/xtrizeShino/items/183cfcc03454189c4571

# キャリブレーション用にデータセットを入手する

続けて、作成したONNXをHailo8L上で実行するためのHEF形式へと変換していきましょう。ONNX形式からHEF形式へ変換する際、モデルを精密に調整するための **「キャリブレーション用のデータセット」** が求められます。そこでホストOSで **「[ImageNet Object Localization Challenge](https://www.kaggle.com/c/imagenet-object-localization-challenge/data)」へとアクセス** して、imagenetのデータセットをダウンロードします。下記Webサイトを表示した後、ページ右下の **「Download All」** をクリックします。ユーザー登録を求められますので、メールアドレスを準備しておいてください。

![Screenshot from 2024-12-09 11-27-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/8fb07691-384b-41a1-ccff-96ac39d09c27.png)

![Screenshot from 2024-12-09 12-28-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/e8b56e98-4cf4-4ca3-363d-375b90d73fa9.png)

ユーザー登録後、再度「Download All」をクリックすると160GB超の **「imagenet-object-localization-challenge.zip」** のダウンロードが開始されます。まずこのファイルをホストOS上に保存しておいてください。以降の手順でDocker上へと移動します。

![Screenshot from 2024-12-09 10-39-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/b68fb17f-7264-2d2b-1f70-97f3f83c3c1f.png)

# Docker上でHEFをコンパイルする準備をする

ONNX形式からHEF形式への変換はDocker上で実行します。また変換処理は非常に重いためGPUを搭載したPCで作業したほうがよいでしょう。環境構築方法は前回記事を参考にしてください。

https://qiita.com/xtrizeShino/items/63281c1e3ed14f9501e2

### 作業ディレクトリを作成して必要なファイルを投入する

まず、Dockerへログインして、作業ディレクトリを作成、必要なファイルを作業ディレクトリ上に集めます。前手順でダウンロードした **imagenetのデータセット「imagenet-object-localization-challenge.zip」** に加えて、ResNetをHailo8L向けに変換する際にONNXモデルの中の **どのノードを入出力とするかを定める「hailo_model_zoo/cfg/networks/resnet_v1_50.yaml」**、Hailo8Lに **最適化するためのパラメータ「hailo_model_zoo/cfg/alls/generic/resnet_v1_50.alls」** を作業ディレクトリに集めます。

```bash
# 作業ディレクトリを作成する
$ cd /local/workspace
$ mkdir onnx-sandbox
$ cd onnx-sandbox/

# HEF変換の際に利用するキャリブレーション用のデータセットを入手する
$ scp shino@172.17.0.1:/home/shino/Downloads/imagenet-object-localization-challenge.zip ./
# shino@172.17.0.1's password: ...

# 変換するONNXモデルを取得する
$ scp shino@172.17.0.1:/home/shino/keras-work/ResNet50.onnx ./
# shino@172.17.0.1's password: ...
$ ls *.onnx
ResNet50.onnx

# ResNetをHailo8L向けにビルドするための設定ファイルを取得する
$ cp /local/workspace/hailo_model_zoo/hailo_model_zoo/cfg/networks/resnet_v1_50.yaml ./
$ cp /local/workspace/hailo_model_zoo/hailo_model_zoo/cfg/alls/generic/resnet_v1_50.alls ./

# 設定ファイル名を変更しておく(オリジナルと混同するため)
$ mv resnet_v1_50.yaml my_resnet50.yaml
$ mv resnet_v1_50.alls my_resnet50.alls

# 作業ディレクトリの内容
$ ls
imagenet-object-localization-challenge.zip
my_resnet50.alls
my_resnet50.yaml
ResNet50.onnx

```

https://github.com/hailo-ai/hailo_model_zoo/issues/63

### Docker上でimagenetデータセットをキャリブレーション向けに登録する

次に、入手したimagenetデータセットをHEFファイルを生成する際のキャリブレーションに利用する旨を設定します。データセットのzipファイルを解凍し `train` データが格納されているパスを確認しておきましょう。最後にhalio_model_zooに含まれている **データセット変換スクリプトの「create_imagenet_tfrecord.py」** を利用してtfrecordと呼ばれる、キャリブレーションの際に参照する設定ファイルを生成します。

```bash
# 作業ディレクトリへ移動する
$ cd /local/workspace
$ cd onnx-sandbox/

# データセットを展開する
$ ls *.zip
imagenet-object-localization-challenge.zip
$ unzip imagenet-object-localization-challenge.zip
### ...10分程度かかります

# 展開後のディレクトリ内
$ ls
ILSVRC                                      LOC_synset_mapping.txt
imagenet-object-localization-challenge.zip  LOC_train_solution.csv
LOC_sample_submission.csv                   LOC_val_solution.csv

# 画像ファイルの格納先
$ ls ILSVRC/Data/CLS-LOC/
test  train  val

# データセットを変換するスクリプトをローカルにコピーする
$ cp /local/workspace/hailo_model_zoo/hailo_model_zoo/datasets/create_imagenet_tfrecord.py ./

# キャリブレーション用のデータセットを準備する
$ python ./create_imagenet_tfrecord.py calib --img ILSVRC/Data/CLS-LOC/train/
# calib #8191: ILSVRC/Data/CLS-LOC/train/n02095889/n02095889_7380.JPEG: 100%|█| 81
# Done converting 8192 images
```

# ONNXモデルの先頭ノードと終端ノード名を調べて設定ファイルへ書き込む

次に、ONNX形式からHEF形式へとモデルを変換する際に、ONNXで定義されたノードのうち、どれを入出力ノードとするかを決めるため、ノードの名前を調べます。 **hailo_model_zooの設定ファイルのままでONNXモデルを変換しようとすると下記のようにノード名が見つからない旨のエラーが出力されてしまいます。**

```bash
# ONNXモデルをHAR()形式へと変換する
$ hailomz parse --hw-arch hailo8l --ckpt ./ResNet50.onnx --yaml my_resnet50.yaml 
### hailo_sdk_client.model_translator.exceptions.MisspellNodeError: Unable to find end node name: ['resnet_v1_50/predictions/Softmax'], please verify and try again.
```


### Netronでノード名を確認する

ONNX形式のモデルを[Netron](https://netron.app/)で開き、先頭ノードと終端ノードをクリックすると、画面右にノード名が表示されますので、これを控えておきましょう。自分のモデルでは先頭ノード名が **「Conv__435」** で、終端ノード名が **「resnet50/predictions/Softmax」** でした。これをモデルを定義するファイルに書き込みます。

![Screenshot from 2024-12-09 12-03-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/298f60b3-c6c4-4668-1c12-4508bccbce11.png)


![Screenshot from 2024-12-09 12-24-29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/92b7fc52-bcdd-9127-a5e4-a72b8348ef4f.png)

![Screenshot from 2024-12-09 12-24-50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/8341c8d8-df03-2221-b9d5-16179f82584d.png)

### ノード名を設定ファイルへ書き込む

Docker上の作業ディレクトリに戻り **モデルを定義している「my_resnet50.yaml」(前手順でhailo_model_zooからコピーし名前を変更したファイル)** に設定を書き込みます。変更する箇所は以下の通りです。

- network: network_name: 
  - モデル名前を指定します。今回は「my_resnet50」としました
  - この名前で中間ファイル等が生成されます
- paths: alls_script: 
  - ハードウェアへの最適化を指定するallsファイルのパスを設定します
  - 今回は前手順でコピーして名前を変更した「my_resnet50.alls」を指定しました
  - 本来は相対パスでいけるようですが...ここでは絶対パスにしました
- parser:
  - 入力ノード名と出力ノード名を指定します
    - 入力ノード名はNetronを参考に「Conv__435」としました
    - 出力ノード名はNetronを参考に「resnet50/predictions/Softmax」としました

以上を設定ファイルへと書き込み、保存します。

```bash
# 作業ディレクトリへ移動する
$ cd /local/workspace
$ cd onnx-sandbox

# モデルを定義するyamlファイルを自分のONNXモデル向けに書き換える
$ vi my_resnet50.yaml
```

```yaml
base:
- base/resnet.yaml
network:
  ### ネットワーク名を自分のものに変更
  network_name: my_resnet50
...
paths:
  ### 名前を変更したallsファイルを指定する
  alls_script: /local/workspace/onnx-sandbox/my_resnet50.alls
...
parser:
  nodes:
  ### Netronで調べた入力ノード名を指定
  - Conv__435
  ### Netronで調べた出力ノード名を指定
  - resnet50/predictions/Softmax
```

# Docker上でONNXをHARに変換する

それでは変換を始めましょう。まず、Docker上に移動し、作業ディレクトリの中で`hailomz parse`を実行します。これによりONNX形式からHAR(Hailo ARchive file)形式へと変換します。変換の際には、モデルを実行するハードウェアのアーキテクチャを`--hw-arch`で指定し、入力となるONNXモデルを`--ckpt`で指定、最後に変換のルールを記述した設定ファイルを`--yaml`で指定します。

```bash
# ONNXモデルをHAR(Hailo ARchive)形式へと変換する
$ hailomz parse --hw-arch hailo8l --ckpt ./ResNet50.onnx --yaml my_resnet50.yaml 
# <Hailo Model Zoo INFO> Start run for network my_resnet50 ...
# <Hailo Model Zoo INFO> Initializing the runner...
# [info] Translation started on ONNX model my_resnet50
# [info] Restored ONNX model my_resnet50 (completion time: 00:00:00.23)
# [info] Extracted ONNXRuntime meta-data for Hailo model (completion time: 00:00:00.72)
# [info] Start nodes mapped from original model: 'resnet50/conv1_conv/Conv2D__6': 'my_resnet50/input_layer1'.
# [info] End nodes mapped from original model: 'resnet50/predictions/Softmax'.
# [info] Translation completed on ONNX model my_resnet50 (completion time: 00:00:00.96)
# [info] Saved HAR to: /local/workspace/onnx-sandbox/my_resnet50.har

# HARファイルを生成できていることを確認する
$ ls *.har
my_resnet50.har

# Dockerからホスト上にharファイルを移動する
$ scp ./my_resnet50.har shino@172.17.0.1:/home/shino/keras-work/
### -->> Firefox上のNetronで可視化する
```

### 生成されたネットワークをNetronで確認する

parseしたHAR形式のファイルはNetronにより可視化することができます **（注：この後の手順でHAR形式をよりハードウェアに適したHAR形式へ変換するoptimize作業がありますが、optimize作業以降はモデルが複雑になりすぎてNetronが落ちます、今のうちに見ておきましょう）** 。前述のONNX形式を可視化したものと比較し、HAR形式のファイルが正しいモデルを保持できているかを確認しておいてください。

![Screenshot from 2024-12-09 13-39-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/f2e44ab6-bfd0-f242-8093-a86b1f27ec04.png)

![Screenshot from 2024-12-09 13-39-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/d83a98ee-4f83-c1b8-f470-55a88c7de0cb.png)

# Docker上でHARからHEFを生成する

生成したHAR形式のモデルを、前手順でキャリブレーション用に設定したimagenetデータセットを活用して、optimize(最適化)とcompile(実行形式へと変換)します。変換の際には`--har`指定子でHAR形式のファイルのパスを指定してください。 **すべて成功すると `<ネットワーク名>.hef` ファイルが生成されます。**

```bash
# harを最適化する
$ hailomz optimize --hw-arch hailo8l --har ./my_resnet50.har --yaml my_resnet50.yaml 
# <Hailo Model Zoo INFO> Start run for network my_resnet50 ...
# <Hailo Model Zoo INFO> Initializing the hailo8l runner...
# <Hailo Model Zoo INFO> Preparing calibration data...
# [info] Loading model script commands to my_resnet50 from /local/workspace/onnx-sandbox/my_resnet50.alls
# [info] Starting Model Optimization
# [info] Using default optimization level of 2
# [info] Using default compression level of 1
# ...

# hefファイルを生成する
$ hailomz compile --hw-arch hailo8l --har ./my_resnet50.har --yaml my_resnet50.yaml 
# <Hailo Model Zoo INFO> Start run for network my_resnet50 ...
# <Hailo Model Zoo INFO> Initializing the hailo8l runner...
# [info] Loadinhttps://raw.githubusercontent.com/pytorch/hub/refs/heads/master/imagenet_classes.txtg model script commands to my_resnet50 from /local/workspace/onnx-sandbox/my_resnet50.alls
# [info] To achieve optimal performance, set the compiler_optimization_level to "max" by adding performance_param(compiler_optimization_level=max) to the model script. Note that this may increase compilation time.
# [info] Loading network parameters
# [info] Starting Hailo allocation and compilation flow
# ...
# [info] Compiling context_0...
# [info] Compiling context_1...
# [info] Compiling context_2...
# [info] Bandwidth of model inputs: 1.14844 Mbps, outputs: 0.00762939 Mbps (for a single frame)
# [info] Bandwidth of DDR buffers: 0.0 Mbps (for a single frame)
# [info] Bandwidth of inter context tensors: 6.50781 Mbps (for a single frame)
# [info] Building HEF...
# [info] Successful Compilation (compilation time: 19s)
# [info] Saved HAR to: /local/workspace/onnx-sandbox/my_resnet50.har
# <Hailo Model Zoo INFO> HEF file written to my_resnet50.hef

$ ls *.hef
my_resnet50.hef
```

![Screenshot from 2024-12-09 13-26-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/ba37ca30-4638-c0ef-d096-92a223dd8043.png)

![Screenshot from 2024-12-09 13-27-29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/f795e5c3-43f7-eb09-2587-e9d52b2bafc3.png)

### HEFをHailo8l上に読み込めることを確認する

生成したHEFファイルが実行できることを `hailortcli run` コマンドで調べます。今回のモデルはResNet50と深めのモデルですが、88FPS出るようです。また最適化をかけているため量子化が有効化されていることを確認できます（`Quantized: true`）。

```bash
$ hailortcli run my_resnet50.hef 
# Running streaming inference (my_resnet50.hef):
#   Transform data: true
#     Type:      auto
#     Quantized: true
# Network my_resnet50/my_resnet50: 100% | 442 | FPS: 88.37 | ETA: 00:00:00
# > Inference result:
#  Network group: my_resnet50
#     Frames count: 442
#     FPS: 88.38
#     Send Rate: 106.42 Mbit/s
#     Recv Rate: 0.71 Mbit/s
```

# 生成したHEFファイルを使って推論する

最後に、今回作成したHEF形式のファイルを使って推論してみましょう。まずは推論に必要なファイルを集めます。推論にはResNetで分類するクラス名を含んだ `imagenet_classes.txt` と、推論の入力となる画像（ここでは `dog.jpg`）が必要となります。ファイルが揃ったら、推論用のスクリプト `test.py` を作成します。

![dog.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/bf1e9827-f8f0-9ef8-d325-8215338ce41f.jpeg)

```bash
# imagenetのクラス名一覧を入手する
$ wget https://raw.githubusercontent.com/pytorch/hub/refs/heads/master/imagenet_classes.txt

# 物体検出に使う画像を入手する
$ scp shino@172.17.0.1:/home/shino/keras-work/dog.jpg ./

# 推論用のスクリプトを作成する
$ vi test.py
```

推論用スクリプトの内容は下記の記事で紹介したものを利用しました。

https://qiita.com/xtrizeShino/items/0d51dbf8f86f3040c218

```python
mport numpy as np
from hailo_platform import VDevice, HailoSchedulingAlgorithm
import cv2

timeout_ms = 1000

params = VDevice.create_params()
params.scheduling_algorithm = HailoSchedulingAlgorithm.ROUND_ROBIN

# load resnet classes
with open('./imagenet_classes.txt') as f:
    labels = [line.strip() for line in f.readlines()] 

# The vdevice is used as a context manager ("with" statement) to ensure it's released on time.
with VDevice(params) as vdevice:

    # Create an infer model from an HEF:
    infer_model = vdevice.create_infer_model('./my_resnet50.hef')

    # Configure the infer model and create bindings for it
    with infer_model.configure() as configured_infer_model:
        bindings = configured_infer_model.create_bindings()

        im = cv2.imread('dog.jpg')
        input_img = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
        img_detect = cv2.resize(input_img, (224, 224))
        # print(img_detect.shape) -->> (224, 224, 3)

        # Set input and output buffers
        #buffer = np.empty(infer_model.input().shape, dtype=np.uint8)
        buffer = img_detect
        bindings.input().set_buffer(buffer)
        # print(buffer.shape) -->> (224, 224, 3)

        buffer = np.empty(infer_model.output().shape, dtype=np.uint8)
        bindings.output().set_buffer(buffer)
        # print(buffer.shape) -->> (1000,)

        # Run synchronous inference and access the output buffers
        configured_infer_model.run([bindings], timeout_ms)
        buffer = bindings.output().get_buffer()
        # print(buffer.shape) -->> (1000,)

        sorted_val = np.sort(buffer)[::-1]
        sorted_idx = np.argsort(buffer)[::-1]
        print("-->> {0}:{1:.1f}%".format(
            labels[sorted_idx[0]],
            sorted_val[0] / 255 * 100
            ))

        # Run asynchronous inference
        job = configured_infer_model.run_async([bindings])
        job.wait(timeout_ms)

```

推論を実行すると下記のように結果を得ることができます。

```bash
$ python test.py 
# -->> whippet:29.8%
```

whippet(犬)であることを検出できましたね！

![{3D1CACC5-D689-45EC-AA57-4193E07497ED}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/537ffcab-5234-b8a3-9e6c-ed3ad8f5a967.png)

----

以上のように **Raspberry Pi AI Kitでは、Hailoが配布しているモデルだけでなく、自作のONNX形式のモデルをHEF形式に変換することにより、様々なモデルを利用することができます。** ONNX形式のモデルはPyTorch、Keras、Tensorflowなど数多くのフレームワークから出力できますので、色々なアプリケーションを実現できそうですね！

----

是非お試しください！