CCCMKホールディングス TECH Labの Tech Blog

TECH Labスタッフによる格闘記録やマーケティング界隈についての記事など

Jetson Nanoをセットアップして人物検出モデルを動かしてみました!

こんにちは、技術開発の三浦です。

「第二の人生」について、最近ふと考えました。まだまだ先の話ですが、仕事をリタイアしたら自分は何をしたいのかなぁと。ゲームをしたり、自然の中で過ごすのも良さそうです。そして何よりもテクノロジーはきっとこれからも進化していくので、いくつになっても最新のテクノロジーを追いかけていけたらいいな、と思います。

さて以前購入した"Jetson Nano Developer Kit"を久しぶりに触ってみたくなり、初期設定から"Google Colab"で学習したPyTorchの人物検出モデルを動かすところまでやってみました。けっこう手順が多かったので、何をしたのか忘れないようにまとめたいと思います。

Jetson

JetsonはNvidia社が発売している組み込みシングルボードコンピュータシリーズです。最近新しい"Jetson Orin Nano"が発表され、話題になりました。今回取り上げる"Jetson Nano"は2019年に発売された製品で、発売されたときは「こんなに小さいのに深層学習の処理が動かせるんだ!」とびっくりしたのを覚えています。

Jetson NanoはNvidiaのGPUを搭載しており、機械学習の推論処理に最適化されているため、例えば物体検出モデルも動かすことが出来ます。USBカメラやセンサーを接続することも出来ますので、AIを搭載したちょっとしたロボットを作ることも出来ます。

機器そのものはJetsonモジュールと呼ばれ、Jetsonモジュール上でアプリを動かすために必要となるSDKは"JetPack SDK"としてNvidia社から提供されています。このSDKの中にはJetsonモジュールに搭載されているTegra SOC向けのLinux(L4T)やCUDA, TensorRT, NVIDIA Container Runtimeなどのライブラリが含まれています。JetPackはJetson Nanoの初期セットアップ時にダウンロードし、SDカードに書き込んでインストールします。

Jetson Nano初期セットアップ

Jetson Nanoをセットアップする際、Developer Kitには含まれていない、必要なものがいくつかあるので事前に忘れずに用意します。私の場合はmicroSDカード(32GB)とmicroUSB-USBケーブル(シリアル通信用)、DC電源ケーブルを使用しました。セットアップの方法はJetsonにディスプレイやキーボードなどを接続して行う"setup with display attached"の方法と、別のPCからシリアル通信でJetsonに接続して行う"headless mode"の方法を選ぶことが出来て、それによっても必要になるものが変わります。(私は"headless mode"でセットアップしました。)

セットアップ手順はNvidia社のこちらのページで確認することが出来ます。

developer.nvidia.com

後で足りないものが出てこないよう、セットアップを始める前に一通り読んでおいた方がよいと思います。私は"headless mode"でセットアップしようとして、途中でDC電源ケーブルが手元に無いことに気づき、作業が一時中断してしまいました。

Tera Termを使ってシリアル通信でJetson Nanoにアクセスしました

"headless mode"ではディスプレイをJetson Nanoに接続しなくてもセットアップを行うことが出来ます。

Jetson NanoでWiFiドングルを使用する

Jetson NanoにはWiFi通信機器が搭載されていないので、WiFiに接続するためには別途WiFi機器を用意する必要があります。たまたま手元にUSBタイプのWiFiドングルがあったので、そちらを使用して上手く接続することが出来ました。

NvidiaのDeveloper Forumで推奨されるドングルが紹介されています。

forums.developer.nvidia.com

こちらのページにもリンクが貼っているのですが、以下のページのWirelessの項目にもいくつか機器がリストアップされています。

elinux.org

ドングルを接続したら、nmcliコマンドを使ってWiFi接続のセットアップを行うことが出来ました。

  • ドングルが認識されているか確認
nmcli d
  • WiFiをONにする
nmcli r wifi on
  • SSIDリストを表示する
nmcli d wifi list
  • 対照のSSIDに接続する(sudoが必要)
sudo nmcli d wifi connect "SSID" password "password"

ここまではシリアル通信で行いましたが、以降は同じネットワーク上の端末からSSH接続してJetsonにアクセス出来るようになります。

機械学習の環境を立ち上げる

初期セットアップが完了し、インターネットへの通信経路を確保出来たので、さっそくJetson Nano上で機械学習の環境を立ち上げてみました。

JetPack SDKにはNvidia Runtime Containerが含まれていて、Nvidia Runtime Containerを使用するとDocker ContainerからJetson NanoのGPUなどに簡単にアクセスすることが出来ます。またJetson Nanoに直接影響を及ぼさずに色々と試すことが出来ますので、今回はDocker Containerで機械学習の環境を立ち上げてみました。

l4t-mlというDocker ImageにはTensorFlow, PyTorch, Scikit-Learn, JupyterLabといった機械学習に必要なライブラリが一通り含まれているのでこのDocker ImageでContainerを立ち上げます。Jetson Nanoにインストールされたl4tのバージョンによってl4t-mlの対応バージョンが異なるので、まずJetson Nanoのl4tのバージョンを確認します。以下のコマンドを実行します。

cat /etc/nv_tegra_release

出力結果

R32 (release), REVISION: 7.1, GCID:...

l4tのバージョンは、32.7.1であることが分かりました。これに対応するl4t-mlのDokcer Imageは以下のページよりl4t-ml:r32.7.1-py3であることが分かります。

catalog.ngc.nvidia.com

まずDocker Imageをpullします。

sudo docker pull nvcr.io/nvidia/l4t-ml:r32.7.1-py3

次にDocker Containerを立ち上げます。

sudo docker run -it --rm --runtime nvidia --network host nvcr.io/nvidia/l4t-ml:r32.7.1-py3

このContainerを起動すると、同時にJupyterLabも起動します。http://"Jetson Nanoのipアドレス":8888で同じネットワーク上の端末からJupyterLabにアクセス出来ます。JupyterLabの初期パスワードはnvidiaでした。

USBカメラを接続する

手元にあったUSBカメラをJetson Nanoに接続し、USBカメラで撮影した画像に対して物体検出を行ってみました。Docker ContainerからUSBカメラにアクセスするにはContainer起動時に--deviceオプションを指定します。USBカメラが/dev/video0に接続されている場合は

sudo docker run -it --rm --runtime nvidia --network --device /dev/video0 host nvcr.io/nvidia/l4t-ml:r32.7.1-py3

のようにしてContainerを起動します。

Google Colabで作った人物検出モデルを使用する

以前こちらの記事でPyTorchでFaseterRCNNの人物検出モデルを学習しました。

techblog.cccmk.co.jp

この内容に従って構築したモデルをJetson Nanoで動かしてみます。 (backboneのモデルはResNetからMobileNetに変更しました。)

まずモデルは以下のような構造になっています。

from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torchvision.models.detection.faster_rcnn import fasterrcnn_mobilenet_v3_large_320_fpn

num_classes = 2 #0: background, 1: person
model = fasterrcnn_mobilenet_v3_large_320_fpn()
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

また、Google Colabのtorchtorchvisionのバージョンを確認しておきました。

print(torch.__version__)
print(torchvision.__version__)

出力結果

1.12.1+cu113
0.13.1+cu113

Jetson Nanoでこのモデルを動かすために、学習済みのパラメータを出力します。 以下のコードを実行しました。

torch.save(model.state_dict(),PATH)

出力されたパラメータファイルをJetson Nanoに転送し、JupyterLabでロードしてみます。

model.load_state_dict(torch.load(PATH))

しかし、以下のようなエラーが発生し、ロードに失敗してしまいました・・・。

RuntimeError: Error(s) in loading state_dict for FasterRCNN:
    Missing key(s) in state_dict: "backbone.fpn.inner_blocks.0.weight", "backbone.fpn.inner_blocks.0.bias", "backbone.fpn.inner_blocks.1.weight", "backbone.fpn.inner_blocks.1.bias", "backbone.fpn.layer_blocks.0.weight", "backbone.fpn.layer_blocks.0.bias", "backbone.fpn.layer_blocks.1.weight", "backbone.fpn.layer_blocks.1.bias", "rpn.head.conv.weight", "rpn.head.conv.bias". 
    Unexpected key(s) in state_dict: "backbone.fpn.inner_blocks.0.0.weight", "backbone.fpn.inner_blocks.0.0.bias", "backbone.fpn.inner_blocks.1.0.weight", "backbone.fpn.inner_blocks.1.0.bias", "backbone.fpn.layer_blocks.0.0.weight", "backbone.fpn.layer_blocks.0.0.bias", "backbone.fpn.layer_blocks.1.0.weight", "backbone.fpn.layer_blocks.1.0.bias", "rpn.head.conv.0.0.weight", "rpn.head.conv.0.0.bias". 

エラーの内容から、以下のパラメータの読み込みで失敗しているようです。

backbone.fpn.inner_blocks.0.0.weight
backbone.fpn.inner_blocks.0.0.bias
backbone.fpn.inner_blocks.1.0.weight
backbone.fpn.inner_blocks.1.0.bias
backbone.fpn.layer_blocks.0.0.weight
backbone.fpn.layer_blocks.0.0.bias
backbone.fpn.layer_blocks.1.0.weight
backbone.fpn.layer_blocks.1.0.bias
rpn.head.conv.0.0.weight
rpn.head.conv.0.0.bias

Jetson Nanoで起動しているDocker Containerl4t-ml:r32.7.1-py3torchtorchvisionのバージョンを確認します。

1.10.0
0.11.0a0+fa347eb

Google Colabとバージョンが異なっており、それに起因するエラーのようです。実際l4t-ml:r32.7.1-py3で同様にモデルを定義し、該当する箇所のパラメータの名称をリストアップしてみると以下のように名称が異なっています。

backbone.fpn.inner_blocks.0.weight
backbone.fpn.inner_blocks.0.bias
backbone.fpn.inner_blocks.1.weight
backbone.fpn.inner_blocks.1.bias
backbone.fpn.layer_blocks.0.weight
backbone.fpn.layer_blocks.0.bias
backbone.fpn.layer_blocks.1.weight
backbone.fpn.layer_blocks.1.bias
rpn.head.conv.weight
rpn.head.conv.bias

ですのでGoogle Colabでパラメータを出力する時に、以下のように名称を変更して出力するようにしました。

colabo_param = [
  'backbone.fpn.inner_blocks.0.0.weight',
  'backbone.fpn.inner_blocks.0.0.bias',
  'backbone.fpn.inner_blocks.1.0.weight',
  'backbone.fpn.inner_blocks.1.0.bias',
  'backbone.fpn.layer_blocks.0.0.weight',
  'backbone.fpn.layer_blocks.0.0.bias',
  'backbone.fpn.layer_blocks.1.0.weight',
  'backbone.fpn.layer_blocks.1.0.bias',
  'rpn.head.conv.0.0.weight',
  'rpn.head.conv.0.0.bias',
]
jetson_param= [
  'backbone.fpn.inner_blocks.0.weight',
  'backbone.fpn.inner_blocks.0.bias',
  'backbone.fpn.inner_blocks.1.weight',
  'backbone.fpn.inner_blocks.1.bias',
  'backbone.fpn.layer_blocks.0.weight',
  'backbone.fpn.layer_blocks.0.bias',
  'backbone.fpn.layer_blocks.1.weight',
  'backbone.fpn.layer_blocks.1.bias',
  'rpn.head.conv.weight',
  'rpn.head.conv.bias',
]

#Jetson paramに対応したパラメータの保存

weight_dict = {}

for k, v in model.state_dict().items():
  if k in colabo_param:
    #parameter keyをJetson Nano側に対応させる
    k = jetson_param[colabo_param.index(k)]
  weight_dict[k] = v

torch.save(weight_dict, PATH)

これでJetson Nanoで正常に読み込める形でモデルのパラメータファイルを出力することが出来ました。

Google Colabで学習したモデルをJetson Nanoで動かし、USBカメラの画像に対して人物検出を行う

最後にGoogle Colabで学習したモデルをJetson Nanoで動かしてUSBカメラの画像に対して人物検出を行うコードを以下に掲載させて頂きます。

from IPython.dislpay import dislpay
from PIL import Image
import cv2
import torch
import torchvision
import torchvision.transforms as T
from torchvision.utils import draw_bounding_boxes
from torchvision.models.detection.faster_rcnn import fasterrcnn_mobilenet_v3_large_320_fpn
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor

#モデル定義
num_classes = 2
model = fasterrcnn_mobilenet_v3_large_320_fpn()
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

#Google Colabで学習し、出力したパラメータファイルの読み込み
model.load_state_dict(torch.load(PATH))
model = model.cuda().eval()

#USBカメラで画像を撮影し、人物検知を行う

try:
    #USBカメラで撮影
    cap = cv2.VideoCapture(0)
    _, cv_img = cap.read()

    #OpenCV形式からPillow形式に変換
    pil_img = cv_img.copy()
    pil_img = cv2.cvtColor(pil_img, cv2.COLOR_BGR2RGB)
    pil_img = Image.fromarray(pil_img)

    #モデルによる推論
    input_img = T.PILToTensor()(pil_img).cuda()
    input_img = T.ConvertImageDtype(torch.float)(input_img)
    input_img = torch.unsqueeze(input_img, axis=0)
    output = model(input_img)
    boxes = torch.squeeze(torch.stack([o['boxes'] for o in output]),axis=0)
    
    #推論結果の描画
    draw_img = draw_bounding_boxes(T.PILToTensor()(pil_img), boxes, colors='red', width=5)
    display(T.ToPILImage()(draw_img))
except Exception as e:
    print(e)

finally:
    cap.release()

USBカメラで撮影した画像に対する人物検出の結果

頭の先っちょだけですが、人物として検出されました!

まとめ

ということで、今回はJetson Nanoの初期セットアップから最近作った人物検出モデルを動かしてみた話までを紹介させて頂きました。クラウド環境で学習したモデルがエッジデバイスで動く様子を見ると、なんだか感慨深いものがあります。JetPackにはJetsonモジュールの性能を効率的に利用して深層学習の推論処理を行うことが出来るTensorRTが含まれていて、今回それも試してみたかったのですが時間が間に合わず、至りませんでした。これはまた今度試してみたいと思います。

また、来年発売予定の"Jetson Orin Nano"が今からとても楽しみです!

スマートフォンの画面の中の人物(自分)も検知しました。