MegaDetectorでイノシシ威嚇システムの制作 その1 PC編

Windows11 (WSL2) RTX4080Super16GB Jupyter Notebook Docker上で作動させる

田舎の住まいは周りが山に囲まれているので色々な動物がやってくる。
家の周りには1キロ程度にわたり侵入防止の柵を張り巡らしているが、難なく突破して侵入してくる。
昼間でも時々現れる事もあるが、大抵は寝静まった夜中の事が多い。
現れるだけなら一緒に連れてくるマダニの被害を心配するだけで済むが、イノシシの場合大好物のミ
ミズさんを求めてそこいらじゅうを掘り返すので始末が悪い。
家の前にある電柱の根元を掘り返されて電柱が倒れそうになったこともある。
そうゆうわけでこいつらを寄せ付けない為には、何らかの威嚇装置の設置が必須事項なのである。

今までは、市販の赤外線検知式のLEDライトを数か所設置して、ライトが作動した際の電流を検知
して威嚇装置を作動させていた。
威嚇装置は大音量で犬がわんわん吠えまくるもので、これを数か所に設置し、検知すると時間差で
あちこちからわんわん吠えまくる仕組みである。
これは、かなり効果があり、イノシシはやな顔をして逃げてゆく場合が多い。
しかし、問題が一つあって、台風が来た日などに赤外線LEDが飛び交う木の葉っぱや動く物に反応
して誤動作することである。
結局、風の強い日にはOFFにしないとわんわん吠えて眠れない夜になってしまうのだ。
これらの結果からセンサーなどで検知させるというのは、限界があるという事で、やはり最終的に
はAIセンサーが求められる訳である。

購入先 アマゾン
購入価格 ¥5,478



Microsoftが開発した「Megadetector」を使う

メジャーなところでは「YOLO」を使用する場合が多いが、Megadetectorは「夜間の赤外線画像に強い
」という特徴に魅かれた、YOLOだと標準modelは昼間画像素材だし、自分で学習させると言っても素
材入手の敷居は高い。
過去に実際素材から挑戦してみたが、手持ちのGPUでは、YOLO標準modelのYOLO11Xレベルの感度
の物は作ることが出来なかった。
手持ちのRTX4080Super16GBでは、レンダリングに1週間近く要するも希望の感度のものは作れな
かった、このレベルのGPUでは役不足だという事が分かった、最低でもGPUはRTX4090 24GBレ
ベルは確保しないと仕事にならないようだ(実際は遊びだが)   買う銭が無い。
他にもMegadetectorを1回試してみたいという好奇心も大きい。
実際に稼働するのは夜間のみだし、カメラはTAPO 310Aで夜間は赤外線画像になるので相性が良い
夜間の低コントラストなモノクロ画像から、草むらに隠れた動物の輪郭を見つけ出す能力が標準
のYOLOとはけた違いらしい。

このモデルは、個別のアニマルネーム(イノシシ、猫、猿)を直接出力するのではなく、「Animal(動物
」か「Person(人間)」かを感度良く検出するものでYOLOの場合と異なるが、私のやりたい「威嚇装
置」用としてはここまでで十分に利用できる。

庭は広いので死角が無いように2か所にTAPO 310Aカメラを仕掛ける。
通信方式はRTSP方式でRaspi CameraのようにRaspi本体にぶら下げて使わないので使いやすい。
また、Docker内との通信の難しさもあり動画は現状表示出来ないが、他の携帯画面、フリーソフト
の「VLC media player」などを使って同時並行してモニター出来るので不便はない。
検出したら、MQTT通信によってRaspberry Pi NODE-REDサーバーに送信し、最終的に「威嚇装置」
わんわんを起動 及びGoogle HOMEに送信し音声による警告を発するようにする。
同時に検出動物は静止画で保存する。

Megadetectorは最新版のV6を使用する。

https://microsoft.github.io/MegaDetector/

V6のModelは種類があるが、シリーズ最高性能な「MDV6-apa-rtdetr-e」を使用する。

インストールの手順

インストールは、ドッカーによるJupyter Notebookの環境で行います。
Jupyter Notebookの環境の作り方は以下参照の事 (事例2の方)
workspaceに更に個別のフォルダを作りその中で実行すると、一つの環境内で色々なスクリプトが
実行できる。
Jupyter Notebookを立ち上げセルに貼って順番に実行します。
1セルに下記の1行分ずつ貼り付けて実行します。
インストールすると以下のメッセージが出ますので、Jupyter Notebookのプルダウンメニュー
から「Kernel」->「Restart Kernel」を実行して下さい。
 Note: you may need to restart the kernel to use updated packages.

%pip install PytorchWildlife --break-system-packages
%pip install paho-mqtt --break-system-packages
%pip install soundfile --break-system-packages
%pip install librosa --break-system-packages
%pip install setuptools --break-system-packages
%pip install --force-reinstall "setuptools==80.9.0" --break-system-packages

正常にインストールされたか確認の為以下を実行します。

  import setuptools
  import pkg_resources
  print(setuptools.version)
  print("pkg_resources を読み込めました")

上記の前作業が出来たら本チャンの以下のスクリプトをセルに貼り付け実行します。
TAPO310Aカメラの電源を入れて準備します。
MQTTサーバーの準備も必要です。以下参照
raspberrypiでnode-red-mqttサーバーを設置する方法

Megadetector V6 (MDV6-apa-rtdetr-e) 動物検知スクリプト

rtsp通信のためのIPアドレスは自身の場合のアドレスを入力します。
同じくIDとPasswdは携帯で自身が登録したIDとPasswdを入れます。
MQTT_BROKERアドレスも自身の場合のアドレスを入れます。
初回の起動は「【1/3】MegaDetector V6 (MDV6-apa-rtdetr-e) モデルを読み込み中…」
と表示されて読み込み完了迄十分程度の時間が掛かります。(2回以降は掛らない)

import cv2
import json
import time
import os
import paho.mqtt.client as mqtt
# Ultralytics の代わりに PytorchWildlife をインポート
from PytorchWildlife.models import detection as pw_detection

# ==========================================
# 1. 設定
# ==========================================
CAMERAS = [
    {
        "id": "tapo310a_1",
        "rtsp_url": "rtsp://USER:PASSWORD@192.168.1.100:554/stream1",
    },
    # 2台目も使う場合は、下記を有効にして実際のURLを設定してください。
    #{
         #"id": "tapo310a_2",
         #"rtsp_url": "rtsp://USER:PASSWORD@192.168.1.101:554/stream1",
    #},
]

MQTT_BROKER = "192.168.1.200"
MQTT_PORT = 1883
MQTT_TOPIC = "megadt"
# MDV6の標準クラスマップ
CLASS_NAMES = {0: "animal", 1: "person", 2: "vehicle"}
SAVE_DIR = "detected_images"
SAVE_INTERVAL_SEC = 5
CONFIDENCE = 0.25
CLASS_COOLDOWN_SEC = 60  # 同じ種類を無視する時間(秒)
# PyTorch-Wildlife用のデバイス指定 ("cuda" または "cpu")
DEVICE = "cuda" 

os.makedirs(SAVE_DIR, exist_ok=True)

# ==========================================
# 2. 初期化
# ==========================================
print("【1/3】MegaDetector V6 (MDV6-apa-rtdetr-e) モデルを読み込み中...")
# 初回実行時は自動的に重量ファイル(約1.2GB)のダウンロードが始まります
model = pw_detection.MegaDetectorV6Apache(device=DEVICE, pretrained=True, version="MDV6-apa-rtdetr-e")
print(" -> モデル読み込み完了。")

print(f"【2/3】MQTTサーバー ({MQTT_BROKER}) に接続中...")
mqtt_client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
mqtt_connected = False
try:
    mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
    mqtt_client.loop_start()
    mqtt_connected = True
    print(" -> MQTT接続に成功しました。")
except Exception as e:
    print(f" -> MQTT接続に失敗しました (検知処理は続行します): {e}")

print("【3/3】TAPOカメラへのストリームを開いています...")
for camera in CAMERAS:
    camera["cap"] = cv2.VideoCapture(camera["rtsp_url"])
    camera["last_saved_time"] = 0
    camera["last_detected_by_class"] = {}
    if camera["cap"].isOpened():
        print(f" -> {camera['id']}: 接続完了")
    else:
        print(f" -> {camera['id']}: 接続できませんでした。RTSP URLを確認してください。")

print("\nMegaDetector V6 (RT-DETR) 監視システムを開始しました!")
print("※ 画面は表示されません。検出があるとログが流れ、画像が保存されます。")

# ==========================================
# 3. メインループ (画面表示なし)
# ==========================================
try:
    while True:
        active_cameras = 0
        for camera in CAMERAS:
            cap = camera["cap"]
            if not cap.isOpened():
                continue
            active_cameras += 1

            ret, frame = cap.read()
            if not ret:
                time.sleep(0.1)
                continue

            original_frame = frame.copy()

            # OpenCVのBGRからPyTorch-Wildlife(並びにRT-DETR)が想定するRGBに変換
            img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            
            # 推論の実行
            results = model.single_image_detection(img_rgb)
            detections = []

            # PyTorch-Wildlifeの出力結果をパース
            # クラスごとの信頼度しきい値
            CLASS_THRESHOLDS = {
                0: 0.95,  # animal
                1: 0.95,  # person:誤検出が多ければさらに上げる
                2: 0.95,  # vehicle
            }

            dets = results.get("detections")

            if dets is not None and len(dets) > 0:
                for xyxy, cls_id, conf in zip(
                    dets.xyxy, dets.class_id, dets.confidence
                ):
                    cls_id = int(cls_id)
                    conf = float(conf)
                    label = CLASS_NAMES.get(cls_id, f"unknown-{cls_id}")

                    if conf < CLASS_THRESHOLDS.get(cls_id, 1.0):
                        continue

                    detections.append({
                        "class": label,
                        "confidence": round(conf, 2),
                        "box": [int(value) for value in xyxy],
                    })
                    label = CLASS_NAMES.get(cls_id, f"unknown-{cls_id}")
                    detections.append({
                        "class": label,
                        "confidence": round(conf, 2),
                        "box": [int(value) for value in xyxy],
                    })

                    # 枠付き画像描画(必要に応じてコメントアウト解除)
                    # xmin, ymin, xmax, ymax = map(int, xyxy)
                    # cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), (0, 255, 0), 3)
                    # cv2.putText(frame, f"{label} {conf:.2f}", (xmin, ymin - 10),
                    #             cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)

            if detections:
                # クラスごとにクールダウンを適用
                current_time = time.time()
                last_detected = camera["last_detected_by_class"]

                active_detections = [
                    detection
                    for detection in detections
                    if current_time - last_detected.get(detection["class"], 0)
                    >= CLASS_COOLDOWN_SEC
                ]

                if active_detections:
                    # 今回通知する種類の時刻を更新
                    active_classes = sorted({d["class"] for d in active_detections})
                    for class_name in active_classes:
                        last_detected[class_name] = current_time
                        print(f"検出->{class_name}")

                    # Node-REDへMQTT送信
                    if mqtt_connected:
                        payload = {
                            "timestamp": current_time,
                            "camera_id": camera["id"],
                            "detected": True,
                            "count": len(active_detections),
                            "objects": active_detections,
                        }
                        mqtt_client.publish(MQTT_TOPIC, json.dumps(payload))

                    # 画像保存(従来どおり、カメラごとに5秒間隔)
                    if current_time - camera["last_saved_time"] > SAVE_INTERVAL_SEC:
                        timestamp_str = time.strftime("%Y%m%d_%H%M%S")
                        save_path = os.path.join(
                            SAVE_DIR, f"{camera['id']}_detected_{timestamp_str}.jpg"
                        )
                        cv2.imwrite(save_path, original_frame)
                        camera["last_saved_time"] = current_time

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

except KeyboardInterrupt:
    print("\nユーザーによってシステムが停止されました。")

finally:
    for camera in CAMERAS:
        camera["cap"].release()
    if mqtt_connected:
        mqtt_client.loop_stop()
        mqtt_client.disconnect()
    print("すべてのリソースを解放しました。")

実行例

【1/3】MegaDetector V6 (MDV6-apa-rtdetr-e) モデルを読み込み中...
Load PResNet101 state_dict
 -> モデル読み込み完了。
【2/3】MQTTサーバー (192.168.1.200) に接続中...
 -> MQTT接続に成功しました。
【3/3】TAPOカメラへのストリームを開いています...
 -> tapo310a_1: 接続完了

MegaDetector V6 (RT-DETR) 監視システムを開始しました!
※ 画面は表示されません。検出があるとログが流れ、画像が保存されます。
検出->person

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

対象物の感度調整は以下の部分で行います。
CLASS_THRESHOLDS = {
0: 0.95, # animal
1: 0.95, # person
2: 0.95, # vehicle
}

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

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