Megadetector model MDV6-yolov10.ptをherに変換する

Raspberry Pi5 + AI HAT+2 (HEILO-10H)の組み合わせでも動作するMegadetectorの軽量model
「MDV6-yolov10-c」をインストールに必要なhefファイルに変換する手順を書きます。

モデルのダウンロード

model 「MDV6-yolov10-c」はここからダウンロード出来ます。
https://zenodo.org/records/20348979  (無料 登録が必要) 

解凍してみると「model.onnx」が添付されていました。
ここから作業を始めます。
今回のworkspaceは
/mnt/c/Users/name/MyDocker2/YOLOV10C
で行います。(nameはあなたの実際のユーザーネームに変えて下さい)
MDV6b-yolov9cの変換と同様に事前に以下の3つのファイルをworkspace内YOLOV10C配置しておき
ます。
本ブログ記事の「Raspberry Pi5 と AI HAT+2で作るMegadetectorによるAI検知システム」
と同じやり方です。(記事を参照下さい)

1.hailo_dataflow_compiler-5.4.0-py3-none-linux_x86_64.whl
2.PICTURE (ファオルダ内に5,000枚の静止画)
3.model.onnx

1.DFC(hailo dataflow compiler)用環境を作成

本ブログ記事の「Raspberry Pi5 と AI HAT+2で作るMegadetectorによるAI検知システム」
と同じやり方です。
記事を参照下さい、 「終わったら、Model Zooが使えるか確認します。」の手前までを同様に行います。

2.HARの出力

source ~/hailo54-env/bin/activate
export PATH=/usr/local/cuda-12.8/bin:$PATH
export XLA_FLAGS=--xla_gpu_cuda_data_dir=/usr/local/cuda-12.8

MODEL="/mnt/c/Users/name/MyDocker2/YOLOV10C/model.onnx"

python - "$MODEL" <<'PY'
import sys
from hailo_sdk_client import ClientRunner

runner = ClientRunner(hw_arch="hailo10h")
runner.translate_onnx_model(
    sys.argv[1],
    net_name="mdv6_yolov10c_nms",
    start_node_names=["images"],
    end_node_names=[
        "/model.23/one2one_cv3.0/one2one_cv3.0.2/Conv",
        "/model.23/one2one_cv2.0/one2one_cv2.0.2/Conv",
        "/model.23/one2one_cv2.1/one2one_cv2.1.2/Conv",
        "/model.23/one2one_cv3.1/one2one_cv3.1.2/Conv",
        "/model.23/one2one_cv3.2/one2one_cv3.2.2/Conv",
        "/model.23/one2one_cv2.2/one2one_cv2.2.2/Conv",
    ],
    net_input_shapes={"images": [1, 3, 640, 640]},
)
output_path = "/home/name/mdv6_yolov10c_nms.har"
runner.save_har(output_path)
print("HAR:", output_path)
PY

3.最適化

 元の5,000枚の赤外線画像から新しくキャリブレーション画像1,024枚の作成。

SRC="/mnt/c/Users/name/MyDocker2/YOLOV10C/PICTURE"
DST="$HOME/mdv6_yolov10c_calib_1024"

python3 - "$SRC" "$DST" <<'PY'
import random
import shutil
import sys
from pathlib import Path

src = Path(sys.argv[1])
dst = Path(sys.argv[2])

if not src.is_dir():
    raise SystemExit(f"画像フォルダが見つかりません: {src}")
if dst.exists():
    raise SystemExit(f"作成先が既にあります: {dst}")

files = sorted(
    p for p in src.iterdir()
    if p.is_file() and p.suffix.lower() in {".jpg", ".jpeg", ".png"}
)
if len(files) < 1024:
    raise SystemExit(f"画像が1,024枚未満です: {len(files)}枚")

dst.mkdir(parents=True)
selected = random.Random(42).sample(files, 1024)
for path in selected:
    shutil.copy2(path, dst / path.name)

print(f"作成完了: {dst}")
print(f"コピーした画像: {len(selected)}枚")
PY

付属NMS設定はCOCO用の80クラス・閾値0.3なので、MDV6用に3クラス・閾値0.2へ変更してから最
適化します。
まず、既存設定を変更せずにカスタム設定を作ります。

python3 - <<'PY'
import json
from pathlib import Path

zoo = Path.home() / "hailo_model_zoo/hailo_model_zoo/cfg"
dst = Path.home() / "mdv6_yolov10c_config"
dst.mkdir(exist_ok=True)

# YOLOv10nのNMS設定をMDV6用にコピーして変更
cfg = json.loads(
    (zoo / "postprocess_config/yolov10n_nms_config.json").read_text()
)
cfg["classes"] = 3
cfg["nms_scores_th"] = 0.2

json_path = dst / "mdv6_yolov10c_nms_config.json"
json_path.write_text(json.dumps(cfg, indent=2) + "\n")

# YOLOv10nのALLSを複製し、上で作ったNMS設定を参照させる
alls = (zoo / "alls/generic/yolov10n.alls").read_text()
alls = alls.replace(
    '"../../postprocess_config/yolov10n_nms_config.json"',
    f'"{json_path}"'
)
alls_path = dst / "mdv6_yolov10c.alls"
alls_path.write_text(alls)

print("ALLS:", alls_path)
print("NMS JSON:", json_path)
print("NMS classes:", cfg["classes"])
print("NMS score threshold:", cfg["nms_scores_th"])
PY

表示がNMS classes: 3、NMS score threshold: 0.2になったら、最適化を実行します。

mkdir -p "$HOME/mdv6_yolov10c_results"
cd "$HOME/mdv6_yolov10c_results"

source ~/hailo54-env/bin/activate
export PATH=/usr/local/cuda-12.8/bin:$PATH
export XLA_FLAGS=--xla_gpu_cuda_data_dir=/usr/local/cuda-12.8

hailomz optimize yolov10n \
  --har /home/name/mdv6_yolov10c_nms.har \
  --calib-path /home/name/mdv6_yolov10c_calib_1024 \
  --classes 3 \
  --hw-arch hailo10h \
  --model-script /home/name/mdv6_yolov10c_config/mdv6_yolov10c.alls

4.HEFの出力

cd "$HOME/mdv6_yolov10c_results"

hailomz compile yolov10n \
  --har /home/name/mdv6_yolov10c_results/yolov10n.har \
  --hw-arch hailo10h \
  --model-script /home/name/mdv6_yolov10c_config/mdv6_yolov10c.alls

結果
[info] mdv6_yolov10c_nms Successful Compilation (duration: 13m 27s)
[info] Saved HAR to: /home/name/mdv6_yolov10c_results/yolov10n.har
HEF file written to yolov10n.hef

[info] Saved HAR to: /home/name/mdv6_yolov10c_results/yolov10n.har
HEF file written to yolov10n.hef

/home/name/mdv6_yolov10c_results
に「yolov10n.hef」の名前でhefファイルが保存されています。
workspaceに移動して保管、その後、Raspberry Pi5へコピーします。
cp yolov10n.her /home/name/MyDocker2/YOLOV10C
最終の名前は「mdv6_yolov10c.hef」でリネームして保管します。





Raspberry Pi5 + AI HAT+2 で動かす

「mdv6-yolov9」の場合と同様に今回書き出した「mdv6_yolov10c.her」をRaspberry Pi + AI HAT+2
 の以下環境にコピーします。
 /home/pi/HAT
 HATは仮想環境です、「Raspberry Pi5 と AI HAT+2で作るMegadetectorによるAI検知システム」の
 「インストール手順」の部分を同様に事前に行って下さい。)
 

 スクリプトは以下になります。

import cv2
import json
import os
import time
import numpy as np
import paho.mqtt.client as mqtt

from hailo_platform import HEF, VDevice, FormatType


# ============================================================
# 設定
# ============================================================

HEF_PATH = "/home/pi/HAT/mdv6_yolov10c.hef"

CAMERAS = [
    {
        "id": "tapo310a_1",
        # USER と PASSWORD を実際のカメラ認証情報に置き換えてください
        "rtsp_url": "rtsp://id:passwd@192.168.1.101:554/stream1",
    },
]

MQTT_BROKER = "192.168.1.200"#Set your MQTT Server Address
MQTT_PORT = 1883
MQTT_TOPIC = "megadt"

SAVE_DIR = "detected_images"
SAVE_INTERVAL_SEC = 5
CLASS_COOLDOWN_SEC = 5

# MegaDetector V6のクラスID
CLASS_NAMES = {
    0: "animal",
    1: "person",
    2: "vehicle",
}

# HEF内蔵NMSのスコア閾値より高い値で、アプリ側でも絞り込みます
CLASS_THRESHOLDS = {
    0: 0.50,
    1: 0.60,
    2: 0.80,
}

INPUT_SIZE = 640

# 今回作成したHEFで確認済みの出力名と形状
OUTPUT_NMS_NAME = "mdv6_yolov10c_nms/yolov8_nms_postprocess"
EXPECTED_NMS_SHAPE = (3, 5, 100)

os.makedirs(SAVE_DIR, exist_ok=True)


# ============================================================
# 前処理
# ============================================================

def letterbox_rgb(frame_bgr, size=INPUT_SIZE):
    """縦横比を維持して640x640にレターボックスし、RGB画像を返す。"""
    height, width = frame_bgr.shape[:2]

    scale = min(size / width, size / height)
    resized_w = int(round(width * scale))
    resized_h = int(round(height * scale))

    resized = cv2.resize(
        frame_bgr,
        (resized_w, resized_h),
        interpolation=cv2.INTER_LINEAR,
    )

    pad_w = size - resized_w
    pad_h = size - resized_h
    left = pad_w // 2
    right = pad_w - left
    top = pad_h // 2
    bottom = pad_h - top

    padded = cv2.copyMakeBorder(
        resized,
        top,
        bottom,
        left,
        right,
        cv2.BORDER_CONSTANT,
        value=(114, 114, 114),
    )

    rgb = cv2.cvtColor(padded, cv2.COLOR_BGR2RGB)
    return np.ascontiguousarray(rgb), scale, left, top


# ============================================================
# 後処理
# ============================================================
def decode_hailo_nms(nms_output, scale, pad_left, pad_top, orig_w, orig_h):
    """
    Hailo NMSの出力を検出結果に変換する。
    クラスごとに「検出数、続いて検出データ」が可変長で並ぶ。
    検出データ: y_min, x_min, y_max, x_max, score
    """
    raw = np.asarray(nms_output, dtype=np.float32).reshape(-1)

    max_proposals = 100
    values_per_detection = 5
    expected_size = 3 * (1 + max_proposals * values_per_detection)

    if raw.size != expected_size:
        raise RuntimeError(
            f"NMS出力サイズが想定外です。想定={expected_size}, 実際={raw.size}"
        )

    detections = []
    cursor = 0

    for class_id in range(3):
        if cursor >= raw.size:
            raise RuntimeError("NMS出力が途中で終わっています")

        count = int(round(float(raw[cursor])))
        count = max(0, min(count, max_proposals))

        data_start = cursor + 1
        data_end = data_start + count * values_per_detection
        boxes = raw[data_start:data_end].reshape(count, values_per_detection)

        for y1, x1, y2, x2, score in boxes:
            score = float(score)

            if score < CLASS_THRESHOLDS[class_id]:
                continue

            x1 = (float(x1) * INPUT_SIZE - pad_left) / scale
            x2 = (float(x2) * INPUT_SIZE - pad_left) / scale
            y1 = (float(y1) * INPUT_SIZE - pad_top) / scale
            y2 = (float(y2) * INPUT_SIZE - pad_top) / scale

            x1 = float(np.clip(x1, 0, orig_w - 1))
            x2 = float(np.clip(x2, 0, orig_w - 1))
            y1 = float(np.clip(y1, 0, orig_h - 1))
            y2 = float(np.clip(y2, 0, orig_h - 1))

            if x2 <= x1 or y2 <= y1:
                continue

            detections.append({
                "class": CLASS_NAMES[class_id],
                "confidence": round(score, 3),
                "box": [
                    int(round(x1)), int(round(y1)),
                    int(round(x2)), int(round(y2)),
                ],
            })

        # 次クラスの検出数位置へ進む
        cursor = data_end

    return detections


# ============================================================
# HEFの読み込みと入出力確認
# ============================================================

print("Hailo-10H用HEFを読み込み中...")
hef = HEF(HEF_PATH)

input_infos = hef.get_input_vstream_infos()
output_infos = hef.get_output_vstream_infos()

if len(input_infos) != 1:
    raise RuntimeError(f"入力数が想定外です: {len(input_infos)}")

input_info = input_infos[0]

print(f"入力: {input_info.name}, shape={input_info.shape}")
print("出力:")
for info in output_infos:
    print(f"  {info.name}, shape={info.shape}, format={info.format.type}")

output_names = [info.name for info in output_infos]
if OUTPUT_NMS_NAME not in output_names:
    raise RuntimeError(
        f"HEF出力名が見つかりません: {OUTPUT_NMS_NAME}\n"
        f"実際の出力名: {output_names}"
    )


# ============================================================
# MQTT接続
# ============================================================

mqtt_client = mqtt.Client(client_id="mdv6-pi5")
mqtt_connected = False

print(f"MQTTサーバー ({MQTT_BROKER}:{MQTT_PORT}) に接続中...")
try:
    mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
    mqtt_client.loop_start()
    mqtt_connected = True
    print("MQTT接続に成功しました。")
except Exception as exc:
    print(f"MQTT接続に失敗しました。検知処理は続行します: {exc}")


# ============================================================
# カメラを開く
# ============================================================

for camera in CAMERAS:
    camera["cap"] = cv2.VideoCapture(camera["rtsp_url"], cv2.CAP_FFMPEG)
    camera["cap"].set(cv2.CAP_PROP_BUFFERSIZE, 1)
    camera["last_saved_time"] = 0.0
    camera["last_detected_by_class"] = {}

    if camera["cap"].isOpened():
        print(f"{camera['id']}: カメラ接続完了")
    else:
        print(f"{camera['id']}: 接続失敗。RTSP URLを確認してください。")


# ============================================================
# 推論ループ
# ============================================================

print("MegaDetector監視を開始しました。終了するには Ctrl+C を押してください。")

try:
    with VDevice() as target:
        infer_model = target.create_infer_model(HEF_PATH)
        infer_model.set_batch_size(1)

        infer_model.input(input_info.name).set_format_type(FormatType.UINT8)

        for output_info in output_infos:
            infer_model.output(output_info.name).set_format_type(FormatType.FLOAT32)

        with infer_model.configure() as configured_model:
            while True:
                active_camera_count = 0

                for camera in CAMERAS:
                    cap = camera["cap"]

                    if not cap.isOpened():
                        continue

                    active_camera_count += 1
                    ret, frame = cap.read()

                    if not ret:
                        print(f"{camera['id']}: フレームを取得できません")
                        time.sleep(0.1)
                        continue

                    frame_h, frame_w = frame.shape[:2]
                    input_rgb, scale, pad_left, pad_top = letterbox_rgb(frame)

                    input_buffer = np.expand_dims(input_rgb, axis=0)
                    input_buffer = input_buffer.astype(np.uint8, copy=False)

                    bindings = configured_model.create_bindings()
                    bindings.input(input_info.name).set_buffer(input_buffer)

                    output_buffers = {}

                    for output_info in output_infos:
                        name = output_info.name
                        shape = tuple(infer_model.output(name).shape)
                        buffer = np.empty(shape, dtype=np.float32)

                        bindings.output(name).set_buffer(buffer)
                        output_buffers[name] = buffer

                    configured_model.run([bindings], timeout=10000)

                    nms_output = output_buffers[OUTPUT_NMS_NAME]
                    raw = np.asarray(nms_output, dtype=np.float32).reshape(-1)
                    values_per_class = 1 + 5 * 100

                    if time.monotonic() - camera.get("last_debug_time", 0.0) >= 5:
                        camera["last_debug_time"] = time.monotonic()

                        summaries = []
                        for class_id, class_name in CLASS_NAMES.items():
                            start = class_id * values_per_class
                            class_data = raw[start : start + values_per_class]
                            count = max(0, min(int(round(float(class_data[0]))), 100))

                            if count:
                                scores = class_data[1 : 1 + count * 5].reshape(count, 5)[:, 4]
                                max_score = float(np.max(scores))
                            else:
                                max_score = 0.0

                            summaries.append(
                                  f"{class_name}: count={count}, max_score={max_score:.3f}"
                            )

                        print("[NMS] " + " | ".join(summaries))
                    detections = decode_hailo_nms(
                        nms_output,
                        scale,
                        pad_left,
                        pad_top,
                        frame_w,
                        frame_h,
                    )

                    now = time.time()
                    last_by_class = camera["last_detected_by_class"]

                    active_detections = [
                        detection
                        for detection in detections
                        if now - last_by_class.get(detection["class"], 0.0)
                        >= CLASS_COOLDOWN_SEC
                    ]

                    if not active_detections:
                        continue

                    active_classes = sorted(
                        {detection["class"] for detection in active_detections}
                    )

                    for class_name in active_classes:
                        last_by_class[class_name] = now
                        print(f"{camera['id']}: 検出 -> {class_name}")

                    if mqtt_connected:
                        payload = {
                            "timestamp": now,
                            "camera_id": camera["id"],
                            "detected": True,
                            "count": len(active_detections),
                            "objects": active_detections,
                        }

                        mqtt_client.publish(
                            MQTT_TOPIC,
                            json.dumps(payload),
                        )

                    if now - camera["last_saved_time"] >= SAVE_INTERVAL_SEC:
                        timestamp = time.strftime("%Y%m%d_%H%M%S")
                        save_path = os.path.join(
                            SAVE_DIR,
                            f"{camera['id']}_detected_{timestamp}.jpg",
                        )

                        # 検出枠を描かず、元フレームを保存
                        cv2.imwrite(save_path, frame)
                        camera["last_saved_time"] = now
                        print(f"画像保存: {save_path}")

                if active_camera_count == 0:
                    print("有効なカメラがありません。RTSP URLを確認してください。")
                    time.sleep(5)
                else:
                    time.sleep(0.01)

except KeyboardInterrupt:
    print("\n停止します。")

finally:
    for camera in CAMERAS:
        cap = camera.get("cap")
        if cap is not None:
            cap.release()

    if mqtt_connected:
        mqtt_client.loop_stop()
        mqtt_client.disconnect()

    print("カメラとMQTTを終了しました。")