🍣

Orange Pi5のNPUを使用してyolo(高速?)を動かしてみる(rknn-toolkit2)

2023/05/23に公開
1

記事を書くきっかけになった経緯 📺

Orange Pi5を早期予約購入して、何かやろうと思っていたら、6か月経っていた。
https://blog.osakana.net/archives/13174
最近は開発環境が整ってきており、インターネットの記事をみると面白うなことをやっている人がけっこういます。
https://denor.jp/orange-pi-5をnvme-ssdから起動してみました
https://zenn.dev/techmadot/articles/hello-orangepi5
私も、何か人がやっていないことをやりたいなーと思い、日々すごしていたら非常に興味のそそられる動画に会いました。
https://www.bilibili.com/video/BV1sM4y1D7Q1/?spm_id_from=333.337.search-card.all.click
https://www.bilibili.com/video/BV19V4y1Z7qk/?spm_id_from=333.788.recommend_more_video.0
Orange Pi5でYoloを動かしていてfps値が60ぐらいでています。どうやら、Orange Pi5に搭載されているNPUを使用している感じです。香橙派5(中国語でOrange Pi5を意味するらしい)

これは面白そう!と思い、どうやるのかを調べてみても日本語のページはおろか英語のページすらなかなかでてこない。香橙派5をキーワードに中国語の記事をあさってみる。
そして、いろいろ調べて結果、中国語技術サイトcsdnでそれらしき記事たちを発見!
https://blog.csdn.net/weixin_51651698/article/details/130187558?spm=1001.2014.3001.5502
https://blog.csdn.net/weixin_51651698/article/details/129335874?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168463864516782425167049%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168463864516782425167049&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-129335874-null-null.142^v87^insert_down28,239^v2^insert_chatgpt&utm_term=香橙派5&spm=1018.2226.3001.4187
https://blog.csdn.net/weixin_51651698/article/details/129343955?ops_request_misc=&request_id=&biz_id=102&utm_term=香橙派5&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-129343955.142^v87^insert_down28,239^v2^insert_chatgpt&spm=1018.2226.3001.4187
サイトとDeepL翻訳を駆使しながらやってみましたが、かなり失敗ました。環境構築に失敗ため、WSLの再インストールやツールのverによるエラー。中国のミラーサイトからダウンロードをしているようだがアクセスが出来ずにダウンロードできないなど、かなり踏んだり蹴ったりでした。
 
そのため、Orange Pi5のNPUを用いたyoloアルゴリズム実装を日本語で記載を行い、多くの人にOrange Pi5と機械学習(yolo)の可能性を知っていただきたい。そして、私がまだ実装できていないmodelをOrange Pi5に実装する一助になれば幸いです。

必要なもの 🎓

  • Orange Pi5本体
    • SDカード32GB
    • OSはOrange 1.1.4 Jammy
  • Ubuntu 22.04.2 LTS環境(公式ではUbuntu 18.04 python 3.6 / Ubuntu 20.04 python 3.8をサポートしてるらしい。私はWSL2を使って環境を作りました。)

実装手順概要 🔨

  1. Orange Pi5にOSをインストール
  2. Ubuntu22.04.2 LTS(WSL2)上でyoloのモデルをonnx形式からrknn形式に変換する
  3. Orange Pi5にrknn形式のモデルを実装する

1. Orange Pi5にOSをインストール 🍘

Orange Pi5にOSをインストールします。使用するソフトウェアはbalenaEtcherを使用します。各自にあった環境をインストールしてください。
https://etcher.balena.io/#download-etcher

OSをOrange Pi5の公式からインストールします。Downloadのとこから、Ubuntu Imageを選択してもらいOrangepi5_1.1.4_ubuntu_jammy_desktop_xfce_linux5.10.110.7zをダウンロードします。
http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/service-and-support/Orange-pi-5.html
7-Zipなどで、中にあるimgファイルを展開する。
imgファイルをbalenaEtcherで書き込んでください。
書き込みが終了したら、SDカードをOrange Pi5に差し込みUbuntuを起動!起動できたら成功です。
とりあえず、初期設定とかネットワークの設定を行ってください。私は毎回windowsからファイルをscp転送したかっため、固定ipにしてipが毎回変わらないようにしました。
https://gihyo.jp/admin/serial/01/ubuntu-recipe/0708
とりあえず、updateとupgradeをしておきます。

sudo apt-get update
sudo apt-get upgrade

ひとまず、Orange Pi5のセットアップは終了です。

2. Ubuntu22.04.2 LTS(WSL2)上でyoloのモデルをonnx形式からrknn形式に変換する🍘

Ubuntuのインストール(WSL2)

Ubuntu22.04.2 LTS環境を用意します。私はメインPCがWindows 11なので、WSL2を使用してUbuntu環境を構築しました。まず、WSL2でUbuntu22.04.2 LTS(WSL2)をインストールします。Win11上でWSLを使うにはMicrosoft StoreでUbuntuをダウンロードして、インストールすれば終了です。2023/5現在では、ただのUbuntuをインストールすれば、Ubuntu22.04.2 LTSが手に入ります。初回起動に下記のメッセージが表示されたら、下記サイトに従って、「Windows の機能の有効化または無効化」を開き、「Linux 用 Windows サブシステム」を有効にして下さい。

このアプリケーションには、Linux 用 Windows サブシステムオプション コンポーネントが必要です。
変更を有効にするには、システムの再起動が必要な場合があります。

https://chigusa-web.com/blog/wsl2-win11/

Ubuntuのインストールが終了したら、Anaconda環境でPython環境を構築していきます。参考にしたサイトは下記サイトです。
https://www.salesanalytics.co.jp/datascience/datascience141/
Anacondaの公式サイトからLinuxの64-Bit (x86) InstallerのURLをコピーする。

2023/5現在では下記のような感じで、ダウンロードできる。

wget https://repo.anaconda.com/archive/Anaconda3-2023.03-1-Linux-x86_64.sh

下記コマンドでインストール用シェルを実行。流れにそってインストールする。

bash Anaconda3-2023.03-1-Linux-x86_64.sh

インストールが終了したらパスに追加して実行できるようにする

echo "export PATH=~/anaconda3/bin:\$PATH" >> ~/.bashrc
source ~/.bashrc

rknn-toolkit2の環境構築

次に、rknn-toolkit2の環境を構築していきます。まず最初にanacondaで仮想環境を作っていきます。仮想環境はPython=3.8で構築しました。

rknn-toolkit2をcloneします。
https://github.com/rockchip-linux/rknn-toolkit2

git clone https://github.com/rockchip-linux/rknn-toolkit2.git

念のためにcpでコピーして別の名前にして使用します。

cp -r rknn-toolkit2/ rknn

仮想環境を用意します。

conda create -n rknn python=3.8
conda activate rknn

docのフォルダに移動して、requirement.txtを利用してpythonのライブラリををインストールすると中国の記事には書いてある。(しかしながら、これが動かない。)

cd rknn/doc
pip install -r requirements_cp38-1.4.0.txt -i https://mirror.
baidu.com/pypi/simple

ミラーを外してやってみたり、pip3でやってみたり小細工をしてみたがtensorflowのインストールでエラー吐いたり、subprocess-exited-with-errorが生じたり、解決することが出来なかった。

そのため、下記の順番でrequirements_cp38-1.4.0.txtに書かれているものをインストールしたら、動作することを確認できた。なぜかはわからない。tensorflowとかは最新のを使用しようしている?ため、必要なライブラリが色々と複雑?であるため、一番初めにインストールしておく。

pip install tensorflow==2.6.2
conda install pytorch==1.10.1 torchvision==0.11.2 torchaudio==0.10.1 cpuonly -c pytorch
pip install onnx==1.9.0
pip install onnxoptimizer==0.2.7
pip install onnxruntime==1.10.0
pip install opencv-python==4.5.5.64
pip install bfloat16
pip install cmake
sudo apt-get install gcc libpq-dev -y
sudo apt-get install python-dev  python-pip -y
sudo apt-get install python3-dev python3-pip python3-venv python3-wheel -y
pip install bfloat16==1.1
pip install tqdm==4.64.0
pip install scipy==1.5.4
pip install ruamel.yaml==0.17.4
pip install psutil==5.9.0
pip install requests==2.27.1
pip install flatbuffers==1.12
pip install protobuf==3.12.2
pip install numpy==1.19.5

2回目のpip install bfloat16==1.1はいらないかもしれませんが、とりあえずやってみて下さい。
全て、インストールし終えたら、Packageに移動します。

cd ~/rknn/packages
pip install rknn_toolkit2-1.4.0_22dcfef4-cp38-cp38-linux_x86_64.whl

これで、rknn_toolkit2がインストールできたと思います。

Installing collected packages: typing-extensions, rknn-toolkit2
  Attempting uninstall: typing-extensions
    Found existing installation: typing_extensions 4.5.0
    Uninstalling typing_extensions-4.5.0:
      Successfully uninstalled typing_extensions-4.5.0
Successfully installed rknn-toolkit2-1.4.0-22dcfef4 typing-extensions-3.7.4.3

ここで、ライブラリインストールは終わりではありません。わたしはこの後、rknnをファイルを生成するためにエラーに引っ掛かり、かなりの時間を要してしまいました。これも理由がわからないのですが、Pythonのライブラリのver関係上このまま動かすとエラーが生じます。やってみたい人は次のステップを飛ばして、rknnモデル生成をやってみると下記のようなエラーが生じると思います。

raise InvalidVersion(f"Invalid version: '{version}'")
pkg_resources.extern.packaging.version.InvalidVersion: Invalid version: '1.4.0-22dcfef4'

上記のエラーは調べてもでてこなかったのですが、近しい質問を調べているとどうやら、setuptoolsが関係してそうな感じでした。そのため、setuptoolsのverをダウングレードして、動作させるようにしました。

pip uninstall setuptools
pip install setuptools==65.0.0

最終的にはcondaの環境はこんな形のライブラリ構成になると思います。

# Name                    Version                   Build  Channel
_libgcc_mutex             0.1                        main
_openmp_mutex             5.1                       1_gnu
absl-py                   0.15.0                   pypi_0    pypi
astunparse                1.6.3                    pypi_0    pypi
bfloat16                  1.1                      pypi_0    pypi
blas                      1.0                         mkl
bzip2                     1.0.8                h7b6447c_0
ca-certificates           2023.01.10           h06a4308_0
cachetools                4.2.4                    pypi_0    pypi
certifi                   2023.5.7                 pypi_0    pypi
charset-normalizer        2.0.12                   pypi_0    pypi
clang                     5.0                      pypi_0    pypi
cmake                     3.26.3                   pypi_0    pypi
cpuonly                   2.0                           0    pytorch
ffmpeg                    4.3                  hf484d3e_0    pytorch
flatbuffers               1.12                     pypi_0    pypi
freetype                  2.12.1               h4a9f257_0
gast                      0.4.0                    pypi_0    pypi
giflib                    5.2.1                h5eee18b_3
gmp                       6.2.1                h295c915_3
gnutls                    3.6.15               he1e5248_0
google-auth               1.35.0                   pypi_0    pypi
google-auth-oauthlib      0.4.6                    pypi_0    pypi
google-pasta              0.2.0                    pypi_0    pypi
grpcio                    1.54.0                   pypi_0    pypi
h5py                      3.1.0                    pypi_0    pypi
idna                      3.4                      pypi_0    pypi
importlib-metadata        6.6.0                    pypi_0    pypi
intel-openmp              2023.1.0         hdb19cb5_46305
jpeg                      9e                   h5eee18b_1
keras                     2.6.0                    pypi_0    pypi
keras-preprocessing       1.1.2                    pypi_0    pypi
lame                      3.100                h7b6447c_0
lcms2                     2.12                 h3be6417_0
ld_impl_linux-64          2.38                 h1181459_1
lerc                      3.0                  h295c915_0
libdeflate                1.17                 h5eee18b_0
libffi                    3.4.4                h6a678d5_0
libgcc-ng                 11.2.0               h1234567_1
libgomp                   11.2.0               h1234567_1
libiconv                  1.16                 h7f8727e_2
libidn2                   2.3.4                h5eee18b_0
libpng                    1.6.39               h5eee18b_0
libstdcxx-ng              11.2.0               h1234567_1
libtasn1                  4.19.0               h5eee18b_0
libtiff                   4.5.0                h6a678d5_2
libunistring              0.9.10               h27cfd23_0
libuv                     1.44.2               h5eee18b_0
libwebp                   1.2.4                h11a3e52_1
libwebp-base              1.2.4                h5eee18b_1
lz4-c                     1.9.4                h6a678d5_0
markdown                  3.4.3                    pypi_0    pypi
markupsafe                2.1.2                    pypi_0    pypi
mkl                       2023.1.0         h6d00ec8_46342
mkl-service               2.4.0            py38h5eee18b_1
mkl_fft                   1.3.6            py38h417a72b_1
mkl_random                1.2.2            py38h417a72b_1
ncurses                   6.4                  h6a678d5_0
nettle                    3.7.3                hbbd107a_1
numpy                     1.19.5                   pypi_0    pypi
oauthlib                  3.2.2                    pypi_0    pypi
onnx                      1.9.0                    pypi_0    pypi
onnxoptimizer             0.2.7                    pypi_0    pypi
onnxruntime               1.10.0                   pypi_0    pypi
opencv-python             4.5.5.64                 pypi_0    pypi
openh264                  2.1.1                h4ff587b_0
openssl                   1.1.1t               h7f8727e_0
opt-einsum                3.3.0                    pypi_0    pypi
pillow                    9.4.0            py38h6a678d5_0
pip                       23.0.1           py38h06a4308_0
protobuf                  3.12.2                   pypi_0    pypi
psutil                    5.9.0                    pypi_0    pypi
pyasn1                    0.5.0                    pypi_0    pypi
pyasn1-modules            0.3.0                    pypi_0    pypi
python                    3.8.16               h7a1cb2a_3
pytorch                   1.10.1              py3.8_cpu_0    pytorch
pytorch-mutex             1.0                         cpu    pytorch
readline                  8.2                  h5eee18b_0
requests                  2.27.1                   pypi_0    pypi
requests-oauthlib         1.3.1                    pypi_0    pypi
rknn-toolkit2             1.4.0-22dcfef4           pypi_0    pypi
rsa                       4.8                      pypi_0    pypi
ruamel-yaml               0.17.4                   pypi_0    pypi
ruamel-yaml-clib          0.2.7                    pypi_0    pypi
scipy                     1.5.4                    pypi_0    pypi
setuptools                65.0.0                   pypi_0    pypi
six                       1.15.0                   pypi_0    pypi
sqlite                    3.41.2               h5eee18b_0
tbb                       2021.8.0             hdb19cb5_0
tensorboard               2.6.0                    pypi_0    pypi
tensorboard-data-server   0.6.1                    pypi_0    pypi
tensorboard-plugin-wit    1.8.1                    pypi_0    pypi
tensorflow                2.6.2                    pypi_0    pypi
tensorflow-estimator      2.6.0                    pypi_0    pypi
termcolor                 1.1.0                    pypi_0    pypi
tk                        8.6.12               h1ccaba5_0
torchaudio                0.10.1                 py38_cpu  [cpuonly]  pytorch
torchvision               0.11.2                 py38_cpu  [cpuonly]  pytorch
tqdm                      4.64.0                   pypi_0    pypi
typing-extensions         3.7.4.3                  pypi_0    pypi
urllib3                   1.26.15                  pypi_0    pypi
werkzeug                  2.3.4                    pypi_0    pypi
wheel                     0.38.4           py38h06a4308_0
wrapt                     1.12.1                   pypi_0    pypi
xz                        5.4.2                h5eee18b_0
zipp                      3.15.0                   pypi_0    pypi
zlib                      1.2.13               h5eee18b_0
zstd                      1.5.5                hc292b87_0

最後にrknnの動作確認をします。
pythonの実行環境に入り、from rknn.api import RKNNをインポートできれば環境構築成功です!

(rknn) test@DESKTOP-6ILOAAU:~/rknn/packages$ python
Python 3.8.16 (default, Mar  2 2023, 03:21:46)
[GCC 11.2.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from rknn.api import RKNN
>>>

onnxモデルからrknnモデルの生成を行う

いよいよ機会学習モデルを生成します。中国のサイトでは独自にyoloのonnxモデルを生成していますが、今回はライブラリにあるモデルを使用します(本当は自分でモデルを作りrknnに変換したかったが、変換がうまくいかずにライブラリにあるモデルを使っている)。exapmleにあるyolov5に移動します。

cd ~/rknn/examples/onnx/yolov5

移動したら下記のようなファイル構成になっていると思います。

(rknn) test@DESKTOP-6ILOAAU:~/rknn/examples/onnx/yolov5$ ls
bus.jpg  dataset.txt  test.py  yolov5s.onnx

test.pyの中身を確かめます。

(rknn) test@DESKTOP-6ILOAAU:~/rknn/examples/onnx/yolov5$ vi test.py
import os
import urllib
import traceback
import time
import sys
import numpy as np
import cv2
from rknn.api import RKNN

ONNX_MODEL = 'yolov5s.onnx'
RKNN_MODEL = 'yolov5s.rknn'
IMG_PATH = './bus.jpg'
DATASET = './dataset.txt'
以下プログラムが続く

ONNX_MODEL = 'yolov5s.onnx'が現在フォルダにあるonnxモデル
RKNN_MODEL = 'yolov5s.rknn'が生成するrknnモデルになります。

またorange Pi5に動作させるために一部プログラムを変更してください。
target_platform='rk3588'をtest.pyに追加してください。

    # pre-process config
    print('--> Config model')
    rknn.config(mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]],target_platform='rk3588')#変更箇所
    print('done')

それではプログラムを動作させます。

(rknn) test@DESKTOP-6ILOAAU:~/rknn/examples/onnx/yolov5$ python test.py
I rknn buiding done
done
--> Export rknn model
done
--> Init runtime environment
W init_runtime: Target is None, use simulator!
done
--> Running model
Analysing : 100%|███████████████████████████████████████████████| 146/146 [00:00<00:00, 3723.17it/s]
Preparing : 100%|████████████████████████████████████████████████| 146/146 [00:00<00:00, 235.03it/s]
W inference: The dims of input(ndarray) shape (640, 640, 3) is wrong, expect dims is 4! Try expand dims to (1, 640, 640, 3)!
done
class: person, score: 0.8253369927406311
box coordinate left,top,right,down: [211.98967677354813, 246.17457234859467, 283.707863509655, 515.0830570459366]
class: person, score: 0.822945237159729
box coordinate left,top,right,down: [473.26746129989624, 231.93780636787415, 562.1268258094788, 519.7597033977509]
class: person, score: 0.7970155477523804
box coordinate left,top,right,down: [114.68497347831726, 238.2831370830536, 207.21904873847961, 541.6129200458527]
class: person, score: 0.4426550567150116
box coordinate left,top,right,down: [79.09243053197861, 339.18039524555206, 121.60037952661514, 514.2349489927292]
class: bus , score: 0.7698131203651428
box coordinate left,top,right,down: [102.34192335605621, 134.41848754882812, 540.0445512533188, 460.4184875488281]

rknnモデルの生成とフォルダに入っているbus.jpgのyolo detectionを実行したような形になります。全てプログラムを見てもらうとわかりますが、imshow()はコメントアウトされているため、結果は数値でのみ出力されます。
そしてフォルダを見てみるとyolov5s.rknnモデルが生成されているのがわかります。

(rknn) test@DESKTOP-6ILOAAU:~/rknn/examples/onnx/yolov5$ ls
bus.jpg      onnx_yolov5_0.npy  onnx_yolov5_2.npy  yolov5s.onnx
dataset.txt  onnx_yolov5_1.npy  test.py            yolov5s.rknn

今回使用するのはyolov5s.rknnのみになります。

3. Orange Pi5にrknn形式のモデルを実装する

Orange Pi5にモデルを移動させます。
私はscpコマンドで転送をしました。(記事を書きながら、思ったのですがわざわざwindowsにもってきてからscpやらずとも仮想環境上でやればいいということに気が付きました。)
とりあえずデスクトップに置いておきます。

scp -r yolov5s.rknn オレンジパイのユーザー名@IPアドレス:/home/orangepi/Desktop

https://snowsystem.net/other/windows/wsl2-ubuntu-explorer/

Minicondaのインストール

Minicondaのインストールを行います。minicondaのインストールは公式のホームページから
Miniconda3 Linux-aarch64 64-bitをDLする
https://docs.conda.io/en/latest/miniconda.html
ダウンロードしたらダウンロードしたディレクトリに移動して実行する。そして、インストールとPATHに追加を行う。conda -Vでverが確認できたら、無事インストール完了

bash Miniconda3-py39_23.3.1-0-Linux-aarch64.sh
export PATH=~/miniconda3/bin/conda:$PATH
source ~/.bashrc
conda -V

rknn_toolkit_lite2のインストール

まず、仮想環境を作成します。PythonのverはPython=3.9.1をインストールします。

conda create --name rknn python==3.9.1
conda activate rknn

rknn_toolkit_lite2(rknn_toolkit_lite2-1.4.0-cp39-cp39-linux_aarch64.whl)をインストールするのですが手法は二つあります。一つ目はrknn-toolkit2のgitからorangepiに同じようにcloneして、rknn-toolkit2/rknn_toolkit_lite2/packages/のディレクトリにあるwhlファイルをインストールする。二つ目はrknnを生成したUbuntuからscpコマンドでwhlファイルだけ持ってくる。どちらでもいいと思います。中国のサイトではscpで転送していたため、私も転送しました。

###WSL2のUbuntu
cd ~/rknn/rknn_toolkit_lite2/packages
scp -r rknn_toolkit_lite2-1.4.0-cp39-cp39-linux_aarch64.whl オレンジパイのユーザー名@IPアドレス:/home/orangepi/Desktop

デスクトップ上に転送したため、デスクトップのディレクトリに移動してインストールをすすめていきます、

中国のサイトから清華大学のリポジトリ?からインストールしたい感じだったため、同じように設定して実行しました

cd ~/Desktop
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install rknn_toolkit_lite2-1.4.0-cp39-cp39-linux_aarch64.whl

インストールが完了したら、opencvを入れます。

pip install opencv-python

下記のdeploy.pyをDesktop上に置きます。

deploy.py
import numpy as np
import cv2
from rknnlite.api import RKNNLite

RKNN_MODEL = 'yolov5s.rknn'
IMG_PATH = 'bus.jpg'

QUANTIZE_ON = True

OBJ_THRESH = 0.25
NMS_THRESH = 0.45
IMG_SIZE = 640

CLASSES = ("person", "bicycle", "car", "motorbike ", "aeroplane ", "bus ", "train", "truck ", "boat", "traffic light",
           "fire hydrant", "stop sign ", "parking meter", "bench", "bird", "cat", "dog ", "horse ", "sheep", "cow", "elephant",
           "bear", "zebra ", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite",
           "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife ",
           "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza ", "donut", "cake", "chair", "sofa",
           "pottedplant", "bed", "diningtable", "toilet ", "tvmonitor", "laptop	", "mouse	", "remote ", "keyboard ", "cell phone", "microwave ",
           "oven ", "toaster", "sink", "refrigerator ", "book", "clock", "vase", "scissors ", "teddy bear ", "hair drier", "toothbrush ")


def sigmoid(x):
    return 1 / (1 + np.exp(-x))


def xywh2xyxy(x):
    # Convert [x, y, w, h] to [x1, y1, x2, y2]
    y = np.copy(x)
    y[:, 0] = x[:, 0] - x[:, 2] / 2  # top left x
    y[:, 1] = x[:, 1] - x[:, 3] / 2  # top left y
    y[:, 2] = x[:, 0] + x[:, 2] / 2  # bottom right x
    y[:, 3] = x[:, 1] + x[:, 3] / 2  # bottom right y
    return y


def process(input, mask, anchors):

    anchors = [anchors[i] for i in mask]
    grid_h, grid_w = map(int, input.shape[0:2])

    box_confidence = sigmoid(input[..., 4])
    box_confidence = np.expand_dims(box_confidence, axis=-1)

    box_class_probs = sigmoid(input[..., 5:])

    box_xy = sigmoid(input[..., :2])*2 - 0.5

    col = np.tile(np.arange(0, grid_w), grid_w).reshape(-1, grid_w)
    row = np.tile(np.arange(0, grid_h).reshape(-1, 1), grid_h)
    col = col.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2)
    row = row.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2)
    grid = np.concatenate((col, row), axis=-1)
    box_xy += grid
    box_xy *= int(IMG_SIZE/grid_h)

    box_wh = pow(sigmoid(input[..., 2:4])*2, 2)
    box_wh = box_wh * anchors

    box = np.concatenate((box_xy, box_wh), axis=-1)

    return box, box_confidence, box_class_probs


def filter_boxes(boxes, box_confidences, box_class_probs):
    """Filter boxes with box threshold. It's a bit different with origin yolov5 post process!

    # Arguments
        boxes: ndarray, boxes of objects.
        box_confidences: ndarray, confidences of objects.
        box_class_probs: ndarray, class_probs of objects.

    # Returns
        boxes: ndarray, filtered boxes.
        classes: ndarray, classes for boxes.
        scores: ndarray, scores for boxes.
    """
    boxes = boxes.reshape(-1, 4)
    box_confidences = box_confidences.reshape(-1)
    box_class_probs = box_class_probs.reshape(-1, box_class_probs.shape[-1])

    _box_pos = np.where(box_confidences >= OBJ_THRESH)
    boxes = boxes[_box_pos]
    box_confidences = box_confidences[_box_pos]
    box_class_probs = box_class_probs[_box_pos]

    class_max_score = np.max(box_class_probs, axis=-1)
    classes = np.argmax(box_class_probs, axis=-1)
    _class_pos = np.where(class_max_score >= OBJ_THRESH)

    boxes = boxes[_class_pos]
    classes = classes[_class_pos]
    scores = (class_max_score* box_confidences)[_class_pos]

    return boxes, classes, scores


def nms_boxes(boxes, scores):
    """Suppress non-maximal boxes.

    # Arguments
        boxes: ndarray, boxes of objects.
        scores: ndarray, scores of objects.

    # Returns
        keep: ndarray, index of effective boxes.
    """
    x = boxes[:, 0]
    y = boxes[:, 1]
    w = boxes[:, 2] - boxes[:, 0]
    h = boxes[:, 3] - boxes[:, 1]

    areas = w * h
    order = scores.argsort()[::-1]

    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)

        xx1 = np.maximum(x[i], x[order[1:]])
        yy1 = np.maximum(y[i], y[order[1:]])
        xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
        yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])

        w1 = np.maximum(0.0, xx2 - xx1 + 0.00001)
        h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
        inter = w1 * h1

        ovr = inter / (areas[i] + areas[order[1:]] - inter)
        inds = np.where(ovr <= NMS_THRESH)[0]
        order = order[inds + 1]
    keep = np.array(keep)
    return keep


def yolov5_post_process(input_data):
    masks = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
    anchors = [[10, 13], [16, 30], [33, 23], [30, 61], [62, 45],
               [59, 119], [116, 90], [156, 198], [373, 326]]

    boxes, classes, scores = [], [], []
    for input, mask in zip(input_data, masks):
        b, c, s = process(input, mask, anchors)
        b, c, s = filter_boxes(b, c, s)
        boxes.append(b)
        classes.append(c)
        scores.append(s)

    boxes = np.concatenate(boxes)
    boxes = xywh2xyxy(boxes)
    classes = np.concatenate(classes)
    scores = np.concatenate(scores)

    nboxes, nclasses, nscores = [], [], []
    for c in set(classes):
        inds = np.where(classes == c)
        b = boxes[inds]
        c = classes[inds]
        s = scores[inds]

        keep = nms_boxes(b, s)

        nboxes.append(b[keep])
        nclasses.append(c[keep])
        nscores.append(s[keep])

    if not nclasses and not nscores:
        return None, None, None

    boxes = np.concatenate(nboxes)
    classes = np.concatenate(nclasses)
    scores = np.concatenate(nscores)

    return boxes, classes, scores


def draw(image, boxes, scores, classes):
    """Draw the boxes on the image.

    # Argument:
        image: original image.
        boxes: ndarray, boxes of objects.
        classes: ndarray, classes of objects.
        scores: ndarray, scores of objects.
        all_classes: all classes name.
    """
    for box, score, cl in zip(boxes, scores, classes):
        top, left, right, bottom = box
        print('class: {}, score: {}'.format(CLASSES[cl], score))
        print('box coordinate left,top,right,down: [{}, {}, {}, {}]'.format(top, left, right, bottom))
        top = int(top)
        left = int(left)
        right = int(right)
        bottom = int(bottom)

        cv2.rectangle(image, (top, left), (right, bottom), (255, 0, 0), 2)
        cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score),
                    (top, left - 6),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.6, (0, 0, 255), 2)


def letterbox(im, new_shape=(640, 640), color=(0, 0, 0)):
    # Resize and pad image while meeting stride-multiple constraints
    shape = im.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])

    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return im, ratio, (dw, dh)


if __name__ == '__main__':

    # Create RKNN object
    rknn = RKNNLite()

    # load RKNN model
    print('--> Load RKNN model')
    ret = rknn.load_rknn(RKNN_MODEL)
    print(ret)


    # Init runtime environment
    print('--> Init runtime environment')
    ret = rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)  #使用0 1 2三个NPU核心
    # ret = rknn.init_runtime('rk3566')
    if ret != 0:
        print('Init runtime environment failed!')
        exit(ret)
    print('done')

    # Set inputs
    img = cv2.imread(IMG_PATH)
    # img, ratio, (dw, dh) = letterbox(img, new_shape=(IMG_SIZE, IMG_SIZE))
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))

    # Inference
    outputs = rknn.inference(inputs=[img])


    # post process
    input0_data = outputs[0]
    input1_data = outputs[1]
    input2_data = outputs[2]

    input0_data = input0_data.reshape([3, -1]+list(input0_data.shape[-2:]))
    input1_data = input1_data.reshape([3, -1]+list(input1_data.shape[-2:]))
    input2_data = input2_data.reshape([3, -1]+list(input2_data.shape[-2:]))

    input_data = list()
    input_data.append(np.transpose(input0_data, (2, 3, 0, 1)))
    input_data.append(np.transpose(input1_data, (2, 3, 0, 1)))
    input_data.append(np.transpose(input2_data, (2, 3, 0, 1)))

    boxes, classes, scores = yolov5_post_process(input_data)

    img_1 = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
    if boxes is not None:
        draw(img_1, boxes, scores, classes)
    # show output
    cv2.imshow("post process result", img_1)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    rknn.release()

Desktop上にWSL2上で作成したyolov5s.rknnファイルを持ってきます

###WSL2のUbuntu
cd ~/rknn/examples/onnx/yolov5
scp -r yolov5s.rknn オレンジパイのユーザー名@IPアドレス:/home/orangepi/Desktop

検証用で使用するbus.jpgとdataset.txtもDesktopに持っていきます。dataset.txtはいらないかもしれません。

###WSL2のUbuntu
cd ~/rknn/examples/onnx/yolov5
scp -r bus.jpg オレンジパイのユーザー名@IPアドレス:/home/orangepi/Desktop
scp -r dataset.txt オレンジパイのユーザー名@IPアドレス:/home/orangepi/Desktop

動作確認

動作確認していきましょう。
Desktop上に全て置いたため、デスクトップに移動して、実行します。

###Orange pi5上
python deploy.py

バスと人を検出できましたね。おめでとうございます!Orange Pi5上でrknnモデルを動かせました!!

USBカメラテスト

USBカメラでrknnモデルを動かすサンプルを載せておきます。

demo_camera.py
import os
import urllib
import traceback
import time
import datetime as dt
import sys
import numpy as np
import cv2
from rknnlite.api import RKNNLite
 
 
RKNN_MODEL = 'yolov5s.rknn'
DATASET = './dataset.txt'
 
QUANTIZE_ON = True
 
OBJ_THRESH = 0.25
NMS_THRESH = 0.45
IMG_SIZE = 640
 
CLASSES = ("person", "bicycle", "car", "motorbike ", "aeroplane ", "bus ", "train", "truck ", "boat", "traffic light",
           "fire hydrant", "stop sign ", "parking meter", "bench", "bird", "cat", "dog ", "horse ", "sheep", "cow", "elephant",
           "bear", "zebra ", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite",
           "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife ",
           "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza ", "donut", "cake", "chair", "sofa",
           "pottedplant", "bed", "diningtable", "toilet ", "tvmonitor", "laptop	", "mouse	", "remote ", "keyboard ", "cell phone", "microwave ",
           "oven ", "toaster", "sink", "refrigerator ", "book", "clock", "vase", "scissors ", "teddy bear ", "hair drier", "toothbrush ")
 
 
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
 
 
def xywh2xyxy(x):
    # Convert [x, y, w, h] to [x1, y1, x2, y2]
    y = np.copy(x)
    y[:, 0] = x[:, 0] - x[:, 2] / 2  # top left x
    y[:, 1] = x[:, 1] - x[:, 3] / 2  # top left y
    y[:, 2] = x[:, 0] + x[:, 2] / 2  # bottom right x
    y[:, 3] = x[:, 1] + x[:, 3] / 2  # bottom right y
    return y
 
 
def process(input, mask, anchors):
 
    anchors = [anchors[i] for i in mask]
    grid_h, grid_w = map(int, input.shape[0:2])
 
    box_confidence = sigmoid(input[..., 4])
    box_confidence = np.expand_dims(box_confidence, axis=-1)
 
    box_class_probs = sigmoid(input[..., 5:])
 
    box_xy = sigmoid(input[..., :2])*2 - 0.5
 
    col = np.tile(np.arange(0, grid_w), grid_w).reshape(-1, grid_w)
    row = np.tile(np.arange(0, grid_h).reshape(-1, 1), grid_h)
    col = col.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2)
    row = row.reshape(grid_h, grid_w, 1, 1).repeat(3, axis=-2)
    grid = np.concatenate((col, row), axis=-1)
    box_xy += grid
    box_xy *= int(IMG_SIZE/grid_h)
 
    box_wh = pow(sigmoid(input[..., 2:4])*2, 2)
    box_wh = box_wh * anchors
 
    box = np.concatenate((box_xy, box_wh), axis=-1)
 
    return box, box_confidence, box_class_probs
 
 
def filter_boxes(boxes, box_confidences, box_class_probs):
    """Filter boxes with box threshold. It's a bit different with origin yolov5 post process!
    # Arguments
        boxes: ndarray, boxes of objects.
        box_confidences: ndarray, confidences of objects.
        box_class_probs: ndarray, class_probs of objects.
    # Returns
        boxes: ndarray, filtered boxes.
        classes: ndarray, classes for boxes.
        scores: ndarray, scores for boxes.
    """
    boxes = boxes.reshape(-1, 4)
    box_confidences = box_confidences.reshape(-1)
    box_class_probs = box_class_probs.reshape(-1, box_class_probs.shape[-1])
 
    _box_pos = np.where(box_confidences >= OBJ_THRESH)
    boxes = boxes[_box_pos]
    box_confidences = box_confidences[_box_pos]
    box_class_probs = box_class_probs[_box_pos]
 
    class_max_score = np.max(box_class_probs, axis=-1)
    classes = np.argmax(box_class_probs, axis=-1)
    _class_pos = np.where(class_max_score >= OBJ_THRESH)
 
    boxes = boxes[_class_pos]
    classes = classes[_class_pos]
    scores = (class_max_score* box_confidences)[_class_pos]
 
    return boxes, classes, scores
 
 
def nms_boxes(boxes, scores):
    """Suppress non-maximal boxes.
    # Arguments
        boxes: ndarray, boxes of objects.
        scores: ndarray, scores of objects.
    # Returns
        keep: ndarray, index of effective boxes.
    """
    x = boxes[:, 0]
    y = boxes[:, 1]
    w = boxes[:, 2] - boxes[:, 0]
    h = boxes[:, 3] - boxes[:, 1]
 
    areas = w * h
    order = scores.argsort()[::-1]
 
    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
 
        xx1 = np.maximum(x[i], x[order[1:]])
        yy1 = np.maximum(y[i], y[order[1:]])
        xx2 = np.minimum(x[i] + w[i], x[order[1:]] + w[order[1:]])
        yy2 = np.minimum(y[i] + h[i], y[order[1:]] + h[order[1:]])
 
        w1 = np.maximum(0.0, xx2 - xx1 + 0.00001)
        h1 = np.maximum(0.0, yy2 - yy1 + 0.00001)
        inter = w1 * h1
 
        ovr = inter / (areas[i] + areas[order[1:]] - inter)
        inds = np.where(ovr <= NMS_THRESH)[0]
        order = order[inds + 1]
    keep = np.array(keep)
    return keep
 
 
def yolov5_post_process(input_data):
    masks = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
    anchors = [[10, 13], [16, 30], [33, 23], [30, 61], [62, 45],
               [59, 119], [116, 90], [156, 198], [373, 326]]
 
    boxes, classes, scores = [], [], []
    for input, mask in zip(input_data, masks):
        b, c, s = process(input, mask, anchors)
        b, c, s = filter_boxes(b, c, s)
        boxes.append(b)
        classes.append(c)
        scores.append(s)
 
    boxes = np.concatenate(boxes)
    boxes = xywh2xyxy(boxes)
    classes = np.concatenate(classes)
    scores = np.concatenate(scores)
 
    nboxes, nclasses, nscores = [], [], []
    for c in set(classes):
        inds = np.where(classes == c)
        b = boxes[inds]
        c = classes[inds]
        s = scores[inds]
 
        keep = nms_boxes(b, s)
 
        nboxes.append(b[keep])
        nclasses.append(c[keep])
        nscores.append(s[keep])
 
    if not nclasses and not nscores:
        return None, None, None
 
    boxes = np.concatenate(nboxes)
    classes = np.concatenate(nclasses)
    scores = np.concatenate(nscores)
 
    return boxes, classes, scores
 
 
def draw(image, boxes, scores, classes, fps):
    """Draw the boxes on the image.
    # Argument:
        image: original image.
        boxes: ndarray, boxes of objects.
        classes: ndarray, classes of objects.
        scores: ndarray, scores of objects.
        fps: int.
        all_classes: all classes name.
    """
    for box, score, cl in zip(boxes, scores, classes):
        top, left, right, bottom = box
        print('class: {}, score: {}'.format(CLASSES[cl], score))
        print('box coordinate left,top,right,down: [{}, {}, {}, {}]'.format(top, left, right, bottom))
        top = int(top)
        left = int(left)
        right = int(right)
        bottom = int(bottom)
 
        cv2.rectangle(image, (top, left), (right, bottom), (255, 0, 0), 2)
        cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score),
                    (top, left - 6),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.6, (0, 0, 255), 2)
 
def letterbox(im, new_shape=(640, 640), color=(0, 0, 0)):
    # Resize and pad image while meeting stride-multiple constraints
    shape = im.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)
 
    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
 
    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding
 
    dw /= 2  # divide padding into 2 sides
    dh /= 2
 
    if shape[::-1] != new_unpad:  # resize
        im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return im, ratio, (dw, dh)
 
# ==================================
# 如下为改动部分,主要就是去掉了官方 demo 中的模型转换代码,直接加载 rknn 模型,并将 RKNN 类换成了 rknn_toolkit2_lite 中的 RKNNLite 类
# ==================================
 
rknn = RKNNLite()
 
# load RKNN model
print('--> Load RKNN model')
ret = rknn.load_rknn(RKNN_MODEL)
 
# Init runtime environment
print('--> Init runtime environment')
# use NPU core 0 1 2
ret = rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)
if ret != 0:
    print('Init runtime environment failed!')
    exit(ret)
print('done')
 
# Create a VideoCapture object and read from input file
# If the input is the camera, pass 0 instead of the video file name
cap = cv2.VideoCapture(0)
 
# Check if camera opened successfully
if (cap.isOpened()== False): 
  print("Error opening video stream or file")
 
# Read until video is completed
while(cap.isOpened()):
    start = dt.datetime.utcnow()
    # Capture frame-by-frame
    ret, img = cap.read()
    if not ret:
        break
    
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))
 
    # Inference
    print('--> Running model')
    outputs = rknn.inference(inputs=[img])
    print('done')
 
    # post process
    input0_data = outputs[0]
    input1_data = outputs[1]
    input2_data = outputs[2]
 
    input0_data = input0_data.reshape([3, -1]+list(input0_data.shape[-2:]))
    input1_data = input1_data.reshape([3, -1]+list(input1_data.shape[-2:]))
    input2_data = input2_data.reshape([3, -1]+list(input2_data.shape[-2:]))
 
    input_data = list()
    input_data.append(np.transpose(input0_data, (2, 3, 0, 1)))
    input_data.append(np.transpose(input1_data, (2, 3, 0, 1)))
    input_data.append(np.transpose(input2_data, (2, 3, 0, 1)))
 
    boxes, classes, scores = yolov5_post_process(input_data)
    duration = dt.datetime.utcnow() - start
    fps = round(10000000 / duration.microseconds)
 
    # draw process result and fps
    img_1 = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
    cv2.putText(img_1, f'fps: {fps}',
            (20, 20),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.6, (0, 125, 125), 2)
    if boxes is not None:
        draw(img_1, boxes, scores, classes, fps)
 
    # show output
    cv2.imshow("post process result", img_1)
 
    # Press Q on keyboard to  exit
    if cv2.waitKey(25) & 0xFF == ord('q'):
        break
 
# When everything done, release the video capture object
cap.release()
 
# Closes all the frames
cv2.destroyAllWindows()


Discussion

ysuitoysuito

NPUで推論する情報を探していたところたどり着きました。貴重な情報を有難うございます!