カウント、ヒートマップ、速度推定
トラッキングを基盤として、ラインカウンター、ゾーン占有率、ヒートマップ、速度推定を構築し、適切な方法でキャリブレーションします。
本番環境のビジョンシステムのほとんどはmodel.predict()ではありません。トラッカーに、オブジェクトが横切るライン、オブジェクトが占有するゾーン、ピクセルをメートルに変換するスケールといった簡単な幾何処理を組み合わせています。結果が狂う原因は幾何処理にあります。このレッスンのデモ動画で速度推定器が歩行者の速度を実際の4分の1にあたる時速1 kmと報告するのは、1つの数値を推測で設定しているためです。
実践的
トラッキングと幾何処理#
ultralytics.solutions内の各ソリューションクラスは、内部でTrackモードを実行し、その後、トラック履歴に独自の幾何処理を適用します。pip install "ultralytics[solutions]"を使って追加パッケージを一度インストールしてください。
| クラス | 幾何形状 | 主な結果 |
|---|---|---|
ObjectCounter | 2点 = ライン、3点以上 = ポリゴン | in_count, out_count, classwise_count |
RegionCounter | 1つ以上の名前付きポリゴン | region_counts |
Heatmap | ボックスの面積を時間経過とともに蓄積 | plot_im(領域を指定するとカウントも実行) |
SpeedEstimator | トラックの変位 × meter_per_pixel | speed_dict(トラックIDごとのkm/h) |
ソリューションはデフォルトでtracker="tracktrack.yaml"を使用します。前のレッスンで検証したトラッカーを渡してください。
ライン通過をカウントする#

カウントラインは線分です。オブジェクトは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を開き、各横断を目視で確認してください。

道路では同じラインを使って交通を方向別に分けます。上向きに横切る車両は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 zonexywh[:, :2]は各ボックスの中心です。待ち行列や滞在時間を扱う場合は、ゾーンに入った時刻をIDごとに記録してください。
速度を推定する#

上の各ラベルは、車両の後ろにある軌跡をもとに算出されています。既知のフレーム数の間にトラックがピクセル単位で移動した距離を示しています。速度 = ピクセル単位の距離 × 1ピクセルあたりのメートル数 ÷ 経過時間です。結果の正確さを左右する入力は2つあります。
- 1ピクセルあたりのメートル数。 オブジェクトが移動する深度で、シーン内の既知の長さ(床タイル、車線標示、駐車スペースなど)を測定します。斜めに設置されたカメラでは、代わりに4点ホモグラフィを使って地面の平面をマッピングします。
- 実際の撮影フレームレートです。速度は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倍ずれています。物体の大きさからスケールを推測しているためです。このようなトップダウンカメラではボックス中心で問題ありませんが、斜め向きのカメラでは下部中央のループかホモグラフィを使用してください。
ヒートマップを蓄積する#

ヒートマップは、各フレームで追跡対象のすべてのボックスの下に熱量を加えるため、交通量の多い車線や通路ほど明るく表示されます。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は、同じ構成要素から作られています。
よくある落とし穴#
症状: カウンターのパイプラインが、想定FPSの半分で動作します。 対処: ObjectCounter、Heatmap、SpeedEstimatorは内部で検出とトラッキングを行います。これらを独自のmodel.track()ループで囲んでr.orig_imgを渡さず、生のフレームを入力してください。
症状: 歩行者の速度が1 km/h、または車の速度が300 km/hと表示されます。 対処: 物体が移動する既知の距離からmeter_per_pixelを測定し、カメラの実際のFPSを確認して、中央値を既知の速度(歩行速度や標識の制限速度)と照合します。
物体がカメラに近づいてボックスが大きくなると、幾何学的な中心位置は上がります。下端中央は物体が地面に接する位置に近似するため、そこから測定した距離は実際の移動をより正確に反映します。
解答を表示
フレーム内で測定できるものを使ってキャリブレーションし、妥当性を確認してください。
# 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.0300pixels_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ではありません。どちらなのかを確かめるには、シーン内での測定が必要です。