[FRDM-IMX95] i.MX 95 カメラ+AI - YOLOv8mの例 (日本語ブログ)

cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

[FRDM-IMX95] i.MX 95 カメラ+AI - YOLOv8mの例 (日本語ブログ)

shigenobukatagi
NXP Employee
NXP Employee
0 0 101

はじめに

FRDM-IMX95ボードを使って、カメラで取得した画像をAIアクセラレータ(NPU)に渡し、物体認識をさせてみます。使うモデルはYOLOv8mです。
[FRDM-IMX95] i.MX 95 カメラアプリケーションの導入 (日本語ブログ)の続きですので、カメラの動作確認ができている前提でご覧ください。

(作業時間:15分) *LinuxイメージがFRDM-IMX95に書き込み、カメラの動作が確認できている前提

 

Yoloについて

You Only Look Once : 1枚の画像全体を1度だけCNNにかけて物体認識をするため非常に高速なアルゴリズムだそうです。物体認識としては非常にポピュラーですね。

 

目次

 

1. eIQ Neutron用へのモデル変換

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の組み合わせを使用しています。

 

1.1 SDKの準備

まずは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です。

 

1.2 モデルの準備、TFliteへの変換

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

 

1.3 ラベルファイルの作成

このモデルはCOCOデータセットを使ってトレーニングされており、80種類の物体が認識対象になっています。Yoloのモデルが出力した結果が何なのかは、別に用意した80種類の対象物が書いてあるラベルファイルを参照する必要があります。以下にソースとなるファイルがあります。

./venv/lib/python3.12/site-packages/ultralytics/cfg/datasets/coco.yaml

(注: pythonのバージョンはお使いの環境によって変わる可能性があります)

shigenobukatagi_0-1776388033195.png

ラベルファイルは、先頭の行から対象物が1行ごとに書いてあるテキストですので、以下のようにcoco.yamlのヘッダ部分、フッタ部分を削除して、80行のテキストファイル labels_yolov8.txt を作成ください。

shigenobukatagi_1-1776388107849.png

shigenobukatagi_2-1776388231530.png

 

1.4 モデルの変換

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

 

1.5 モデル、ラベルファイルをターゲットにコピー

パスは後ほど作成するソースコードで指定できるのでどこでも構わないですが、TFLiteの例がある場所に置くようにします。

$ scp yolov8m_int8_neutron.tflite labels_yolov8.txt root@<IP addr>:/usr/bin/tensorflow-lite-2.19.0/examples

 

2. Pythonコード

以下のコードで、カメラ入力に対してYolov8mでの認識を行い、検出された物体を緑の枠(バウンディングボックス)で囲むという処理を行います。

  1. カメラの入力セットアップ
  2. 320x160にリサイズしたイメージデータをYoloで推論、物体の検出とバウンディングボックスの座標を受け取る
  3. バウンディングボックスの座標を、ディスプレイに表示させるフレームデータに合致するように計算し直し、合成させてディスプレイ出力させる

という流れです。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へ保存してください。

 

3. 実機動作

root@imx95-15x15-lpddr4x-frdm:~# python3 run-yolo.py

(view in My Videos)

 

おわりに

物体認識としてよく使われるYoloを、カメラのライブ画像に対して実施してみました。
カメラの入力サイズ、フォーマットをYOLOv8mの入力に合わせたり、OpenCVで扱えるフォーマットに変換したりなど、それなりに調整しなければならないことはありますが、この記事を参考にして頂ければすぐに動かすことができるかと思います。

*この記事には、AI生成コードをベースに投稿者が内容を確認した内容が含まれます。

 

=========================

本投稿の「Comment」欄にコメントをいただいても、現在返信に対応しておりません。
お手数をおかけしますが、お問い合わせの際には「NXPへの技術質問 問い合わせ方法 (日本語ブログ)」をご参照ください。
(既に弊社NXP代理店、もしくはNXPとお付き合いのある方は、直接担当者へご質問いただいてもかまいません。)