FRDM-IMX95ボードを使って、カメラで取得した画像をAIアクセラレータ(NPU)に渡し、物体認識をさせてみます。使うモデルはYOLOv8mです。
[FRDM-IMX95] i.MX 95 カメラアプリケーションの導入 (日本語ブログ)の続きですので、カメラの動作確認ができている前提でご覧ください。
(作業時間:15分) *LinuxイメージがFRDM-IMX95に書き込み、カメラの動作が確認できている前提
You Only Look Once : 1枚の画像全体を1度だけCNNにかけて物体認識をするため非常に高速なアルゴリズムだそうです。物体認識としては非常にポピュラーですね。
Tensorflow Liteで提供されているモデルを、i.MX 95内蔵のNPU(=eIQ Neutron)用に変換する必要があります。変換にはNXPが提供するeIQ Neutron SDKを使う必要があります。また、変換に使用したeIQ Neutron SDKに含まれるNeutron firmware / Neutron driver / TensorFlow neutron delegateを、FRDM-IMX95へコピーする必要があります。
詳細はeIQ Neutron SDKのdoc/NeutronSDKUserGuide.mdをご参照ください。
今回はLinux L6.18.2_1.0.0とeIQ Neutron SDK v3.0.1の組み合わせを使用しています。
まずはeIQ Neutron SDKをダウンロードします。Windows版、Linux版の2種類があります。私はLinux版をWSL2で動かして確認しています。
$ mkdir eiq-neutron-sdk-3.0.1
$ cd eiq-neutron-sdk-3.0.1
$ unzip /path/to/eiq-neutron-sdk-3.0.1.zip
$ ls bin/
neutron-converter tflite-profiler tflite-quantizer
のように、展開先のbinの下に、今回使うneutron-converterがあればOKです。
YOLOv8はUltralytics社がパッケージをリリースしており、NXPではModel zooにてi.MX 8M Plus, i.MX 93向けの手順を用意しています。モデルの準備は、Model zooを参照するのが簡単です。
$ python -m venv venv
$ source venv/bin/activate
(venv) $ pip install ultralytics
pip install ultralyticsは環境によっては1度エラーが出るかもしれません。エラーがでても、何回か繰り返す(筆者の場合は計2回)と最後まで完了しました。完了したら、次のコマンドを実施ください。
(venv) $ yolo export model=yolov8m.pt imgsz=320 format=tflite int8
YoloはPytorchで提供されていますが、eIQ Neutronで動かすためにはTensorflow Lite(TFlite)形式にする必要があり、これはYoloが提供しているコンバータを使っていることになります。実行後、yolov8m_saved_modelディレクトリに、TFlite形式のファイルが生成されます。
$ ls yolov8m_saved_model/
assets metadata.yaml variables yolov8m_float16.tflite yolov8m_full_integer_quant.tflite yolov8m_integer_quant.tflite
fingerprint.pb saved_model.pb yolo-converted.tflite yolov8m_float32.tflite yolov8m_int8.tflite
このモデルはCOCOデータセットを使ってトレーニングされており、80種類の物体が認識対象になっています。Yoloのモデルが出力した結果が何なのかは、別に用意した80種類の対象物が書いてあるラベルファイルを参照する必要があります。以下にソースとなるファイルがあります。
./venv/lib/python3.12/site-packages/ultralytics/cfg/datasets/coco.yaml
(注: pythonのバージョンはお使いの環境によって変わる可能性があります)
ラベルファイルは、先頭の行から対象物が1行ごとに書いてあるテキストですので、以下のようにcoco.yamlのヘッダ部分、フッタ部分を削除して、80行のテキストファイル labels_yolov8.txt を作成ください。
1.1で準備したSDKのディレクトリに移行し、以下コマンドを実行することでeIQ Neutronで実行できるモデルに変換します。
$ cd yolov8m_saved_model/
$ /path/to/eiq-neutron-sdk-3.0.1/bin/neutron-converter --input yolov8m_full_integer_quant.tflite --output yolov8m_int8_neutron.tflite --target imx95
パスは後ほど作成するソースコードで指定できるのでどこでも構わないですが、TFLiteの例がある場所に置くようにします。
$ scp yolov8m_int8_neutron.tflite labels_yolov8.txt root@<IP addr>:/usr/bin/tensorflow-lite-2.19.0/examples
以下のコードで、カメラ入力に対してYolov8mでの認識を行い、検出された物体を緑の枠(バウンディングボックス)で囲むという処理を行います。
という流れです。2ー3をwhileループでフレーム毎実施しています。
#!/usr/bin/env python3
import os
import cv2
import numpy as np
import time
import tflite_runtime.interpreter as tflite
# =========================
# 設定
# =========================
MODEL_PATH = "/usr/bin/tensorflow-lite-2.19.0/examples/yolov8m_int8_neutron.tflite"
LABEL_PATH = "/usr/bin/tensorflow-lite-2.19.0/examples/labels_yolov8.txt" # COCO 80 クラス
CONF_TH = 0.60
IOU_TH = 0.45
MAX_DET = 50
MIN_BOX_W = 4
MIN_BOX_H = 4
GST_PIPELINE = (
"libcamerasrc camera-name=/base/soc/bus@42000000/i2c@42540000/os08a20_mipi@36 ! "
"video/x-raw,format=YUY2,framerate=30/1,width=3840,height=2160 ! "
"imxvideoconvert_g2d rotation=4 ! "
"video/x-raw,width=1280,height=720,format=BGRA ! "
"appsink drop=true max-buffers=1"
)
os.environ["LIBCAMERA_IPA_MODULE_PATH"] = "/usr/lib/libcamera/ipa-nxp-neo-uguzzi/"
os.environ["LIBCAMERA_PIPELINES_MATCH_LIST"] = "nxp/neo,imx8-isi,uvc"
# Neutron 使用時は XNNPACK を無効化
os.environ["TFLITE_DISABLE_XNNPACK"] = "1"
DELEGATE_LIB = "/usr/lib/libneutron_delegate.so"
# =========================
# ユーティリティ
# =========================
def load_labels(path):
with open(path, "r", encoding="utf-8") as f:
return [l.strip() for l in f]
def bbox_iou(box, boxes):
x1 = np.maximum(box[0], boxes[:, 0])
y1 = np.maximum(box[1], boxes[:, 1])
x2 = np.minimum(box[2], boxes[:, 2])
y2 = np.minimum(box[3], boxes[:, 3])
inter_w = np.maximum(0, x2 - x1)
inter_h = np.maximum(0, y2 - y1)
inter = inter_w * inter_h
area1 = (box[2] - box[0]) * (box[3] - box[1])
area2 = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
union = area1 + area2 - inter + 1e-6
return inter / union
def nms(boxes, scores, iou_th):
idxs = np.argsort(scores)[::-1]
keep = []
while len(idxs) > 0:
i = idxs[0]
keep.append(i)
if len(idxs) == 1:
break
ious = bbox_iou(boxes[i], boxes[idxs[1:]])
idxs = idxs[1:][ious < iou_th]
return keep
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
# =========================
# TFLite Interpreter (CPU)
# =========================
delegate = tflite.load_delegate(DELEGATE_LIB)
interpreter = tflite.Interpreter(
model_path=MODEL_PATH,
experimental_delegates=[delegate],
)
interpreter.allocate_tensors()
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
print("Input details:", input_details)
print("Output details:", output_details)
input_index = input_details[0]["index"]
input_shape = input_details[0]["shape"] # [1, H, W, 3]
_, in_h, in_w, _ = input_shape
input_dtype = input_details[0]["dtype"]
output_index = output_details[0]["index"]
out_qparams = output_details[0]["quantization_parameters"]
out_scale = out_qparams["scales"][0] if len(out_qparams["scales"]) > 0 else 1.0
out_zero = out_qparams["zero_points"][0] if len(out_qparams["zero_points"]) > 0 else 0
out_dtype = output_details[0]["dtype"]
labels = load_labels(LABEL_PATH)
# 入力量子化パラメータ(必要な場合)
in_qparams = input_details[0]["quantization_parameters"]
in_scale = in_qparams["scales"][0] if len(in_qparams["scales"]) > 0 else 1.0
in_zero = in_qparams["zero_points"][0] if len(in_qparams["zero_points"]) > 0 else 0
# =========================
# カメラ初期化
# =========================
cap = cv2.VideoCapture(GST_PIPELINE, cv2.CAP_GSTREAMER)
if not cap.isOpened():
raise RuntimeError("Failed to open camera")
prev_time = time.time()
fps = 0.0
frame_id = 0
print("Press 'q' to quit")
# ループの前で一度だけ確保
canvas = np.full((in_h, in_w, 3), 114, dtype=np.uint8)
input_data = np.empty((1, in_h, in_w, 3), dtype=input_dtype)
while True:
ret, frame_bgra = cap.read()
if not ret:
print("Failed to read frame")
break
frame_id += 1
# FPS
now = time.time()
dt = now - prev_time
if dt > 0:
fps = 1.0 / dt
prev_time = now
# BGRA -> BGR (表示用の基本フォーマット)
frame_bgr = cv2.cvtColor(frame_bgra, cv2.COLOR_BGRA2BGR)
h0, w0 = frame_bgr.shape[:2]
# =========================
# 前処理(letterbox)
# =========================
scale = min(in_w / w0, in_h / h0)
nw, nh = int(w0 * scale), int(h0 * scale)
resized = cv2.resize(frame_bgr, (nw, nh))
canvas[:] = 114
top = (in_h - nh) // 2
left = (in_w - nw) // 2
canvas[top:top + nh, left:left + nw] = resized
img_rgb = canvas
# 入力テンソル作成
if input_dtype == np.uint8:
input_data[0, ...] = img_rgb
else:
# int8 モデルの場合
img_f = img_rgb.astype(np.float32) / 255.0
input_data[0, ...] = (img_f / in_scale + in_zero).astype(np.int8)
# =========================
# 推論
# =========================
interpreter.set_tensor(input_index, input_data)
interpreter.invoke()
out_raw = interpreter.get_tensor(output_index)[0] # shape: (84, 2100) 期待
# 逆量子化
if np.issubdtype(out_dtype, np.integer):
out_f = (out_raw.astype(np.float32) - out_zero) * out_scale
else:
out_f = out_raw.astype(np.float32)
# shape 正規化: (84, 2100) → (2100, 84)
if out_f.ndim == 3:
if out_f.shape[0] == 1:
out_f = out_f[0]
if out_f.ndim == 2 and out_f.shape[0] in (84, 85):
out = out_f.reshape(out_f.shape[0], -1).transpose(1, 0)
elif out_f.ndim == 2 and out_f.shape[1] in (84, 85):
out = out_f
else:
out = out_f.reshape(-1, out_f.shape[-1])
num_det, num_ch = out.shape
boxes = []
scores = []
classes = []
# =========================
# 後処理
# =========================
# out: (num_det, 84) を仮定 [cx, cy, w, h, 80クラスlogits]
if num_ch != 84:
boxes = []
scores = []
classes = []
else:
cx = out[:, 0]
cy = out[:, 1]
w = out[:, 2]
h = out[:, 3]
# すべての検出に対して一括で sigmoid & argmax
cls_logits = out[:, 4:] # (N, 80)
cls_probs = 1.0 / (1.0 + np.exp(-cls_logits)) # sigmoid 全体
cls_id = np.argmax(cls_probs, axis=1) # (N,)
conf = cls_probs[np.arange(num_det), cls_id]
# 信頼度しきい値で一括フィルタ
mask = conf >= CONF_TH
if not np.any(mask):
boxes = []
scores = []
classes = []
else:
cx = cx[mask]
cy = cy[mask]
w = w[mask]
h = h[mask]
conf = conf[mask]
cls_id = cls_id[mask]
# 0〜1 正規化座標 → 入力解像度座標
x1 = (cx - w / 2.0) * in_w
y1 = (cy - h / 2.0) * in_h
x2 = (cx + w / 2.0) * in_w
y2 = (cy + h / 2.0) * in_h
# 元画像座標へ戻す(letterbox 補正)
x1 = (x1 - left) / scale
y1 = (y1 - top) / scale
x2 = (x2 - left) / scale
y2 = (y2 - top) / scale
# 最小サイズで一括フィルタ
bw = x2 - x1
bh = y2 - y1
size_mask = (bw >= MIN_BOX_W) & (bh >= MIN_BOX_H)
if not np.any(size_mask):
boxes = []
scores = []
classes = []
else:
x1 = x1[size_mask]
y1 = y1[size_mask]
x2 = x2[size_mask]
y2 = y2[size_mask]
conf = conf[size_mask]
cls_id = cls_id[size_mask]
boxes = np.stack([x1, y1, x2, y2], axis=1).astype(np.float32)
scores = conf.astype(np.float32)
classes = cls_id.astype(np.int32)
if isinstance(boxes, np.ndarray):
has_det = boxes.shape[0] > 0
else:
has_det = len(boxes) > 0
if has_det:
if not isinstance(boxes, np.ndarray):
boxes = np.array(boxes, dtype=np.float32)
if not isinstance(scores, np.ndarray):
scores = np.array(scores, dtype=np.float32)
keep = nms(boxes, scores, IOU_TH)
keep = sorted(keep, key=lambda i: scores[i], reverse=True)[:MAX_DET]
for i in keep:
x1, y1, x2, y2 = boxes[i]
cls_id = int(classes[i])
conf = scores[i]
label = labels[cls_id] if 0 <= cls_id < len(labels) else f"id{cls_id}"
x1 = int(max(0, min(w0 - 1, x1)))
y1 = int(max(0, min(h0 - 1, y1)))
x2 = int(max(0, min(w0 - 1, x2)))
y2 = int(max(0, min(h0 - 1, y2)))
cv2.rectangle(frame_bgr, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(
frame_bgr,
f"{label}:{conf:.2f}",
(x1, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 255, 0),
2,
)
cv2.putText(
frame_bgr,
f"FPS: {fps:.1f}",
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX,
0.8,
(0, 255, 255),
2,
)
cv2.imshow("i.MX95 CPU (YOLOv8 via TFLite + OpenCV)", frame_bgr)
if (cv2.waitKey(1) & 0xFF) == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
こちらをrun-yolo-npu.pyとしてFRDM-IMX95へ保存してください。
root@imx95-15x15-lpddr4x-frdm:~# python3 run-yolo.py
物体認識としてよく使われるYoloを、カメラのライブ画像に対して実施してみました。
カメラの入力サイズ、フォーマットをYOLOv8mの入力に合わせたり、OpenCVで扱えるフォーマットに変換したりなど、それなりに調整しなければならないことはありますが、この記事を参考にして頂ければすぐに動かすことができるかと思います。
*この記事には、AI生成コードをベースに投稿者が内容を確認した内容が含まれます。
=========================
本投稿の「Comment」欄にコメントをいただいても、現在返信に対応しておりません。
お手数をおかけしますが、お問い合わせの際には「NXPへの技術質問 - 問い合わせ方法 (日本語ブログ)」をご参照ください。
(既に弊社NXP代理店、もしくはNXPとお付き合いのある方は、直接担当者へご質問いただいてもかまいません。)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.