Raspberry Pi 5で動かす簡易「交通量計測」Webアプリを作ってみた

店舗入口や構内通路、受付前、イベント会場の導線などでは、「この地点を何人通過したか」を現場側で手軽に確認したい場面があります。交通量計測というと、自動車・自転車・歩行者などさまざまな対象がありますが、今回はその中でも「歩行者の通行人数」に対象を絞っています。まずは最小構成として、人の通過人数を手軽に可視化する仕組みを実装してみました。そこで今回も、「エレクトロニクスを簡単に」をテーマに、Raspberry Pi 5 に接続したUSBカメラ映像を使って通行人数を簡易計測し、同一ネットワーク内のPCブラウザから確認できるWebアプリを、できるだけ短いプログラムで試作してみました。

今回の構成は、顔認識や個人識別ではなく、固定カメラ前提の1地点・1方向の簡易な通行人数計測PoCに絞っています。さらに、画面上には縦のカウントラインカウント方向を示す矢印だけを重ね、結果も映像上に直接描画するようにして、できるだけ短くシンプルな構成にしています。

実行環境には Raspberry Pi 5を想定しています。Raspberry Pi 5は Raspberry Pi 公式でも高性能な汎用コンピュータとして案内されており、Raspberry Pi OS と組み合わせることで、現場設置型の軽量なエッジAI PoCを載せやすい構成にできます。Raspberry Pi 5 は従来世代比で2~3倍の性能向上が案内されており、ローカル推論と軽量なWebアプリを同居させる用途でも扱いやすいです。
Raspberry Pi
Raspberry Pi OS

今回使うのは FlaskOpenCV、そしてONNX Runtimeです。人物検出にはYOLOX 系のONNXモデルを使い、USBカメラ映像の各フレームから人だけを検出し、その中心点を簡易追跡して縦ライン通過人数を数えます。なお、OpenCV 4.5.0 以降は Apache 2.0 ライセンス、ONNX Runtime は MIT License、YOLOXのリポジトリは Apache 2.0 ライセンスで公開されています。
OpenCV License
ONNX Runtime
YOLOX

目次

はじめに

今回のアプリは、Raspberry Pi 5にUSBカメラを接続して固定設置し、そのライブ映像の中で縦の計測ラインを横切った人を1人ずつカウントしていく仕組みです。映像処理自体は Raspberry Pi 5側で行い、確認用の画面は同一ネットワーク内のPCブラウザから開く構成にしています。

構成としてはとてもシンプルで、

  • Raspberry Pi 5にUSBカメラを接続する

  • カメラ映像を Raspberry Pi 5側で読み込む

  • AIモデルで各フレームから人を検出する

  • 人物の中心点を簡易追跡する

  • 縦の計測ラインを指定方向に横切ったら人数を加算する

  • 同一ネットワーク内のPCブラウザで映像と人数を確認する

という流れだけに絞っています。

Raspberry Pi 5を実行環境にした理由

今回のPoCでは、実行環境として Raspberry Pi 5を採用しました。

Raspberry Pi 5 は Raspberry Pi シリーズの中でも高性能なモデルであり、従来世代と比較して大幅な性能向上が図られています。Linux ベースの開発環境を手軽に構築できることに加え、Python、OpenCV、FlaskONNX Runtime などのライブラリやフレームワークも利用しやすいため、エッジAIのPoC環境として扱いやすい点が魅力です。

今回のような交通量計測アプリでは、

  • USBカメラからの映像取得

  • AIモデルによる人物検出

  • 簡易な追跡処理

  • Webサーバーによる映像配信

といった複数の処理を同時に実行する必要があります。Raspberry Pi 5であれば、これらを1台の小型コンピュータ上でまとめて動かすことができ、PCを常時設置しなくても検証しやすい構成を実現できます。

また、GPIOや各種インターフェースも利用できるため、今回のような人数カウントだけでなく、センサー連携や設備監視、混雑状況の可視化などへ発展させる際にも応用しやすいです。

さらに、PoC段階では Raspberry Pi 5のような汎用プラットフォームで素早く検証を進め、将来的に性能要件や運用条件に応じて専用ハードウェアへ展開するという進め方も取りやすいため、エッジAIアプリケーションの試作環境として相性が良いと考えました。

YOLOXとONNX Runtimeを選んだ理由

今回のPoCでは、人物検出モデルとしてYOLOX、推論実行環境としてONNX Runtime を採用しました。

交通量計測のようなアプリケーションでは、カメラ映像の各フレームから人物を検出し続ける必要があります。そのため、検出精度だけでなく、エッジデバイス上で動作させやすい処理性能や実装のしやすさも重要になります。

YOLOXは、YOLOシリーズの中でも扱いやすい構成を持つ物体検出モデルで、人物を含むさまざまな対象を認識できます。今回は、軽量なYOLOX-Nano のONNXモデルを利用し、USBカメラ映像から人物クラスのみを抽出することで、Raspberry Pi 5上でも比較的試しやすい構成にしました。

また、学習済みモデルの実行には ONNX Runtime を使用しています。ONNX Runtime はONNX形式のモデルを実行するための推論エンジンで、Pythonから利用しやすく、さまざまなハードウェア環境で共通のモデルを扱いやすい点が特徴です。今回のようなPoCでは、学習済みモデルを比較的簡単に組み込みながら、推論処理の仕組みを理解しやすいというメリットがあります。

さらに、ONNX形式を利用することで、将来的に別の人物検出モデルへ置き換えたり、車両や自転車などの検出対象を追加したりすることも比較的容易です。交通量計測の対象や用途に応じてモデルを差し替えやすい点も、今回YOLOXONNX Runtimeを選択した理由の一つです。

今回のPoCでは、できるだけ少ないコード量で「映像入力→ AI推論→人数カウント → Web表示」 という流れを体験できることを重視し、その実現手段としてYOLOXONNX Runtimeを組み合わせました。

Webアプリの基本構成

今回は Flask ベースのWebアプリとして実装し、ブラウザにはMJPEG ストリームとしてUSBカメラ映像を表示します。Flask では generator を使ったストリーミングレスポンスのパターンが公式ドキュメントで説明されており、継続的にフレームを返す構成を作れます。
Flask Streaming

カメラ入力は OpenCVの VideoCapture を使います。Raspberry Pi 5 に接続したUSBカメラをここで開き、そのフレームを逐次処理して返します。各フレームではONNX Runtimeで人物検出を実行し、人クラスだけを通過人数の対象にします。

また、今回は Raspberry Pi 5上でFlask を 0.0.0.0 バインドで起動し、同一ネットワーク内のPCから http://RaspberryPiのIPアドレス:5000 にアクセスする構成を取ります。ブラウザ側では、映像そのものの上に人数・カウントライン・方向矢印を重ねて表示するだけにして、画面構成も最小限にしています。

今回の処理フローは以下の通りです。

  1. Raspberry Pi 5でUSBカメラを起動

  2. 各フレームをONNX モデル入力サイズへ変換

  3. 人物検出を実行

  4. 人クラスだけを抽出

  5. 矩形の中心点を簡易追跡

  6. 縦の計測ラインを指定方向に横切ったらカウント

  7. 同一ネットワーク内のPCブラウザに映像と人数を表示

ディレクトリ構成

traffic-counter-app/
├── app.py
├── requirements.txt
└── models/
    └── yolox_nano.onnx

サンプルコード

今回はできるだけ短くするために、HTMLも Python 側へ埋め込んだ1ファイル構成にしています。 Raspberry Pi 5 にUSBカメラを接続して起動すると、同一ネットワーク内のPCブラウザから映像を見ながら、縦の計測ラインを左から右、または右から左へ横切った人をカウントできます。 画面上にはカウントラインカウント方向を示す矢印を重ねて表示します。
※本サンプルコードは生成AIを用いて生成、実装しています。

import math
import traceback
from pathlib import Path

import cv2
import numpy as np
import onnxruntime as ort
from flask import Flask, Response

app = Flask(__name__)

MODEL_PATH = Path("models/yolox_nano.onnx")
INPUT_W, INPUT_H = 416, 416
PERSON_CLASS_ID = 0
VIS_SCORE_TH = 0.28
NMS_SCORE_TH = 0.05
NMS_TH = 0.45
MAX_DIST = 70
MAX_MISSED = 10
COUNT_DIRECTION = "left_to_right"  # "left_to_right" or "right_to_left"
LINE_POS_RATIO = 0.60

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

session = ort.InferenceSession(str(MODEL_PATH), providers=["CPUExecutionProvider"])

total_count = 0
tracks = {}
next_track_id = 1


def preprocess(frame):
    h, w = frame.shape[:2]
    ratio = min(INPUT_H / h, INPUT_W / w)
    nw, nh = int(w * ratio), int(h * ratio)
    resized = cv2.resize(frame, (nw, nh))
    padded = np.full((INPUT_H, INPUT_W, 3), 114, dtype=np.uint8)
    padded[:nh, :nw] = resized
    rgb = cv2.cvtColor(padded, cv2.COLOR_BGR2RGB)
    x = np.transpose(rgb.astype(np.float32), (2, 0, 1))
    return x[None, :, :, :], ratio


def postprocess(outputs):
    grids = []
    strides = []
    for s in (8, 16, 32):
        hs, ws = INPUT_H // s, INPUT_W // s
        xv, yv = np.meshgrid(np.arange(ws), np.arange(hs))
        grid = np.stack((xv, yv), axis=2).reshape(1, -1, 2)
        grids.append(grid)
        strides.append(np.full((1, grid.shape[1], 1), s))
    grids = np.concatenate(grids, axis=1)
    strides = np.concatenate(strides, axis=1)

    outputs[..., :2] = (outputs[..., :2] + grids) * strides
    outputs[..., 2:4] = np.exp(outputs[..., 2:4]) * strides
    return outputs


def nms(boxes, scores, thr):
    x1, y1, x2, y2 = boxes.T
    areas = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
    order = scores.argsort()[::-1]
    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])

        w = np.maximum(0, xx2 - xx1)
        h = np.maximum(0, yy2 - yy1)
        inter = w * h
        iou = inter / (areas[i] + areas[order[1:]] - inter + 1e-6)
        order = order[np.where(iou <= thr)[0] + 1]
    return keep


def infer_people(frame):
    x, ratio = preprocess(frame)
    output = session.run(None, {session.get_inputs()[0].name: x})[0]
    pred = postprocess(output.copy())[0]

    boxes = pred[:, :4]
    scores = pred[:, 4:5] * pred[:, 5:]
    person_scores = scores[:, PERSON_CLASS_ID]

    keep = person_scores >= NMS_SCORE_TH
    boxes = boxes[keep]
    person_scores = person_scores[keep]

    if len(boxes) == 0:
        return []

    xyxy = np.ones_like(boxes)
    xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2
    xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2
    xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2
    xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2
    xyxy /= ratio

    keep = nms(xyxy, person_scores, NMS_TH)

    h, w = frame.shape[:2]
    detections = []
    for i in keep:
        score = float(person_scores[i])
        if score < VIS_SCORE_TH:
            continue
        x1, y1, x2, y2 = xyxy[i]
        x1 = max(0, min(w - 1, int(x1)))
        y1 = max(0, min(h - 1, int(y1)))
        x2 = max(0, min(w - 1, int(x2)))
        y2 = max(0, min(h - 1, int(y2)))
        bw, bh = x2 - x1, y2 - y1
        if bw < 20 or bh < 40:
            continue
        detections.append({
            "bbox": (x1, y1, bw, bh),
            "center": (x1 + bw // 2, y1 + bh // 2),
            "score": score,
        })
    return detections


def update_tracks(detections, line_x):
    global total_count, tracks, next_track_id

    for t in tracks.values():
        t["matched"] = False

    for det in detections:
        cx, cy = det["center"]
        best_id = None
        best_dist = 1e9

        for tid, t in tracks.items():
            if t["matched"]:
                continue
            tx, ty = t["center"]
            dist = math.hypot(cx - tx, cy - ty)
            if dist < MAX_DIST and dist < best_dist:
                best_dist = dist
                best_id = tid

        if best_id is None:
            tid = next_track_id
            next_track_id += 1
            tracks[tid] = {
                "center": (cx, cy),
                "missed": 0,
                "counted": False,
                "matched": True,
            }
            det["track_id"] = tid
        else:
            t = tracks[best_id]
            prev_x, _ = t["center"]
            t["center"] = (cx, cy)
            t["missed"] = 0
            t["matched"] = True

            crossed = (
                prev_x < line_x <= cx
                if COUNT_DIRECTION == "left_to_right"
                else prev_x > line_x >= cx
            )
            if crossed and not t["counted"]:
                total_count += 1
                t["counted"] = True
            det["track_id"] = best_id

    remove_ids = []
    for tid, t in tracks.items():
        if not t["matched"]:
            t["missed"] += 1
            if t["missed"] > MAX_MISSED:
                remove_ids.append(tid)

    for tid in remove_ids:
        del tracks[tid]


def draw(frame, detections, line_x):
    h, w = frame.shape[:2]
    cv2.line(frame, (line_x, 0), (line_x, h), (255, 200, 0), 2)

    if COUNT_DIRECTION == "left_to_right":
        cv2.arrowedLine(frame, (line_x - 100, 40), (line_x - 20, 40), (0, 255, 255), 3, tipLength=0.25)
        label = "LEFT -> RIGHT"
    else:
        cv2.arrowedLine(frame, (line_x + 100, 40), (line_x + 20, 40), (0, 255, 255), 3, tipLength=0.25)
        label = "RIGHT -> LEFT"

    cv2.putText(frame, "COUNT LINE", (max(10, line_x - 70), 25),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 200, 0), 2, cv2.LINE_AA)
    cv2.putText(frame, label, (10, 75),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2, cv2.LINE_AA)
    cv2.putText(frame, f"COUNT: {total_count}", (20, h - 20),
                cv2.FONT_HERSHEY_SIMPLEX, 1.0, (20, 20, 255), 3, cv2.LINE_AA)

    for det in detections:
        x, y, bw, bh = det["bbox"]
        cx, cy = det["center"]
        score = det["score"]
        tid = det.get("track_id", -1)

        cv2.rectangle(frame, (x, y), (x + bw, y + bh), (0, 220, 0), 2)
        cv2.circle(frame, (cx, cy), 4, (0, 0, 255), -1)
        cv2.putText(frame, f"ID:{tid} {score:.2f}", (x, max(20, y - 8)),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 220, 0), 2, cv2.LINE_AA)


def gen_frames():
    while True:
        ok, frame = cap.read()
        if not ok:
            continue
        try:
            line_x = int(frame.shape[1] * LINE_POS_RATIO)
            detections = infer_people(frame)
            update_tracks(detections, line_x)
            draw(frame, detections, line_x)
        except Exception:
            traceback.print_exc()
            cv2.putText(frame, "ERROR", (20, 40),
                        cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 3, cv2.LINE_AA)

        ret, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
        if not ret:
            continue
        yield (
            b"--frame\r\n"
            b"Content-Type: image/jpeg\r\n\r\n" +
            buf.tobytes() +
            b"\r\n"
        )


@app.route("/")
def index():
    return """<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>簡易通行人数計測</title>
<style>
body { font-family: sans-serif; margin: 24px; background: #f6f7fb; }
.wrap { max-width: 960px; margin: auto; background: #fff; padding: 24px; border-radius: 16px; }
img { width: 100%; border: 1px solid #ddd; border-radius: 12px; }
</style>
</head>
<body>
<div class="wrap">
<h1>簡易通行人数計測</h1>
<img src="/video_feed" alt="camera stream">
</div>
</body>
</html>"""


@app.route("/video_feed")
def video_feed():
    return Response(gen_frames(), mimetype="multipart/x-mixed-replace; boundary=frame")


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=False)
Flask>=3.1
numpy>=2.0
opencv-python-headless>=4.10
onnxruntime==1.22.1

セットアップ手順

まずは Raspberry Pi 5Raspberry Pi OS を入れて起動しておきます。Raspberry Pi 公式でも、Raspberry Pi OS は公式サポートOSとして案内されています。
Raspberry Pi OS

その後、Raspberry Pi 5上で仮想環境を作り、依存ライブラリをインストールします。

mkdir traffic-counter-app
cd traffic-counter-app
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install -r requirements.txt

続いて、models ディレクトリを作成し、人クラスを含む YOLOX 系のONNX モデルmodels/yolox_nano.onnx という名前で配置します。 このサンプルコードは、COCO形式のクラス順で person class_id=0 のモデルを前提にしています。YOLOXの公式 ONNXRuntime ドキュメントには、事前生成済みのONNX モデルが案内されています。
YOLOX ONNXRuntime Guide

mkdir -p models
wget -O models/yolox_nano.onnx \
  https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_nano.onnx

app.py を保存したら、以下で起動します。

python app.py

同一ネットワーク内のPCブラウザから以下へアクセスします。
http://Raspberry PiのIPアドレス:5000

使い方

操作は以下の流れです。

  1. Raspberry Pi 5にUSBカメラを接続する

  2. カメラが入口や通路を安定して映せる位置に固定されていることを確認する

  3. Raspberry Pi 5上でアプリを起動する

  4. 同一ネットワーク内のPCブラウザからアクセスする

  5. 計測ラインを指定方向にまたいで人が通過すると、自動でカウントが増える

  6. 必要に応じて COUNT_DIRECTIONLINE_POS_RATIO を調整する

このサンプルでは、縦の計測ラインをまたぐ通過人数を数えます。
COUNT_DIRECTION = "left_to_right" にすると左から右COUNT_DIRECTION = "right_to_left" にすると右から左の通過だけをカウントします。矢印もその方向に合わせて映像上へ表示されます。

実装のポイント

今回の実装は、人数計測の流れをなるべくストレートに書いています。まずUSBカメラからフレームを取得し、ONNX Runtimeで人物検出を実行します。その結果から人クラスだけを抽出し、YOLOXの出力を後処理したうえで、独自の簡易 NMSで重複した矩形を整理してから画面上へバウンディングボックスを重ねています。人物検出モデルと軽量な後処理だけで、簡易カウンタとして成立する点がこのPoCの面白いところです。
YOLOX ONNXRuntime Guide

また、ライブ映像のブラウザ表示は Flask のストリーミングレスポンスで実現しています。Flask 公式ドキュメントでは generator で逐次データを返す方法が説明されており、この方式を使うとMJPEGのような単純なライブ表示を作りやすいです。
Flask Streaming

カメラ入力には OpenCVの VideoCapture を使っています。これはカメラデバイスを開いてフレームを読むための標準的なクラスで、Raspberry Pi 5に接続したUSBカメラを扱うPoCとしてもシンプルです。
OpenCV VideoCapture

さらに、同じ人を何回も数えないために、今回は中心点ベースの簡易追跡を入れています。矩形の中心が前フレームのどの中心に近いかを見てIDを引き継ぎ、そのIDが初めて縦の計測ラインを指定方向に横切ったときだけカウントを加算するようにしています。大規模な追跡ライブラリを使わなくても、PoCとしてはこの程度の簡易追跡で十分試しやすいです。

また、今回のブラウザ画面はかなり簡素で、カウント値を別APIで更新する構成にはせず、映像フレーム上に直接人数を描画しています。これにより、HTMLやJavaScriptを最小限にでき、全体のコードも短く保ちやすくなります。

注意点

このサンプルは「Raspberry Pi 5上で、固定カメラ映像から人が縦ラインをまたいだ回数を数える」という最小構成です。そのため、実運用を考えるといくつか注意点があります。

まず、この構成は人物検出モデルの結果を使って人数を数える方式なので、強い逆光、混雑による重なり、横並びでの通過、部分的な遮蔽などがあると、見落としや二重カウントが起きる可能性があります。特に入口付近で人同士が密集しやすい環境では、計測ラインの位置とカメラ角度の調整が重要です。

また、固定カメラ前提のPoCなので、カメラの揺れや大きな画角変化があると追跡が不安定になりやすいです。人の頭上から見るのか、斜め前方から見るのかでも、検出しやすさや重なり方がかなり変わります。実際の利用環境では、通路幅・設置高さ・逆光の有無を先に確認しておくと調整しやすくなります。

Raspberry Pi 5 は高性能な部類ですが、それでもエッジ側で連続推論を行う以上、入力解像度やフレームレートを上げすぎると負荷が増えます。PoCの段階では640×480程度のカメラ入力から試し、モデル入力サイズやJPEG品質を含めて全体のバランスを見るほうが扱いやすいです。
Raspberry Pi

さらに、通過人数の精度は、VIS_SCORE_THNMS_SCORE_THNMS_THMAX_DISTLINE_POS_RATIOカメラの画角に影響されます。遠距離の小さな人物も拾いたいのか、近距離の導線だけを対象にしたいのかで、しきい値はかなり変わります。PoCの段階では固定値でも十分ですが、実際の利用環境で調整したほうが安全です。

このサンプルは、1方向だけを数える前提です。COUNT_DIRECTION で左→右または右→左のどちらか一方を選ぶ形なので、双方向を同時に集計したい場合は、上下流向けに別カウンタを持たせるなどの拡張が必要です。

最後に、学習済みモデルの配布条件や利用条件は、使用する重みファイルごとに確認してください。実装側のライブラリやリポジトリのライセンス確認に加えて、モデルファイルそのものの利用条件も分けて見ておくと安心です。

また、本サンプルでは中心点ベースの簡易追跡を採用しているため、人の密集が多い環境ではIDの入れ替わりによる誤カウントが発生する可能性があります。

今後の改善案

今回のサンプルは最小構成なので、今後は以下のような拡張が考えられます。

  • 双方向カウントに対応する
    左→右だけでなく、右→左も別カウンタで集計すると、往来人数を分けて見られるようになります。

  • ROI(計測対象領域)の限定
    画面全体ではなく通路部分だけを対象にすると、周辺の不要な検出の影響を減らしやすくなります。

  • CSVログ保存を追加する
    時刻ごとの通過人数を保存しておけば、時間帯別の傾向確認や簡易レポート作成に使いやすくなります。

  • カウントリセットや設定UIを追加する
    今回はコードを短くするため画面を最小構成にしていますが、運用しやすさを上げるなら、ブラウザからリセットや方向切り替えができるようにしても便利です。

  • systemd で自動起動する
    Raspberry Pi 5の電源投入後にアプリを自動で立ち上げるようにすると、現場設置がしやすくなります。

関連技術参考トピック

人物検出や映像解析を活用したAI処理への対応として、RenesasはRZ/Vシリーズを展開しています。今後、顔認識やAIを活用したさまざまなアプリケーションへの展開を検討する際には、RZシリーズの活用も有効な選択肢になると感じました。

RZ/V Embedded AI MPU

まとめ

今回のPoCでは、Raspberry Pi 5とUSBカメラを用いて、歩行者交通量の簡易計測を行うWebアプリを作成してみました。人物検出にYOLOX、推論実行にONNX Runtime、Web画面にFlaskを利用することで、比較的シンプルな構成でも交通量計測の基本的な仕組みを体験できます。

本記事では、交通量計測の中でも歩行者の通行人数計測に対象を絞り、固定カメラ映像から計測ラインを通過した人数をカウントする最小構成のPoCとして実装しました。実運用では精度向上や双方向カウント、データ蓄積機能などが必要になりますが、エッジAIを活用した交通量計測システムの第一歩としては十分試しやすい題材だと感じています。

また、今回のような人物検出や映像解析を活用したアプリケーションは、混雑度推定、施設利用状況の可視化、車両を含めた交通量分析などへ発展させることも考えられます。PoCから実システムへの展開を検討する際には、Linuxベースのアプリケーション実装やAI推論に対応したRenesasのRZ/Vシリーズも有力な選択肢の一つとなるでしょう。

今回の作例が、エッジAIや画像処理を活用した交通量計測システムを手軽に試してみるきっかけになれば幸いです。

参考リンク

Raspberry Pi 5
Raspberry Pi Software / Raspberry Pi OS
OpenCV License
OpenCV VideoCapture
Flask: Streaming Contents
YOLOX GitHub Repository
YOLOX ONNXRuntime Guide
ONNX Runtime GitHub Repository
RZ/V Embedded AI MPU