🚙

【AI CAPTURE】長時間駐車車両検出プログラム

に公開

はじめに

現在、Raspberry Piをベースとした、AI CAPTUREという監視カメラシステムを公開中です。

https://aicap.daddysoffice.com

このシステムで動く検知プログラムとして、長時間駐車している車両を検出するプログラムを公開しましたので、そのプログラムの内容について説明します。

プログラムの概要

このプログラムは物体検出モデルのYOLO11のオブジェクト追跡(トラッキング)を使用しています。

https://docs.ultralytics.com/ja/modes/track/

トラッキング(track)は、通常のオブジェクト検出(predict)の結果に加えて、認識したオブジェクトに個別のIDを付与します。

別の画像フレームでトラッキングを実行したときの戻り値に、同じIDがあれば、それは同じオブジェクトとして認識しているということになります。

このトラッキングを利用して、認識したオブジェクトが動いているかどうかを判断し、動いていない時間をカウントしていきます。

そして、静止時間が指定した時間を超えたらアラート(Push通知)を出す、というプログラムになります。

検出する物体を車ではなく人にすれば、例えば、興味を持ったお客さんが店舗の前で立ち止まっている場合にアラートを表示する、という使い方もできそうです。

処理説明

全プログラムはGitHubで公開中です。
https://github.com/daddyYukio/AICAPTURE/tree/main/programs/stay_counter

関数一覧

プログラム(extmod.py)は、下記の4つの関数と、それらを呼び出すメイン関数から構成されています。

関数名 説明
get_frame aicap get_frameコマンドを実行します。
フレーム画像は標準出力経由でメモリ上で取得します。
push aicap pushコマンドを実行します。
送信する画像は、標準入力経由で引数に指定して、検出した物体を枠で囲んで静止時間を書き込んだ画像を送信します。
create_result_jpeg get_frameで取得したカメラのフレーム画像に、検出した物体を枠で囲んで静止時間を書き込みます。
parse_results トラッキング(track)の結果を成形して、動いているか静止しているかを判断した後、静止時間を計算して格納します。

処理の流れとしては、メイン関数内で上記の関数を以下のように繰り返し実行します。

カメラのフレーム画像取得

AIBOX OS組み込みコマンドのaicap get_frameコマンドを実行して、カメラのフレーム画像をJPEGで取得します。
取得した時間をunixtimeで保持した後、Yoloに渡すために、PLI Imageに変換します。

# ビデオ映像取得
frame = get_frame()
timestamp = int(datetime.now(tz=timezone.utc).timestamp())

# PLI Imageに変換
img = Image.open(BytesIO(frame))

aicapコマンドの詳細はこちらを参照してください

YOLOでトラキング実行

PLIImageに変換したフレーム画像を引数に、トラッキングを実行します。

# トラッキング実行
results = model.track(
    img, 
    conf=CONF, 
    iou=IOU, 
    persist=True,
    classes=CLASSES, 
    verbose=True)

引数の説明は以下です。

引数 説明
img 入力画像
conf 信頼度(confidence)閾値
外部変数CONFで定義
iou IoU(Intersection over Union)閾値
外部変数IOUで定義
persist 追跡IDの維持を有効にするか動画のフラグ
True にすると、フレーム間で同じ物体のIDを維持します(動画トラッキング用)
False だとフレームごとに新しいIDが振られます
classes 検出するクラスを限定するためのリスト
例:classes=[0, 2] → クラスID 0(人)と2(車)だけ検出
指定しないとすべてのクラスが対象
外部変数CLASSESで定義
verbose 詳細ログを出すかどうか

このtrackを実行すると、追跡IDが振られた物体検出結果(results)が返却されます。

返却される結果の主なプロパティは以下です。

  • results[i].boxes:
    • xyxy → 各検出/追跡対象のバウンディングボックス座標 [x1, y1, x2, y2] 形式
    • xywh → [x_center, y_center, width, height] 形式
    • conf → 各検出の信頼度スコア
    • cls → 各検出のクラスラベル(整数インデックス)
    • id → 追跡ID
    • is_track → その Boxes オブジェクトが「追跡ID付きかどうか」を示す真偽値

結果を整形

前述のトラッキングの結果を成形&確認し、メイン関数で保持しているtracking_objects配列に、オブジェクトの情報を格納します。

tracking_objectsに同じIDのオブジェクトが既に格納されている場合は、前回から移動しているかどうかを確認し、移動していない場合は、静止時間を計算して格納します。

# すでにトラッキングされているオブジェクトが
# 今回の処理でも見つかったので
# 情報を更新

p["tracked"] = True
p["conf"] = conf[0]

prev_x = p["pos"]["x"]
prev_y = p["pos"]["y"]

#
# 前回から動いているか?
# 10ピクセル以上動いていたら動いたと判断
move_x = abs(prev_x - pos_x)
move_y = abs(prev_y - pos_y)
move = move_x > 10 or move_y > 10

p["pos"] = {"x" : pos_x, "y" : pos_y}
p["box"] = {"x1" : x1, "y1" : y1, "x2" : x2, "y2" : y2}

if move:
    p["state"] = "move"
else:
    # 静止している場合は静止時間を加算
    p["stay_sec"] = p["stay_sec"] + timestamp - p["prev_timestamp"]
    p["state"] = "stay"

ちなみに、tracking_objects配列に格納されているオブジェクトで、今回のトラッキングで検出されなかったオブジェクトをすぐには削除しません。

これは、あとで検出された際、同じオブジェクトと判断された場合は、ちゃんと同じ追跡IDが振られるためです。

例えば、車が一旦カメラの枠外に出て、短時間で再度戻ってきて駐車した場合でも、以前の追跡IDと同じIDが振られて検出されることがあり、その場合は静止時間を前回の時間に加算していくことが可能になります。

オブジェクトの削除はメイン関数の処理の最後で行います。

# 時間がたったオブジェクトは削除する
tracking_objects = [p for p in tracking_objects if timestamp - p["prev_timestamp"] < OBJECT_RETENTION_TIME_SEC]

最後に検知された時間(prev_timestamp)からOBJECT_RETENTION_TIME_SEC(秒)経過したオブジェクトを削除します。

検出枠と静止時間を描画

tracking_objects配列の中でトラッキングされているオブジェクトを枠で囲んで、静止時間を描画します。
枠の色は、静止時間が長くなるにしたがって、通常(白)、警告(黄色)、アラート(赤)の3色に変化させます。

stay_sec = p["stay_sec"]

# 枠の色を決める
if stay_sec < WARNING_SEC:
    cr = (255, 255, 255)
elif stay_sec < ALERT_SEC:
    cr = (255, 255, 0)
else:
    cr = (255, 0, 0)

作成した画像はJPEGフォーマットでメモリ上に出力して返却します。

dst = BytesIO()
img.save(dst, format='JPEG', quality=75)
dst.seek(0)

return dst.getvalue()

Push通知送信

アラート状態のオブジェクトがあれば、AIBOX OS組み込みコマンドのaicap pushコマンドを実行してPush通知を送信します。

# ALERT_SECを超えているオブジェクトがあればPush通知
if len([p for p in tracking_objects if p["stay_sec"] > ALERT_SEC]) > 0:

    # PUSH通知
    # エラーはここでキャッチしてそのまま処理を流す
    try:
        push(timestamp, frame, tracking_objects)
    except Exception as e:
        print(str(e))

aicapコマンドの詳細はこちらを参照してください

プレビュー用イメージの保存

最後にプレビュー用の結果画像を保存します。

# 結果確認用のプレビューイメージの保存
with open(PREVIEW_IMAGE_PATH, 'wb') as f:
    f.write(frame)

AIBOX OSのプレビュー表示についてはこちらを参照してください

実行結果

AIBOXのプレビュー機能を使用してブラウザで表示した実行結果です。
手持ちのミニカーでもちゃんと認識しています。

Discussion