YOLO11n × Python で交通量調査に挑戦・2025年版 ─ 昼夜検証で分かった弱点

YOLO11n × Python で交通量調査に挑戦・2025年版 ─ 昼夜検証で分かった弱点 HowTo

はじめに

「Webカメラで交通量調査」。
このネタを最初に取り上げたのは2023年のことでした。当時は、OpenCVの背景差分や輪郭抽出をベースにした、いわば“力業のゴリ押しコード”で挑戦したものです。ラズパイ3BとUSBカメラの組み合わせでは処理速度が足りず、数台の車を数えるだけで息切れしていたことを覚えています。

それでも「安価なカメラとPythonだけでリアルタイムに車をカウントできる」ことは十分に面白く、夏休みの自由研究のような実験記事としてまとめたのでした。

あれから2年。GPU環境や物体検出ライブラリの整備は一気に進みました。特に YOLO 系モデルは進化を続け、ついに YOLO11n という軽量版が登場しています。従来のように「PyTorchが難しいから手が出せない」といったハードルは大きく下がり、CPUだけでもそこそこ動く時代が来ています。

そこで今回は、前回断念した「YOLOベースの交通量調査」に再挑戦します。
果たして、昼間の精度はどこまで向上するのか? CUDAを使った場合とCPUのみの場合で、どの程度の性能差があるのか? そして、夜間の検証ではどんな壁にぶつかるのか?

「実験しながら遊ぶ」という前回のノリはそのままに、今回はより実用に近づいた最新の環境で試してみたいと思います。

YOLO11nとは何か(開発元と系譜もざっくり)

YOLOの出自

「You Only Look Once」は 2016 年のRedmonらの論文で登場した“一発推論”の物体検出法。画像全体を一度見てバウンディングボックスとクラスを単一ネットワークで同時予測するのが肝です。

系譜の分岐とUltralyticsの役割

その後はDarknet系(例:YOLOv4)やPyTorch系に枝分かれ。実務で広く使われた節目の一つがUltralytics(ウルトラリティクス)のPyTorch実装(YOLOv5以降)で、学習・推論・エクスポートまでを一本化した使い勝手の良さが普及を後押ししました。現在は YOLO11 が最新世代の主力で、公式ドキュメントとライブラリはUltralyticsが提供・保守しています。

YOLO11(Ultralytics)とは

Ultralytics版のYOLOは検出だけでなく、セグメンテーション/分類/ポーズ推定/回転物体検出(OBB)まで単一APIで扱える“タスク統合フレーム”。CLI・Pythonの両方で同じ感覚で使え、学習・評価・トラッキング・エクスポートの各“モード”も用意されています。

“n”の意味と11nの立ち位置

モデルサイズは n(nano)/s/m/l/x のスケール展開で、11nは最軽量。CPU単体でも回る軽さが特徴で、GPUがあればさらに伸びる——今回あなたの検証で示した「CPUでも実用域、GPUで+αの余裕」はまさにこの設計思想どおりです(実測は後章で)。公式も“YOLO11は最新世代のSOTAを多タスクで提供する”と位置づけています。

追跡(MOT)との相性

11系はUltralyticsのトラッキング機能とも統合しやすく、代表的な外部手法 ByteTrack(低スコア検出も活用してIDを落としにくい)との組み合わせは“ゲート通過カウント”の現場で相性が良いです。

補足:クラス体系(COCO)
既定の学習データはCOCO準拠で、人・クルマ以外に book、remote、handbag など“日常系”も多く含みます。CLASSES で対象を [2,5,7](car/bus/truck)に絞れば交通量調査、[0](person)に切り替えれば学園祭の人数カウントへ……と、用途スイッチが効きます(詳細はパラメータ解説の章で)。

環境と実装枠

ここでは、実際に検証を行った環境や基本的な実装の枠組みを整理しておきます。細かなバージョンや機材名は後で差し込めるように「枠」だけ提示します。


使用機材

  • PC本体
    – Intel core i7 8700 (6コア/12スレッド)
    – DDR4 2666 32GB
  • GPUモデル
    – RTX 3060
  • カメラ
    – 今回はテスト用映像を使用

ソフトウェア環境

  • OS: Windows 11
  • Python バージョン: Python 3.11.5
  • 主要ライブラリ
    • opencv-python 4.11.0.86
    • numpy 2.2.4
    • ultralytics 8.3.193 / YOLO11n

実装の基本枠

  • 入力: 動画ファイル
  • 物体検出: YOLO11nによる推論(CPU / CUDA 双方で実行)
  • カウント方法:
    • 画面内に「ゲート」を設定し、通過するオブジェクトを集計
    • UI操作でゲートの位置や厚みを変更可能
    • CLASSES パラメータで対象を絞り込み(例:車・バス・トラック)
  • 出力:
    • 処理結果のリアルタイム描画(バウンディングボックス、カウント数)
    • 必要に応じてキャプチャ保存

昼間の検証(YOLO11n)

今回の検証では、Pixabayで公開されている交通映像を利用しました。

理想的なアングルで、車線全体を真横から俯瞰する構図。カメラ設置の都合で視界が斜めになる実地映像より、検証には適しています。


CPUとCUDAの比較結果

  • CPUのみ:平均 約9fps
  • CUDA利用(RTX GPU):平均 約19fps
  • カウント結果:どちらも 67台 → ブレなし

つまり、推論速度は倍以上の差があるものの、昼間のシンプルな交通映像ではCPUでも十分実用という結果になりました。


CPUで動いた意外性

これまでYOLOといえば「GPU必須」のイメージが強かったのですが、YOLO11nは軽量設計のため、CPUでも安定してリアルタイム解析が可能です。処理落ちで取りこぼす可能性はゼロではないものの、実際のカウント結果はCUDAと一致しました。

この点は、学園祭の入場者カウントや簡易的な交通調査など「GPU環境を持ち出せない現場」においても、ノートPC+Webカメラだけでお手軽運用可能という大きなメリットになります。

CUDA vs CPUの考察

昼間のPixabay映像では、CPU 9fps/CUDA 19fps という数値差が出ました。
一見「GPUの勝ち」ですが、実際のカウント結果は どちらも67台で一致
この「速度は倍、結果は同じ」というギャップこそがYOLO11nの特徴をよく表しています。


差が小さく見える理由

  • 軽量モデル設計
    YOLO11nは nano モデル。パラメータ数が小さいため、CPUでも処理落ちしにくい。
  • ボトルネックは別にある
    • 映像のデコード(動画をフレームに展開する処理)
    • OpenCVの描画(矩形やテキストオーバーレイ)
      これらがCPU側で処理され、GPU推論が速くても足を引っ張る。
  • imgszを960に設定
    高解像度入力なら差は広がるが、実用的な解像度では「CPUでも十分」の範囲に収まる。

CUDAを使う意味

  • fpsに余裕がある分、取りこぼしリスクが下がる
    CPUでギリギリの9fpsでは、ストリームソース次第でフレーム欠落が起きやすい。
  • 並列処理が効く場面で差が顕著
    高解像度映像や複数カメラ入力では、GPUの有無で結果が大きく変わる。

実務的な読みどころ

  • 「CUDA必須」ではない → CPUオンリー環境でも動く
  • 「GPUは無意味」でもない → 安定性と余裕を与える

このバランス感が、YOLO11nを「研究用途だけでなく、現場にも持ち出せる」存在にしています。

夜間の検証(YOLO11n)

次に検証したのは夜間映像です。


結果

  • CUDA利用で 38fps 前後 と処理速度自体は快調。
  • しかし、検出オブジェクトがほとんど出ない
  • conf値を下げたり、imgszを大きくしてみても改善せず。

なぜ検出できないのか

  • 夜間は光源(ヘッドライトや街灯)が強いコントラストを作る → モデルが混乱しやすい。
  • COCO学習データセットは昼間映像が中心 → 夜間データが不足。
  • 黒い車体や暗い背景は、YOLO11nの軽量モデルでは特に厳しい。

まとめ

YOLO11nは昼間なら十分に実用レベルの精度を示しましたが、夜間は小手先のパラメータ調整ではどうにもならない壁に直面しました。

ここから先は、別アプローチ(力業) の必要性が見えてきます。

夜間専用アプローチ

YOLO11nが夜間ではほとんど機能しなかったため、別の手法を試しました。
結論から言うと──力業のアプローチが夜間では有効でした。


力業とは?

ここでいう「力業」とは、機械学習モデルに頼らず、動き(フロー)や光の変化に敏感なアルゴリズムで対象を検出する方式です。

  • 映像内の動きを直接追跡する
  • ヘッドライトや車体の動きに反応して通過を検出
  • 暗闇でも比較的安定して動体を拾える

成果

  • 昼間では不要だが、夜間では有効
  • 上り下りをきっちりカウントできる場面も確認
  • 弱点:長い車両(トレーラー等)は 2〜4 カウントされやすい

ハイブリッド運用の現実解

  • 昼はYOLO11n で高精度にカウント
  • 夜は力業 で光や動きを頼りに数える
  • 両者を切り替えることで、24時間の交通量調査も視野に入る

今後の展開

記事中では映像キャプチャを提示するに留めますが、実地では RTSPカメラを使った夜間検証 にも進める予定です。
街灯や交通状況に応じた調整が必要になりますが、需要は確実にある領域です。

カメラ設置とアングルの重要性

交通量調査において、どこにカメラを置くかは検出アルゴリズムと同じくらい重要です。YOLOであれ力業であれ、アングルが悪ければ精度は大きく落ち込みます。


理想的な設置例

  • 歩道橋や高所からの俯瞰
    • 車線全体を真上に近い角度で捉えると、車両の重なりが少なくなる。
    • 昼間のPixabay映像のように、横幅いっぱいに道路を収められると精度は大きく向上する。

不適切な設置例

  • 斜め方向からの視点
    • 車体が重なって検出が難しくなる。
    • 停止線や交差点付近は動きが不規則で誤検出が増える。
  • 遮蔽物がある位置
    • ガードレールや信号柱が画角に入ると、アルゴリズムが混乱する。

実地の工夫

  • RTSP対応の防犯カメラを設置し、なるべく高い位置から正面に近いアングルを狙う。
  • 照明条件の確認も大切。夜間は街灯やヘッドライトの映り方で精度が大きく変わる。
  • 実地での最適化は不可欠。記事のコードにある「ゲート位置・厚みの調整機能」は、まさに現場での微調整に役立つ。

コードとパラメータ解説

今回の記事では、YOLO11nをベースにした交通量カウンターのコードを公開します。録画機能など余分な部分は省き、シンプルに「検出 → カウント → 表示」が動く形にしています。


主なパラメータと調整ポイント

CLASSES = [2, 5, 7]   # 交通量調査用:car, bus, truck
  • CLASSES は対象とする物体クラスを指定します。
  • 交通量調査では [2,5,7](car, bus, truck)が基本。
  • 学園祭の入場者カウントなら [0](person)に切り替えるだけで流用可能です。

COCOデータセットのクラスには「book」「remote」「handbag」など、交通と関係ないものも多数含まれています。面白いので、ぜひチェックしてみてください。意外な使い途が見つかるかもしれませんよ。


IMG_SIZE = 960   # 入力画像サイズ
CONF = 0.25      # 検出信頼度しきい値
  • IMG_SIZE を上げると検出精度が上がるが、処理速度は落ちます。
  • CONF を下げると小さい対象も拾いやすくなるが、誤検出も増えます。

# 初期ゲート(画面中央に水平バンド)
gate = [int(w*0.1), int(h*0.45), int(w*0.9), int(h*0.55)]
  • ゲートの位置・厚みをキーボードで調整可能。
  • 現場で角度や高さが異なる場合に合わせやすく、実用性を高めています。

操作キー(例)

  • W/S/A/D … ゲートの移動
  • H/L … ゲートの厚み調整
  • R … リセット
  • N … 夜モード(confを下げて再検出)
  • Q … 終了

実用の幅を広げる一例

  • 交通量調査:車両クラス指定
  • イベント来場者カウント[0](person)に切り替え
  • 動物カウント:COCOの該当クラスを指定すれば、犬や猫を数えることも可能

コード

# yolo11n_gate_counter.py
#
# 以下のコードは MIT ライセンスの下で公開します。
# ご自由に利用・改変・再配布して構いませんが、無保証です。
#
# YOLO11n を用いた交通量カウンター
#
# Usage:
#   python yolo11n_gate_counter.py [input.mp4 or rtsp://...]
#   引数でソースを差し替え可:python yolo11n_gate_counter.py input.mp4
#
# 操作キー:
#   Q / ESC : 終了
#   W/A/S/D : ゲート移動(上下左右)
#   H / L   : ゲート厚み調整
#   R       : リセット
#   N       : 夜モード切替(conf値を下げる)
#   G       : GPU/CPU切替
#   Z       : 入力サイズ切替(640/832/960/1280)
#
# Example:
#   python yolo11n_gate_counter.py highway.mp4
#
# 依存ライブラリ:
#   pip install -U ultralytics opencv-python numpy
#   (PyTorchはultralyticsが自動導入。CUDA環境ならGPU対応)
#
# 出力:
#   gate_counts.csv (時刻/ID/クラス/x/y/crossed)

import cv2, time, csv, sys, os
from collections import defaultdict
from ultralytics import YOLO

# ========= 設定(必要に応じて変更) =========
SOURCE = "input.mp4"   # カメラなら 0 / ファイルパス / RTSP など
CLASSES = [2,5,7]   # COCO:人=0, 自転車=1, 車=2, バイク=3, バス=5, トラック=7
MODEL = "yolo11n.pt"        # 初回実行で自動DL
USE_GPU = True             # RTXがあれば True(Torch+CUDA必須)
IMG_SIZE = 960              # CPU時は 960→832→640 で調整
CONF = 0.25                 # 昼:0.25 / 夜:0.15 まで下げて拾い増し
IOU = 0.45                  # NMS閾値
TTL_SEC = 2.0               # 見失ってからID破棄までの秒数
WRITE_CSV = True
CSV_PATH = "gate_counts.csv"
# ==========================================

def put(img, s, org, scale=0.7, color=(220,220,220), th=1):
    cv2.putText(img, s, org, cv2.FONT_HERSHEY_SIMPLEX, scale, (0,0,0), th+2, cv2.LINE_AA)
    cv2.putText(img, s, org, cv2.FONT_HERSHEY_SIMPLEX, scale, color, th, cv2.LINE_AA)

def main():
    global IMG_SIZE
    model = YOLO(MODEL)
    device = 0 if USE_GPU else "cpu"

    cap = cv2.VideoCapture(SOURCE if isinstance(SOURCE, (str,int)) else 0)
    if not cap.isOpened():
        print(f"[ERROR] ソースを開けませんでした: {SOURCE}", file=sys.stderr); sys.exit(1)

    ok, frame = cap.read()
    if not ok:
        print("[ERROR] 先頭フレームが読み取れませんでした。", file=sys.stderr); sys.exit(1)

    h, w = frame.shape[:2]
    # 左右 10% 内側に寄せた幅、縦方向は高さの45〜55%(中央バンド)
    #gate = [int(w*0.1), int(h*0.45), int(w*0.9), int(h*0.55)]  # x1,y1,x2,y2
    # 「横いっぱいの水平ゲート」が初期状態
    gate = [0, int(h*0.4), w, int(h*0.6)]

    # ID毎の状態
    counted = set()                # 既にカウントしたID
    last_seen = defaultdict(lambda: 0.0)  # 最終検出時刻
    inside_prev = defaultdict(lambda: False)  # 前フレームでゲート内だったか
    total = 0

    # CSV
    csvw = None
    if WRITE_CSV:
        f = open(CSV_PATH, "w", newline="", encoding="utf-8")
        csvw = csv.writer(f)
        csvw.writerow(["ts","id","cls","x","y","crossed"])

    t_prev = time.time(); fps = 0.0
    conf = CONF

    help_lines = [
        "[Q] Quit   [W/A/S/D] Move gate   [H/L] Thin/Thick   [R] Reset",
        "[N] Night mode (lower conf)   [G] GPU toggle   [Z] Size toggle",
        f"Classes: {CLASSES} (COCO)"
    ]


    while True:
        ok, frame = cap.read()
        if not ok: break

        # 推論(追跡)
        results = model.track(
            source=frame,
            imgsz=IMG_SIZE,
            device=device,
            conf=conf,
            iou=IOU,
            classes=CLASSES,
            tracker="bytetrack.yaml",
            persist=True,
            verbose=False,
            stream=False
        )

        now = time.time()
        dt = now - t_prev
        fps = 0.9*fps + 0.1*(1.0/max(dt,1e-6))
        t_prev = now

        # ゲート描画
        x1,y1,x2,y2 = gate
        cv2.rectangle(frame, (x1,y1), (x2,y2), (0,0,255), 2)
        put(frame, f"GATE y:{y1}-{y2}", (10, max(20,y1-8)), 0.7, (70,200,255))

        # 検出結果処理
        dets = []
        try:
            r = results[0]
            if r.boxes is not None and r.boxes.id is not None:
                for b in r.boxes:
                    xyxy = b.xyxy[0].tolist()    # [x1,y1,x2,y2]
                    tid  = int(b.id.item())      # Track ID
                    cls  = int(b.cls.item())
                    confb= float(b.conf.item())
                    dets.append((tid, cls, confb, xyxy))
        except Exception as e:
            # まれに追跡が返らないフレームがある
            dets = []

        # IDごとにゲート通過判定
        for (tid, cls, confb, (bx1,by1,bx2,by2)) in dets:
            cx = int((bx1+bx2)/2); cy = int((by1+by2)/2)
            last_seen[tid] = now

            inside = (y1 <= cy <= y2) and (x1 <= cx <= x2)
            was_inside = inside_prev[tid]

            # 外→内→外 を 1カウントとみなすシンプル判定
            crossed = False
            if was_inside and not inside and tid not in counted:
                crossed = True
                counted.add(tid)
                total += 1

            inside_prev[tid] = inside

            # 可視化
            color = (0,255,0) if not tid in counted else (128,128,128)
            cv2.rectangle(frame, (int(bx1),int(by1)), (int(bx2),int(by2)), color, 2)
            put(frame, f"ID:{tid} C{cls} {confb:.2f}", (int(bx1), max(15,int(by1)-6)), 0.55, (200,255,200))
            cv2.circle(frame, (cx,cy), 3, (255,255,255), -1)

            if csvw:
                csvw.writerow([f"{now:.3f}", tid, cls, cx, cy, int(crossed)])

        # TTLで古いIDを破棄
        to_del = [tid for tid,t in last_seen.items() if now - t > TTL_SEC]
        for tid in to_del:
            last_seen.pop(tid, None)
            inside_prev.pop(tid, None)
            # counted は保持(重複カウント防止)

        # HUD
        put(frame, f"TOTAL: {total}", (10, 24), 0.9, (50,255,50), 2)
        put(frame, f"FPS: {fps:5.1f}  size:{IMG_SIZE}  conf:{conf:.2f}  device:{'cuda' if device==0 else 'cpu'}",
            (10, 50), 0.7, (200,200,255))

        for i, line in enumerate(help_lines):
            put(frame, line, (10, h-10 - 20*(len(help_lines)-1-i)), 0.55, (220,220,220))

        cv2.imshow("YOLO11n Gate Counter", frame)
        k = cv2.waitKey(1) & 0xFF

        if k in (ord('q'), 27):
            break
        elif k == ord('n'):   # 夜モード:confを下げる/戻す
            conf = 0.15 if abs(conf-0.25)<1e-6 else 0.25
        elif k == ord('g'):   # GPUトグル(次フレームから反映)
            device = 0 if device=="cpu" else "cpu"
        elif k == ord('z'):   # 画像サイズトグル
            IMG_SEQ = [640, 832, 960, 1280]
            try:
                idx = IMG_SEQ.index(IMG_SIZE)
                IMG_SIZE = IMG_SEQ[(idx+1)%len(IMG_SEQ)]
            except ValueError:
                IMG_SIZE = 960
        elif k == ord('r'):   # カウンタ/状態リセット
            total = 0; counted.clear(); inside_prev.clear(); last_seen.clear()
        elif k == ord('w'):  # ↑
            gate[1] = max(0, gate[1]-5); gate[3] = max(gate[1]+10, gate[3]-5)
        elif k == ord('s'):  # ↓
            gate[3] = min(h-1, gate[3]+5); gate[1] = min(gate[3]-10, gate[1]+5)
        elif k == ord('a'):  # ←
            gate[0] = max(0, gate[0]-5); gate[2] = max(gate[0]+10, gate[2]-5)
        elif k == ord('d'):  # →
            gate[2] = min(w-1, gate[2]+5); gate[0] = min(gate[2]-10, gate[0]+5)
        elif k == ord('h'):  # 厚み薄く
            if gate[3]-gate[1] > 12: gate[3] -= 3
        elif k == ord('l'):  # 厚み厚く
            if gate[3] < h-1: gate[3] += 3

    if csvw: csvw.__self__.close()  # file close
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    if len(sys.argv) >= 2:
        SOURCE = sys.argv[1]
    main()

まとめと今後

ここまで、YOLO11nによる昼夜の交通量調査を試してきました。


昼間の成果

  • CPUでも 9fps 程度で安定稼働、CUDAなら 19fps と余裕。
  • カウント数は同一(67台) で、精度はCPUでも十分。
  • GPUがなくても実用になる」という意外性は大きなポイント。

夜間の壁

  • YOLO11nでは薄暮〜夜間はほとんど検出できず
  • confやimgszをいじっても改善せず、学習データ不足の限界が浮き彫りに。
  • 力業アプローチなら夜間もカウント可能だが、長車両は分割カウントされやすい。

カメラ設置の重要性

  • 真上や歩道橋からのアングルが理想
  • 斜め視点や遮蔽物があると精度は急落。
  • 設置位置や照明条件を工夫するだけで、同じコードでも結果は大きく変わる

用途の広がり

YOLO11nは「CLASSESを切り替えるだけ」で用途が変わります

  • 車・バス・トラック → 交通量調査
  • person → 学園祭やイベント来場者数カウント
  • dog/cat → 公園や動物施設での動物観察
  • その他COCOの多様なクラス → アイデア次第でいくらでも応用可能

「人や車を数える」から「犬を数える」まで、同じコードで対応できる柔軟さは、遊びから実務まで幅広くカバーします。


今後の展望

  • RTSPカメラを使った実地検証に進みたい。
  • 特に夜間調査はニーズがあるが、既製のサービスであまり強調されていない領域。
  • 昼はYOLO11n、夜は力業──このハイブリッドが当面の解
夜間の交通量調査は難しい

最後に。夜間計測については、相応の精度が出せるようになっていますので、お困りの方はご相談ください。開発のお手伝いができます。