Raspberry Pi5 と AI HAT+2で作るMegadetectorによるAI検知システム

RaspberrPi AI HAT+2
購入先 秋月電子
購入価格 ¥38,390
購入先 アマゾン
購入価格 ¥5,478

PC版のMegadetectorによるイノシシ検出器が出来たので、今度はRaspberry Piで動かそうと企んだ。
当然PCに比べて非力なので果たして使い物になるものが出来るか?半信半疑ではあった。

このシステムを構築する為には、2つの壁がある、一つ目は、ダウンロードで提供されているmodel
この場合、Rapberry Pi5+ HAT+2で利用可能(収まる)な最大性能なモデル「MDV6yolov9-c」をHAT+2
用に変換する過程(hefファイルの書き出し)
そして2つ目は、そのhefファイルを使ってイノシシ検出のスクリプトを作成するという事になる。
幸い既にPC用スクリプトは完成しているので、2つ目はRaspberry Pi5でスクリプトが動作するよう
に調整するだけになる。
最大の難所はhefファイルの書き出しという事になります。

MegaDetector V6 YOLOv9cをHailo-10H用HEFに変換する手順書

対象: Windows 11 + WSL2 Ubuntu 24.04、RTX 4080 Super、Raspberry Pi 5 + AI HAT+2 (Hailo-10H)
この手順書は、今回実際に動作確認できた環境とコマンドをまとめたものです。
ゴールは MDV6b-yolov9c.pt から yolov9c.hef を作り、Pi 5で推論することです。
作業は本ブログの「Jupyter Notebookが動くDockerを作る (Win11用)その2」の環境で行います。
と言ってもhefファイル書き出し作業は、DockerもJupyter Notebookは全く使用しないで、(WSL2)
Ubuntu-24.04のターミナルで行います。
(注:python version 3.12.3指定)

エクスポート用環境 .venv-export とHailoコンパイル用環境 ~/hailo54-env は別々です。
作業段階ごとに有効化する環境を切り替えます。

DFCとHailo Model Zooは5.4.0で揃えます。

WSL2ではWindows側のNVIDIAドライバーを使います。
WSL内にLinux用NVIDIAドライバーはインストールしません。
CUDA ToolkitだけをWSLに導入します。NVIDIA CUDA on WSLガイド

入力画像は640×480のままで構いません。
エクスポート時のモデル入力とPiアプリの前処理で640×640に合わせます。

HEFのNMSスコア閾値は0.2です。
Piスクリプト側で0.1にしても、HEFが0.2未満の候補を出力するようにはなりません。
HEFコンパイルは今回約60分前後掛かりました。
6コンテキストへの分割はこのモデルのリソースに応じた通常のコンパイル経路です。

準備するもの

1.HAILOのサイトから「データーフローコンパイラ」というソフトをダウンロードしてworkspace
 上に配置しておきます。(ロードするには登録が必要です、無料)
 https://hailo.ai/developer-zone/software-downloads/?product=ai_accelerators&device=hailo_10h
 ダウンロードするもの->hailo_dataflow_compiler-5.4.0-py3-none-linux_x86_64.whl

注意:2026/10/3日現在 Pythonへの対応はver3.12迄となっています。

2.MegaDetectorV6 YOLOv9 Compact (MDV6-yolov9-c)のモデル重みのダウンロード。

 以下からダウンロード出来ます。(登録が必要 無料)
 onnxファイルへの変換が必要です、本説明の素材 MDV6-yolov9-c
 https://zenodo.org/records/14567879

 もう一か所
 https://zenodo.org/records/21832059
 こちらは、onnxの書き出しファイルが入手出来るので途中の工程が省略できる。
 本説明のonnxへの書き出しは飛ばして下さい。(名前は要リネーム)
 

3.量子化キャリブレーション用の画像が必要です。
  webから「AI学習用赤外線夜間動物画像 無料」と検索すると見つかります。
  ここでは「Roboflow」さんから拝借しました。(無料 ユーザー登録必要)
    https://universe.roboflow.com/kawaharalabo/far-infrared-rays-
    animals/dataset/11/download/yolov9

  5000枚程度入ったフォルダを作成ここではフォルダ名「PICTURE」。(imageのみラベルは不要)
  640×480ピクセルの画像になります、画像名は何でも構いません、PICTUREフォルダ内に収めて
  下さい。

以上1,2,3はworkspace内に配置しておきます。
workspaceの場所は、この例では、
Windows上の作業フォルダー:C:\Users\name\MyDocker2\MEGA
WSLからのパス:/mnt/c/Users/name/MyDocker2/MEGA
(nameの部分はあなたのユーザー名に変えて下さい)

ターミナル(管理者)を開きwslコマンドを入力します。

Ubuntuの作業用ディレクトリー(workspace)に入ります。
lsコマンドで先程の1,2,3のファイルがあることを確認します。

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

WSLからのパス:/mnt/c/Users/name/MyDocker2/MEGAで実行する。

Hailo変換環境を作るところです、WSL Ubuntuで、以下を上から順に実行してください。
WSLではWindows側のNVIDIAドライバーを使うため、Linux用ドライバーは入れず、CUDA Toolkitだけ
をインストールします。

sudo apt update
sudo apt install -y python3.12-venv python3.12-dev python3-tk graphviz libgraphviz-dev build-essential git wget

 

続いてCUDA Toolkit 12.8を入れます。

cd /tmp
wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get install -y cuda-toolkit-12-8

 nvidia-cuda-toolkit、cuda、cuda-12-8、cuda-driversはインストールしないでください。

次に、DFC 5.4.0用の仮想環境を新しく作り、手元のwheelを入れます。

sudo apt update &&
sudo apt install -y \
  python3.12-venv python3.12-dev python3-tk \
  graphviz libgraphviz-dev build-essential git wget \
  cmake ninja-build &&
python3.12 -m venv ~/hailo54-env &&
source ~/hailo54-env/bin/activate &&
python -m pip install --upgrade pip setuptools wheel &&
python -m pip install \
  /mnt/c/Users/name/MyDocker2/MEGA/hailo_dataflow_compiler-5.4.0-py3-none-linux_x86_64.whl

 最後に、CUDAとDFCが使えるか確認します。

export PATH=/usr/local/cuda-12.8/bin:$PATH
export XLA_FLAGS=--xla_gpu_cuda_data_dir=/usr/local/cuda-12.8

nvcc --version
find /usr/local/cuda-12.8 -name libdevice.10.bc -print
python -c "from hailo_sdk_client import ClientRunner; print('DFC import: OK')"

 ここまでは成功です。nvccがCUDA 12.8を示し、libdevice.10.bcも見つかり、
 最後にDFC import: OKが出ています。

 RAMの29GBは「32GB推奨」に少し届かないという警告です。
 CUDNN version ... found Xも今回のDFC初回チェックでは未検出という表示ですが、 import自体は
 通っています。念のため、次の確認を同じターミナルで実行します。

python - <<'PY'
import tensorflow as tf

@tf.function(jit_compile=True)
def f(x):
    return tf.math.log(x)

print("GPU:", tf.config.list_physical_devices("GPU"))
print("JIT result:", f(tf.constant([2.0])).numpy())
PY

CUDA確認は成功です。GPUとしてRTX 4080 Superが認識され、cuDNN 9.10.2が読み込まれ、Compiled cluster using XLA!とJIT result: [0.6931472]が出ています。初期化時のcuFFT/cuDNN警告はありますが、この確認は通っています。

次は、同じ(hailo54-env)のターミナルでHailo Model Zoo 5.4.0を入れます。まずcloneして登録して
ください。

cd ~
git clone --depth 1 --branch v5.4.0 https://github.com/hailo-ai/hailo_model_zoo.git
cd ~/hailo_model_zoo
python -m pip install -e . --no-deps

続けて、不足モジュールを入れます。通常のpip install -e .では古いscikit-imageのビルドで失敗
したため、--no-depsを付けています。

python -m pip install "omegaconf==2.3.0" Cython "opencv-python==4.11.0.86"

python -m pip install \
  "numba==0.59.0" \
  "imageio==2.38.0" \
  scikit-learn termcolor pycocotools \
  "lap==0.5.12" "motmetrics==1.2.5" "detection-tools==0.3" \
  "scikit-image==0.25.2" nuscenes-devkit \
  "pyquaternion==0.9.9" "Shapely>=2.0.0"

添付ログでは、必要な追加モジュールのインストールは完了しています。
最後の警告は、追加したパッケージがDFC 5.4.0のコア依存(Matplotlib、NetworkX、pyparsing)を
新しい版に置き換えたというものです。ここで失敗したわけではありません。

前の環境で動作したコア依存の版に戻します。同じ(hailo54-env)で実行してください。  

python -m pip install --force-reinstall --no-deps \
  "matplotlib==3.5.2" \
  "networkx==2.8.8" \
  "pyparsing==2.4.7" \
  "scikit-image==0.24.0"

hailomz info yolov9c

終わったら、Model Zooが使えるか確認します。

hailomz info yolov9c
<Hailo Model Zoo INFO> Start run for network yolov9c ...
<Hailo Model Zoo INFO>
        task:                    object detection
        input_shape:             640x640x3
        output_shape:            80x80x64, 80x80x80, 40x40x64, 40x40x80, 20x20x64, 20x20x80
        operations:              102.1G
        parameters:              25.3M
        framework:               pytorch
        training_data:           coco train2017
        validation_data:         coco val2017
        eval_metric:             mAP
        full_precision_result:   52.6
        source:                  https://github.com/WongKinYiu/yolov9
        license_url:             https://github.com/WongKinYiu/yolov9/blob/main/LICENSE.md

 

Model Zooの導入は成功しています。表示された80クラスは標準のyolov9c設定情報です。後の最適化では--classes 3を指定するので、今回のモデルの3クラス設定はそこで適用します。

次はPTからONNXを書き出します。現在のhailo54-envとは別の環境を使います。CPUでエクスポート
するため、この作業にCUDAは不要です。PyTorch公式のCPU向け配布を使います。

2. エクスポート用環境を作成


 同じWSLターミナルで実行してください。

deactivate
python3.12 -m venv ~/mdv6-export-env
source ~/mdv6-export-env/bin/activate
python -m pip install --upgrade pip
python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
python -m pip install "ultralytics==8.3.70" onnx onnxscript

プロンプトが(mdv6-export-env)になったら、環境の切り替えは完了です。

3. モデルを確認してONNXを書き出す

cd /mnt/c/Users/name/MyDocker2/MEGA

python - <<'PY'
from ultralytics import YOLO

model = YOLO("MDV6b-yolov9c.pt")
print("task:", model.task)
print("classes:", model.names)
print("stride:", model.model.stride)
PY

animal、person、vehicleの3クラスが表示されたら、ONNXを書き出します。

python - <<'PY'
from ultralytics import YOLO

model = YOLO("MDV6b-yolov9c.pt")
path = model.export(
    format="onnx",
    imgsz=640,
    opset=13,
    simplify=False,
    dynamic=False,
    batch=1,
    device="cpu",
)
print("ONNX:", path)
PY

成功すると、作業フォルダーにMDV6b-yolov9c.onnxができます。

ONNXの書き出し完了です。次は、Hailo環境に切り替えてONNXをHARに変換します。以前成功した6個の終端ノードを使います。

4.Hailo環境へ切り替え、ONNXを確認

deactivate
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

cd /mnt/c/Users/name/MyDocker2/MEGA

python - <<'PY'
import onnx

path = "MDV6b-yolov9c.onnx"
model = onnx.load(path)
onnx.checker.check_model(model)
print("ONNX check: OK")
print("入力:", model.graph.input[0].name)
print("出力:", [x.name for x in model.graph.output])
PY

ONNX check: OKと表示されたら、続けて次を実行します。

5. Hailo用HARを作る

python - <<'PY'
from hailo_sdk_client import ClientRunner

onnx_path = "/mnt/c/Users/name/MyDocker2/MEGA/MDV6b-yolov9c.onnx"
har_path = "/home/name/MDV6b-yolov9c_nms.har"

runner = ClientRunner(hw_arch="hailo10h")
runner.translate_onnx_model(
    onnx_path,
    "mdv6b_yolov9c_nms",
    end_node_names=[
        "node_conv2d_128",
        "node_conv2d_131",
        "node_conv2d_134",
        "node_conv2d_137",
        "node_conv2d_140",
        "node_conv2d_143",
    ],
    net_input_shapes={"images": [1, 3, 640, 640]},
)
runner.save_har(har_path)
print("HAR:", har_path)
PY

最後にHAR: /home/name/MDV6b-yolov9c_nms.harと表示されたら成功です。

次は校正画像を1,024枚用意して、モデルを最適化します。

6.校正画像を1,024枚コピー

以下はOICTUREとPICTUREのどちらのフォルダー名でも動作します。今のhailo54-envターミナルで実行してください。

cd /mnt/c/Users/name/MyDocker2/MEGA
mkdir -p ~/mdv6_calib_1024

python - <<'PY'
from pathlib import Path
import random
import shutil

src = next(
    (Path(name) for name in ("OICTURE", "PICTURE") if Path(name).is_dir()),
    None,
)
if src is None:
    raise SystemExit("OICTURE または PICTURE フォルダーが見つかりません")

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

random.seed(42)
dst = Path.home() / "mdv6_calib_1024"
for p in random.sample(files, 1024):
    shutil.copy2(p, dst / p.name)

print(f"校正画像: {len(list(dst.glob('*.jpg')))}枚")
print(f"フォルダー: {dst}")
PY

校正画像: 1024枚と表示されたら次に進みます。

7.最適化を実行

export PATH=/usr/local/cuda-12.8/bin:$PATH
export XLA_FLAGS=--xla_gpu_cuda_data_dir=/usr/local/cuda-12.8

mkdir -p ~/mdv6_results
cd ~/mdv6_results

hailomz optimize yolov9c \
  --har /home/name/MDV6b-yolov9c_nms.har \
  --calib-path /home/name/mdv6_calib_1024 \
  --classes 3 \
  --hw-arch hailo10h

完了ログにModel Optimization is doneと、保存先/home/name/mdv6_results/yolov9c.harが出るまで待ちます。最適化は少々時間掛かります。

8.Hailo-10H向けHEFへコンパイル

Hailo-10H向けHEFへコンパイルします。hailo54-envが有効な同じターミナルで実行してください。
時間は1時間前後は掛かります。

cd ~/mdv6_results

hailomz compile yolov9c \
  --har /home/name/mdv6_results/yolov9c.har

HEFは/home/name/mdv6_results/yolov9c.hefにあります。こちらをPiへコピーすれば、使用できます。



Raspberry Pi5 + AI HAT2+のセットアップ

Megadetector model 「yolo9c.hef」ファイルをゲットしたのでいよいよRaspberry Pi5の
セットアップ作業に入ります。

Raspberry Pi5とAI HAT+2を接続組み立てて下さい。
準備が出来たらソフトのインストール作業に入ります。


インストール手順

1,1,Raspberry Pi Imagerを使ってRaspberry Pi OS(64bit)
  A port of Debian Trixie with the Raspberry Pi Desktopをインストール

2.下記のコマンドを1つずつ順番に実行して下さい。
  4~6は1コマンドで繋がっていますので注意して下さい。

sudo apt update
sudo apt full-upgrade -y
sudo rpi-eeprom-update -a
sudo apt install -y build-essential libssl-dev zlib1g-dev libbz2-dev \
libreadline-dev libsqlite3-dev wget curl llvm libncursesw5-dev \
xz-utils tk-dev libffi-dev liblzma-dev

sudo apt install -y python3-opencv python3-venv
/usr/bin/python3 -m venv --system-site-packages ~/HAT
source ~/HAT/bin/activate

# バージョン確認
python --version
# 出力: Python 3.12.15
python3
>>import cv2

sudo apt install -y dkms hailo-h10-all
sudo reboot

仮想領域HATを作ってそこに主要なものをインストールします。
名前HATは別の名前でも良いです。

先に用意した「MDV6b-yolov9c.hef」のファイルもこの仮想エリアHAT内に置きます。

次のコマンドで正常にインストールされたか確認します。

-----------------------------------------
hailortcli fw-control identify
-----------------------------------------
(HAT) pi@pi:~ $ hailortcli fw-control identify
Executing on device: 0001:01:00.0
Identifying board
Control Protocol Version: 2
Firmware Version: 5.1.1 (release,app)
Logger Version: 0
Device Architecture: HAILO10H
-----------------------------------------
hailortcli parse-hef ~/models/yolov9c.hef
---------------------------------------------
(HAT) pi@pi:~ $ hailortcli parse-hef /home/pi/HAT/yolov9c.hef
HEF Compatible for: HAILO15H, HAILO10H
Network group name: yolov9c, Multi Context - Number of contexts: 6
    Network name: yolov9c/yolov9c
        VStream infos:
            Input  yolov9c/input_layer1 UINT8, NHWC(640x640x3)
            Output yolov9c/concat33 UINT8, FCR(1x8400x64)
            Output yolov9c/ne_activation_activation1 UINT8, FCR(1x8400x3)

—枠内の2つのコマンドを実行してその下にあるようなメッセージが表示されたらOkです。
最後に以下のコマンド実行で終了です。

python -m pip install --upgrade pip
pyhon -m pip install paho-mqtt

スクリプトは以下の内容になります。
Raspberry Pi5に付属している「Thonny」から呼び出して使用します。
Thonnyの設定では、Thonny画面の右下角にある三のところをクリックして「Cofigure Interpreter」
をクリック
次に表示される画面でPython Executableのところのパスを「/home/pi/HAT/bin/python3.13」にしてOK
をクリックして下さい。

Raspberry Pi5 + AI HAT+2 用イノシシ威嚇用スクリプト

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/yolov9c.hef"

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

MQTT_BROKER = "192.168.1.200"#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.50,
    2: 0.50,
}

INPUT_SIZE = 640

# 今回作成したHEFで確認済みの出力名と形状
OUTPUT_NMS_NAME = "yolov9c_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を終了しました。")

検出間隔が短いと対象が留まっている場合、連続して検出が発布されますので具合が悪いです。
上記スクリプトの以下の部分で調整して下さい。(現在は5s)
・同じ種類を無視する時間(秒)
 CLASS_COOLDOWN_SEC = 5

・種類ごとに誤検出の出やすさを見て調整します。
 HEF内蔵NMSのスコア閾値より高い値で、アプリ側でも絞り込みます

 CLASS_THRESHOLDS = {
 0: 0.50,
 1: 0.50,
 2: 0.50,
 }

・検出画像静止画で保存されます。(枠無し)
 SAVE_INTERVAL_SEC = 5

さてセンサーは出来ましたので今後は、「わんわん吠えまくる装置」
を作って行きます。