投稿者: sassy

  • 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

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