Ultralytics YOLO27: ウェイトリストに登録する。
Get Started
本番環境でのUltralytics YOLO·トラッキングとパイプラインの構築·レッスン 6/9
レッスン中級

カウント、ヒートマップ、速度推定

トラッキングを基盤として、ラインカウンター、ゾーン占有率、ヒートマップ、速度推定を構築し、適切な方法でキャリブレーションします。

本番環境のビジョンシステムのほとんどはmodel.predict()ではありません。トラッカーに、オブジェクトが横切るライン、オブジェクトが占有するゾーン、ピクセルをメートルに変換するスケールといった簡単な幾何処理を組み合わせています。結果が狂う原因は幾何処理にあります。このレッスンのデモ動画で速度推定器が歩行者の速度を実際の4分の1にあたる時速1 kmと報告するのは、1つの数値を推測で設定しているためです。

成果

実際の動画でラインカウンター、ゾーンカウント、ヒートマップ、速度推定を構築し、既知の基準値と照らし合わせて速度をキャリブレーションします。

短期集中コース
すでに使い方をご存じの方は、こちらをご覧ください。
  1. ライン通過: solutions.ObjectCounter(region=[p1, p2])。トラックの中心がラインを越えると、INまたはOUTに加算されます。

  2. ゾーン占有率: 追跡中の各ボックスに対して点とポリゴンの内外判定を1回行うと、現在ゾーン内にいるオブジェクトがわかります。

  3. ヒートマップ: solutions.Heatmapは、追跡中のオブジェクトが時間を過ごした場所を蓄積します。

  4. 速度: ピクセル変位 × 1ピクセルあたりのメートル数 ÷ 時間。シーン内のスケールを測定し、実際のFPSを確認します。

実践的

トラッキングと幾何処理#

ultralytics.solutions内の各ソリューションクラスは、内部でTrackモードを実行し、その後、トラック履歴に独自の幾何処理を適用します。pip install "ultralytics[solutions]"を使って追加パッケージを一度インストールしてください。

クラス幾何形状主な結果
ObjectCounter2点 = ライン、3点以上 = ポリゴンin_count, out_count, classwise_count
RegionCounter1つ以上の名前付きポリゴンregion_counts
Heatmapボックスの面積を時間経過とともに蓄積plot_im(領域を指定するとカウントも実行)
SpeedEstimatorトラックの変位 × meter_per_pixelspeed_dict(トラックIDごとのkm/h)

ソリューションはデフォルトでtracker="tracktrack.yaml"を使用します。前のレッスンで検証したトラッカーを渡してください。

ライン通過をカウントする#

Ultralytics YOLOによる倉庫のコンベヤー上の荷物カウント。各箱にトラックID(#013~#018)が付き、シアンのカウントラインを箱が通過する場所でINカウンターが12を示しています

カウントラインは線分です。オブジェクトは2つのフレーム間でボックスの中心がラインを横切ったときに1回だけカウントされ、どちら側から来たかによってINかOUTかが決まります。そのため、上のコンベヤーではトラッキングが必要です。IDがなければ、ボックス#015がラインに触れるたびに再びカウントされてしまいます。

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))  # 640x360, 30 FPS, 62 frames
w, h, fps = (int(cap.get(p)) for p in (cv2.CAP_PROP_FRAME_WIDTH, cv2.CAP_PROP_FRAME_HEIGHT, cv2.CAP_PROP_FPS))
writer = cv2.VideoWriter("counted.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))

counter = solutions.ObjectCounter(
    model="yolo26n.pt",
    region=[(0, 180), (640, 180)],  # two points: a horizontal line across the middle of the frame
    classes=[0],  # person
    tracker="bytetrack.yaml",
    verbose=False,
)

while True:
    ok, frame = cap.read()
    if not ok:
        break
    results = counter(frame)  # detect + track + count in one call
    writer.write(results.plot_im)

cap.release()
writer.release()
print(f"in={results.in_count} out={results.out_count} classwise={results.classwise_count}")
in=2 out=2 classwise={'person': {'IN': 2, 'OUT': 2}}

クリップの2秒間に4人が広場の中央を横切り、各方向に2人ずつでした。デフォルトのTrackTrack設定では、同じスクリプトがin=0 out=1を報告します。トラックが少ないほど、横断数も少なくなるためです。数値を信頼する前に、counted.mp4を開き、各横断を目視で確認してください。

Ultralytics YOLOによる中央分離帯のある道路のライン通過カウンター。追跡された車やトラックがシアンのラインを横切り、矢印で方向を示し、車両数はINが24、OUTが18と表示されています

道路では同じラインを使って交通を方向別に分けます。上向きに横切る車両はIN、下向きに横切る車両はOUTとしてカウントされます。オブジェクトが明確に直角に横切る場所にラインを配置し、フレームの端、遮蔽物、ボックスが小さくなる道路の遠端は避けてください。

ゾーン内の占有数をカウントする#

ポリゴンゾーンを使うと、「現在、何個のオブジェクトが中にいるか」を確認できます。領域カウントガイドにはすぐに使えるRegionCounterが用意されています。その内部では、追跡中の各ボックスに対して点とポリゴンの内外判定を1回行います。

from shapely.geometry import Point, Polygon

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
zone = Polygon([(100, 100), (500, 100), (500, 300), (100, 300)])  # pixel coordinates

peak = 0
for r in model.track(video, stream=True, tracker="bytetrack.yaml", classes=[0], verbose=False):
    if not r.boxes.is_track:
        continue
    inside = sum(zone.contains(Point(cx, cy)) for cx, cy in r.boxes.xywh[:, :2].tolist())
    peak = max(peak, inside)
print(f"peak occupancy: {peak} people inside the zone")
peak occupancy: 10 people inside the zone

xywh[:, :2]は各ボックスの中心です。待ち行列や滞在時間を扱う場合は、ゾーンに入った時刻をIDごとに記録してください。

速度を推定する#

高速道路での速度推定。追跡された各車とトラックに推定速度(48~65 km/h)がラベル表示され、短い移動軌跡が付いています

上の各ラベルは、車両の後ろにある軌跡をもとに算出されています。既知のフレーム数の間にトラックがピクセル単位で移動した距離を示しています。速度 = ピクセル単位の距離 × 1ピクセルあたりのメートル数 ÷ 経過時間です。結果の正確さを左右する入力は2つあります。

  1. 1ピクセルあたりのメートル数。 オブジェクトが移動する深度で、シーン内の既知の長さ(床タイル、車線標示、駐車スペースなど)を測定します。斜めに設置されたカメラでは、代わりに4点ホモグラフィを使って地面の平面をマッピングします。
  2. 実際の撮影フレームレートです。速度はFPSに応じて変化します。映像を再エンコードしたり、速度を落としたりすると、すべての速度が同じ倍率で変化します。

このループでは、各ボックスの下部中央(物体が地面に接する位置)から測定し、各トラックの値を10フレームにわたって平均します。

from collections import defaultdict, deque

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
fps = 30  # the real capture rate, not the playback rate
pixels_per_meter = 33  # example: a 1.2 m floor feature spans 40 px; measure your own scene

prev = {}  # track ID -> last bottom-center point
window = defaultdict(lambda: deque(maxlen=10))  # about 0.3 s of speeds per track at 30 FPS
for r in model.track(video, stream=True, tracker="bytetrack.yaml", classes=[0], verbose=False):
    if not r.boxes.is_track:
        continue
    for (x1, y1, x2, y2), tid in zip(r.boxes.xyxy.tolist(), r.boxes.id.int().tolist()):
        cx, cy = (x1 + x2) / 2, y2  # bottom-center sits on the ground plane
        if tid in prev:
            px, py = prev[tid]
            meters = ((cx - px) ** 2 + (cy - py) ** 2) ** 0.5 / pixels_per_meter
            window[tid].append(meters * fps * 3.6)  # km/h
        prev[tid] = (cx, cy)

smoothed = sorted(sum(s) / len(s) for s in window.values() if len(s) == s.maxlen)
print(f"{len(smoothed)} tracks; median {smoothed[len(smoothed) // 2]:.1f} km/h")
29 tracks; median 2.1 km/h

人の歩行速度は時速4~5 kmなので、時速2.1 kmという結果は、このシーンではサンプルのスケールが誤っていることを示しています。pixels_per_meterの値はここで測定したものではありません。中央値を既知の速度と比較するこの妥当性チェックが重要です。

solutions.SpeedEstimatorは、同じ考え方を本番環境向けにパッケージ化したものです。max_histフレームにわたって各トラックのボックス中心を測定し、meter_per_pixelで変換して、IDごとに1つの速度を固定します(上限はmax_speedです)。

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))
speed = solutions.SpeedEstimator(
    model="yolo26n.pt",
    classes=[0],
    tracker="bytetrack.yaml",
    fps=30,  # the real capture rate, not the playback rate
    meter_per_pixel=0.03,  # a guess: a ~0.45 m shoulder width spans ~15 px here
    max_hist=15,  # 0.5 s of history before a speed is locked
    verbose=False,
)

speeds = {}
while True:
    ok, frame = cap.read()
    if not ok:
        break
    speeds.update(speed(frame).speed_dict)  # {track_id: km/h} once locked
cap.release()

kmh = sorted(speeds.values())
print(f"{len(kmh)} tracks; median {kmh[len(kmh) // 2]} km/h, range {kmh[0]}-{kmh[-1]} km/h")
25 tracks; median 1 km/h, range 0-4 km/h

同じ理由で、また約4倍ずれています。物体の大きさからスケールを推測しているためです。このようなトップダウンカメラではボックス中心で問題ありませんが、斜め向きのカメラでは下部中央のループかホモグラフィを使用してください。

ヒートマップを蓄積する#

都市高速道路のUltralytics YOLOヒートマップ。青から赤のオーバーレイは交通量の多い車線ほど明るく、車とトラックのボックス、および車12台、トラック2台の累計数が表示されています

ヒートマップは、各フレームで追跡対象のすべてのボックスの下に熱量を加えるため、交通量の多い車線や通路ほど明るく表示されます。regionを指定すると、上の高速道路の例のように物体もカウントします。

import cv2

from ultralytics import solutions
from ultralytics.utils.downloads import safe_download

video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))
heatmap = solutions.Heatmap(
    model="yolo26n.pt",
    classes=[0],
    tracker="bytetrack.yaml",
    colormap=cv2.COLORMAP_PARULA,
    verbose=False,
)

while True:
    ok, frame = cap.read()
    if not ok:
        break
    results = heatmap(frame)
cap.release()
cv2.imwrite("heatmap.jpg", results.plot_im)  # 蓄積したヒートオーバーレイを表示する最後のフレーム

何時間もの映像にわたって、同じオーバーレイから混雑した通路、使われていない区域、そして人々が実際に通る経路が分かります。ワークアウトのモニタリングとStreamlitのライブ推論UIは、同じ構成要素から作られています。

よくある落とし穴#

1フレームごとにモデルを2回実行する

症状: カウンターのパイプラインが、想定FPSの半分で動作します。 対処: ObjectCounter、Heatmap、SpeedEstimatorは内部で検出とトラッキングを行います。これらを独自のmodel.track()ループで囲んでr.orig_imgを渡さず、生のフレームを入力してください。

未校正の速度を信用する

症状: 歩行者の速度が1 km/h、または車の速度が300 km/hと表示されます。 対処: 物体が移動する既知の距離からmeter_per_pixelを測定し、カメラの実際のFPSを確認して、中央値を既知の速度(歩行速度や標識の制限速度)と照合します。

斜めに設置したカメラではボックスの下端中央を使う

物体がカメラに近づいてボックスが大きくなると、幾何学的な中心位置は上がります。下端中央は物体が地面に接する位置に近似するため、そこから測定した距離は実際の移動をより正確に反映します。

試してみる

独自のドメインの動画(またはデモクリップ)でラインカウンターを作成し、counted.mp4内のすべての通過を目視で確認します。次に、シーン内の実際の長さを1つ測定し、ソリューションスクリプトでスケールを計算して、speed_loop.pyを再実行し、速度の中央値を既知の基準値と比較します。カウント、スケール、変更前後の速度の中央値を提示してください。

完了条件
以下のすべてを満たすと、レッスンは完了です。
  • ラインカウンターのINとOUTが、同じクリップを手動で数えた結果と一致しています。

  • 速度の推定にはシーン内で測定したスケールを使用しており、中央値は既知の基準速度の25%以内です。

  • 斜め向きのカメラで速度を測定する際、ボックスの下部中央が適切な位置である理由を説明できます。

解答を表示

フレーム内で測定できるものを使ってキャリブレーションし、妥当性を確認してください。

# Two points along a floor feature whose real length you know (read them off a saved frame)
(x1, y1), (x2, y2) = (100, 300), (140, 300)
real_length_m = 1.2  # e.g. measured tile width or lane marking

pixels = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print(f"pixels_per_meter = {pixels / real_length_m:.1f}, meter_per_pixel = {real_length_m / pixels:.4f}")
pixels_per_meter = 33.3, meter_per_pixel = 0.0300

pixels_per_meterをspeed_loop.pyに(またはmeter_per_pixelをSpeedEstimatorに)設定して、再実行してください。歩行者の速度が依然として時速4~5 kmから大きく外れる場合は、スケールを誤った奥行きで測定したか、FPSが誤っています。速度を半分にしたクリップでは、すべての測定値が半分になります。デモクリップでは、どちらのスクリプトも同じ推定スケール(1ピクセルあたり0.03 m、つまり1 mあたり33ピクセル)を使用し、実際より2~4倍低い値を示しました。つまり、この映像では1ピクセルが推定より広い範囲を表しているか、クリップの実際の撮影フレームレートが30 FPSではありません。どちらなのかを確かめるには、シーン内での測定が必要です。

次のステップ

モデル1つ、ストリーム1つなら管理できます。次は複数のストリームを並列処理し、それに伴うスレッドセーフな推論の運用方法を学びます。