YOLOv12による物体検出・ByteTrackによる追跡とTTAの機能付き(COCO 80クラス)(ソースコードと説明と利用ガイド)
【概要】YOLOv12による物体検出システム。動画やウェブカメラから人・車・動物など80クラスの物体をリアルタイム検出する。CLAHE前処理で暗所でも安定動作し、TTAとByteTrackで精度と追跡性能を向上。5種類のモデルサイズから選択可能。日本語表示対応、検出結果の自動保存機能を備える。
ツール利用ガイド
1. このプログラムの利用シーン
動画やカメラ映像から人物、車両、動物などの物体を自動検出し、追跡するプログラムである。監視カメラ映像の分析、交通流量調査、店舗内の人数カウント、自動運転システムの開発支援などに利用できる。
2. 主な機能
- リアルタイム物体検出: COCO 80クラス(人、車、自転車、動物、家具など)の物体を検出する。
- 物体追跡: ByteTrackにより、フレーム間で同一物体にID番号を割り当て、移動を追跡する。
- 検出精度向上: CLAHE処理により暗い映像でも物体を検出しやすくする。TTA(Test Time Augmentation)により検出精度を向上させる。
- モデルサイズ選択: nano(軽量・高速)からextra large(高精度)まで5段階から選択できる。
- 結果保存: 検出結果をテキストファイル(result.txt)に保存する。
3. 基本的な使い方
- プログラム起動: Pythonでプログラムを実行する。
- モデル選択: n/s/m/l/xのいずれかを入力し、使用するモデルサイズを選択する。
- 入力ソース選択: 0(動画ファイル)、1(カメラ)、2(サンプル動画)のいずれかを入力する。
- 処理開始: 画面に検出結果が表示される。検出された物体は色付きの枠で囲まれ、クラス名と信頼度が表示される。
- 終了: qキーを押すとプログラムが終了し、result.txtが保存される。
4. 便利な機能
- 物体追跡ID表示: ByteTrack有効時、各物体に固有のID番号が表示され、フレーム間での追跡が可能である。
- GPU自動検出: CUDA対応GPUが搭載されている場合、自動的にGPUで処理を実行する。
- 日本語表示: 検出された物体のクラス名が日本語で表示される(画面上のみ)。
- 詳細ログ: result.txtに各フレームの検出結果、使用モデル、処理フレーム数、検出されたクラスの統計が記録される。
事前準備
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。Python 3.12 のインストール
インストール済みの場合は実行不要。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul【関連する外部ページ】
Python の公式ページ: https://www.python.org/
AI エディタ Windsurf のインストール
Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
winget install --scope machine Codeium.Windsurf -e --silent【関連する外部ページ】
Windsurf の公式ページ: https://windsurf.com/
必要なパッケージのインストール
管理者権限でコマンドプロンプトを起動し、以下のコマンドを実行する:
pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install ultralytics opencv-python numpy pillow boxmot
YOLOv12物体検出プログラム・ByteTrackによる追跡とTTAの機能付き(COCO 80クラス)
概要
このプログラムは、YOLOv12を用いた物体検出プログラムである。動画またはカメラ映像から80クラスのオブジェクトを検出し、バウンディングボックスで表示する。CLAHE(Contrast Limited Adaptive Histogram Equalization)による前処理、TTA(Test Time Augmentation)、ByteTrackによる追跡機能を搭載する[1][2]。
主要技術
YOLOv12(You Only Look Once version 12)
2025年に発表されたアテンション機構に基づく物体検出アルゴリズムである[1]。従来のYOLOモデルがCNNベースであったのに対し、YOLOv12はArea Attention機構を採用し、大きな受容野を維持しながら計算コストを削減する。YOLOv12-NはCOCOデータセットで40.6% mAPを達成し、YOLOv11-Nより1.2%、YOLOv10-Nより2.1%向上している。
ByteTrack
2022年のECCV(European Conference on Computer Vision)で発表された多物体追跡アルゴリズムである[2]。従来の手法が高信頼度の検出のみを追跡するのに対し、ByteTrackは低信頼度の検出も活用し、オクルージョン(遮蔽)による追跡失敗を軽減する。カルマンフィルタによる動き予測とハンガリアンアルゴリズムによるデータアソシエーションを組み合わせる。
技術的特徴
- Area Attention機構
YOLOv12のArea Attention(A2)は、特徴マップを複数の領域に分割し、各領域内で自己注意機構を適用する[1]。標準的な自己注意機構と比較して計算量を削減しながら、大きな受容野を維持する。
- CLAHE前処理
Contrast Limited Adaptive Histogram Equalization(CLAHE)は、画像を小領域(タイル)に分割し、各タイルに対して適応的にヒストグラム均等化を適用する手法である[3]。本プログラムでは、YUV色空間に変換後、輝度チャンネル(Yチャンネル)のみにCLAHEを適用することで、低照度環境下での検出を改善する。
- Test Time Augmentation(TTA)
推論時に元画像と水平反転画像の両方で推論を実行し、結果を統合する手法である。Non-Maximum Suppression(NMS)により重複検出を除去し、信頼度の高い検出結果を得る。
- ByteTrackによる追跡
検出された物体に対してカルマンフィルタで動きを予測し、フレーム間で同一物体にIDを割り当てる[2]。高信頼度検出と低信頼度検出の2段階マッチングにより、オクルージョン発生時も追跡を継続する。
実装の特色
- 5種類のモデルサイズ選択(nano/small/medium/large/extra large)
- GPU/CPU自動選択機能
- TTA有効/無効の切り替え機能
- ByteTrack有効/無効の切り替え機能
- 日本語による物体クラス名表示(PillowライブラリによるTrueTypeフォント描画)
- 信頼度閾値の調整機能(デフォルト0.5)
参考文献
[1] Tian, Y., Ye, Q., & Doermann, D. (2025). YOLOv12: Attention-Centric Real-Time Object Detectors. arXiv preprint arXiv:2502.12524. https://arxiv.org/abs/2502.12524
[2] Zhang, Y., Sun, P., Jiang, Y., Yu, D., Weng, F., Yuan, Z., Luo, P., Liu, W., & Wang, X. (2022). ByteTrack: Multi-Object Tracking by Associating Every Detection Box. In Computer Vision – ECCV 2022: 17th European Conference, Tel Aviv, Israel, October 23–27, 2022, Proceedings, Part XXII (pp. 1-21). Springer. https://arxiv.org/abs/2110.06864
[3] Zuiderveld, K. (1994). Contrast Limited Adaptive Histogram Equalization. In P. Heckbert (Ed.), Graphics Gems IV (pp. 474-485). Academic Press.
ソースコード
# プログラム名: YOLOv12物体検出プログラム・ByteTrackによる追跡とTTAの機能付き(COCO 80クラス)
# 特徴技術名: YOLOv12 (You Only Look Once version 12)
# 出典: Tian, Yunjie and Ye, Qixiang and Doermann, David (2025). YOLOv12: Attention-Centric Real-Time Object Detectors. arXiv preprint arXiv:2502.12524. GitHub: https://github.com/sunsmarterjie/yolov12
# 特徴機能: Area Attention機構による物体検出、TTA、ByteTrack追跡
# 学習済みモデル: yolo12n.pt (COCOデータセットで事前学習済み、80クラス対応、nanoバリアント、手動配置が必要)で、クラス0が人物(person)を検出可能
# モデルサイズ選択可能(デフォルト:n):
# n (nano): yolo12n.pt - 軽量
# s (small): yolo12s.pt - 軽量
# m (medium): yolo12m.pt - バランス型
# l (large): yolo12l.pt - 精度重視
# x (extra large): yolo12x.pt - 最大精度
# 方式設計:
# - 関連利用技術:
# - PyTorch: 深層学習フレームワーク、CUDA対応によるGPU加速
# - OpenCV: 画像処理、カメラ制御、描画処理、動画入出力管理
# - ByteTrack: カルマンフィルタとハンガリアンアルゴリズムによる物体追跡(boxmotパッケージ版)
# - TTA (Test Time Augmentation): 複数の画像変換で推論し結果を統合
# - 入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.aviを使用)、出力: OpenCV画面でリアルタイム表示(検出したオブジェクトをバウンディングボックスで表示)、フレーム毎のprint()による処理結果表示、プログラム終了時にresult.txtファイルに保存
# - 処理手順: 1.フレーム取得、2.前処理(CLAHE)、3.TTA適用、4.YOLOv12推論実行、5.信頼度閾値による選別、6.ByteTrack追跡、7.バウンディングボックス描画
# - 前処理、後処理: 前処理:CLAHE適用。YOLOv12内部で自動実行(640x640リサイズ、正規化)。後処理:信頼度による閾値フィルタリング、ByteTrack追跡による検出結果の安定化とID管理
# - 追加処理: CUDA/CPU自動検出機能により、GPU搭載環境では自動的に処理実行。検出結果の信頼度降順ソートにより重要な検出を優先表示。TTA - 水平反転による推論結果の統合
# - 調整を必要とする設定値: CONF_THRESH(オブジェクト検出信頼度閾値、デフォルト0.5)- 値を上げると誤検出が減少するが検出漏れが増加、TTA_ENABLED(TTAの有効/無効、デフォルトTrue)
# 将来方策: CONF_THRESHの動的調整機能。フレーム毎の検出数を監視し、検出数が閾値を超えた場合は信頼度を上げ、検出数が少ない場合は下げる適応的制御の実装
# その他の重要事項: Windows環境専用設計、CUDA対応GPU推奨(自動検出・CPUフォールバック機能付き)、初回実行時は学習済みモデルの手動配置が必要
# 特徴技術および学習済みモデルの利用制限: AGPL-3.0ライセンス。商用利用の場合はUltralyticsのエンタープライズライセンスが必要。学術研究目的での利用は可能。詳細はhttps://github.com/sunsmarterjie/yolov12のライセンス情報を確認すること。必ず利用者自身で利用制限を確認すること。
# 前準備:
# - pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# - pip install ultralytics opencv-python numpy pillow boxmot
import cv2
import tkinter as tk
from tkinter import filedialog
import torch
import torchvision
import numpy as np
import time
import urllib.request
from ultralytics import YOLO
from datetime import datetime
import sys
import io
from PIL import Image, ImageDraw, ImageFont
from boxmot import ByteTrack
import threading
# Windows文字エンコーディング設定
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True)
# GPU/CPU自動選択
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'デバイス: {str(device)}')
# GPU使用時の最適化
if device.type == 'cuda':
torch.backends.cudnn.benchmark = True
# ===== 設定・定数管理 =====
# モデル情報
MODEL_INFO = {
'n': {'name': 'Nano', 'desc': '軽量', 'params': '3.2M'},
's': {'name': 'Small', 'desc': '軽量', 'params': '11.2M'},
'm': {'name': 'Medium', 'desc': 'バランス型', 'params': '25.9M'},
'l': {'name': 'Large', 'desc': '精度重視', 'params': '43.7M'},
'x': {'name': 'Extra Large', 'desc': '最大精度', 'params': '68.2M'}
}
# 日本語クラス名マッピング
CLASS_NAMES_JP = {
'person': '人', 'bicycle': '自転車', 'car': '車', 'motorcycle': 'バイク',
'airplane': '飛行機', 'bus': 'バス', 'train': '電車', 'truck': 'トラック',
'boat': 'ボート', 'traffic light': '信号機', 'fire hydrant': '消火栓',
'stop sign': '停止標識', 'parking meter': 'パーキングメーター', 'bench': 'ベンチ',
'bird': '鳥', 'cat': '猫', 'dog': '犬', 'horse': '馬', 'sheep': '羊',
'cow': '牛', 'elephant': '象', 'bear': '熊', 'zebra': 'シマウマ', 'giraffe': 'キリン',
'backpack': 'リュック', 'umbrella': '傘', 'handbag': 'ハンドバッグ', 'tie': 'ネクタイ',
'suitcase': 'スーツケース', 'frisbee': 'フリスビー', 'skis': 'スキー板',
'snowboard': 'スノーボード', 'sports ball': 'ボール', 'kite': '凧',
'baseball bat': 'バット', 'baseball glove': 'グローブ', 'skateboard': 'スケートボード',
'surfboard': 'サーフボード', 'tennis racket': 'テニスラケット', 'bottle': 'ボトル',
'wine glass': 'ワイングラス', 'cup': 'カップ', 'fork': 'フォーク', 'knife': 'ナイフ',
'spoon': 'スプーン', 'bowl': 'ボウル', 'banana': 'バナナ', 'apple': 'リンゴ',
'sandwich': 'サンドイッチ', 'orange': 'オレンジ', 'broccoli': 'ブロッコリー',
'carrot': 'ニンジン', 'hot dog': 'ホットドッグ', 'pizza': 'ピザ', 'donut': 'ドーナツ',
'cake': 'ケーキ', 'chair': '椅子', 'couch': 'ソファ', 'potted plant': '鉢植え',
'bed': 'ベッド', 'dining table': 'テーブル', 'toilet': 'トイレ', 'tv': 'テレビ',
'laptop': 'ノートPC', 'mouse': 'マウス', 'remote': 'リモコン', 'keyboard': 'キーボード',
'cell phone': '携帯電話', 'microwave': '電子レンジ', 'oven': 'オーブン',
'toaster': 'トースター', 'sink': 'シンク', 'refrigerator': '冷蔵庫',
'book': '本', 'clock': '時計', 'vase': '花瓶', 'scissors': 'ハサミ',
'teddy bear': 'ぬいぐるみ', 'hair drier': 'ドライヤー', 'toothbrush': '歯ブラシ'
}
# COCO 80クラス名
COCO_CLASSES = list(CLASS_NAMES_JP.keys())
# クラスごとの色生成(HSVからBGRに変換)
def generate_colors(num_classes):
colors = []
for i in range(num_classes):
hue = int(180.0 * i / num_classes)
hsv = np.uint8([[[hue, 255, 255]]])
bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)[0][0]
colors.append((int(bgr[0]), int(bgr[1]), int(bgr[2])))
return colors
# 80クラス分の色を生成
CLASS_COLORS = generate_colors(80)
# BGR→RGB色変換のヘルパー関数
def bgr_to_rgb(color_bgr):
"""BGRカラーをRGBカラーに変換"""
return (color_bgr[2], color_bgr[1], color_bgr[0])
# 日本語フォント設定
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE_MAIN = 16
font_main = ImageFont.truetype(FONT_PATH, FONT_SIZE_MAIN)
SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
SAMPLE_FILE = 'vtest.avi'
# 調整可能な設定値
CONF_THRESH = 0.5 # 検出パラメータ(調整可能)
IOU_THRESH = 0.45
CLAHE_CLIP_LIMIT = 2.0 # CLAHE制限値
CLAHE_TILE_SIZE = (8, 8) # CLAHEタイルサイズ
MAIN_FUNC_DESC = "YOLOv12 Object Detection (CLAHE)" # OpenCVウィンドウ名
TTA_ENABLED = True # TTA(Test Time Augmentation)の有効/無効
TTA_CONF_BOOST = 0.03 # TTA使用時の信頼度ブースト値
NMS_THRESHOLD = 0.6 # TTA用のNMS閾値(独立管理)
USE_TRACKER = True # トラッカーの使用有無
# CLAHEオブジェクトをグローバルスコープで一度だけ定義
clahe = cv2.createCLAHE(clipLimit=CLAHE_CLIP_LIMIT, tileGridSize=CLAHE_TILE_SIZE)
# ByteTrackトラッカーを初期化
tracker = ByteTrack() if USE_TRACKER else None
# グローバル変数
frame_count = 0
results_log = []
class_counts = {}
model = None
class ThreadedVideoCapture:
"""スレッド化されたVideoCapture(常に最新フレームを取得)"""
def __init__(self, src, is_camera=False):
if is_camera:
self.cap = cv2.VideoCapture(src, cv2.CAP_DSHOW)
fourcc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')
self.cap.set(cv2.CAP_PROP_FOURCC, fourcc)
self.cap.set(cv2.CAP_PROP_FPS, 60)
else:
self.cap = cv2.VideoCapture(src)
self.grabbed, self.frame = self.cap.read()
self.stopped = False
self.lock = threading.Lock()
self.thread = threading.Thread(target=self.update, args=())
self.thread.daemon = True
self.thread.start()
def update(self):
"""バックグラウンドでフレームを取得し続ける"""
while not self.stopped:
grabbed, frame = self.cap.read()
with self.lock:
self.grabbed = grabbed
if grabbed:
self.frame = frame
def read(self):
"""最新フレームを返す"""
with self.lock:
return self.grabbed, self.frame.copy() if self.grabbed else None
def isOpened(self):
return self.cap.isOpened()
def get(self, prop):
return self.cap.get(prop)
def release(self):
self.stopped = True
self.thread.join()
self.cap.release()
def display_program_header():
# プログラム概要表示
print('=== YOLOv12オブジェクト検出プログラム (CLAHE) ===')
print('概要: リアルタイムでオブジェクトを検出し、バウンディングボックスで表示します')
print('機能: YOLOv12によるオブジェクト検出(COCOデータセット80クラス)')
print('技術: CLAHE (コントラスト強化) + ByteTrack による追跡 + TTA (Test Time Augmentation)')
print('操作: qキーで終了')
print('出力: 各フレームでの処理結果表示、終了時にresult.txt保存')
print()
# ===== 共通処理関数 =====
def draw_texts_with_pillow(bgr_frame, texts):
"""テキスト描画, texts: list of dict with keys {text, org, color, font_type}"""
img_pil = Image.fromarray(cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
for item in texts:
text = item['text']
x, y = item['org']
color = item['color'] # RGB
draw.text((x, y), text, font=font_main, fill=color)
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
# ===== TTA機能 =====
def normal_inference(frame, model, conf):
"""通常の推論処理"""
results = model(frame, device=device, verbose=False)
objects = []
if results[0].boxes is not None:
boxes = results[0].boxes.xywh.cpu().numpy()
confs = results[0].boxes.conf.cpu().numpy()
classes = results[0].boxes.cls.cpu().numpy()
# 信頼度閾値でフィルタリング
valid_indices = confs > conf
if np.any(valid_indices):
boxes = boxes[valid_indices]
confs = confs[valid_indices]
classes = classes[valid_indices]
# xywh形式からxyxy形式に変換
boxes_xyxy = boxes.copy()
boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2
boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2
boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2
boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2
# 信頼度で降順ソート
sorted_indices = np.argsort(confs)[::-1]
boxes_xyxy = boxes_xyxy[sorted_indices]
confs = confs[sorted_indices]
classes = classes[sorted_indices]
for i, (box, conf_val, cls) in enumerate(zip(boxes_xyxy, confs, classes)):
x1, y1, x2, y2 = map(int, box)
class_id = int(cls)
object_data = {
'box': (x1, y1, x2, y2),
'detection_conf': conf_val,
'class_id': class_id,
'class_name': COCO_CLASSES[class_id]
}
objects.append(object_data)
return objects
def apply_tta_inference(frame, model, conf):
"""Test Time Augmentation (TTA)を適用した推論"""
frame_width = frame.shape[1]
# 水平反転画像を作成
flipped_frame = cv2.flip(frame, 1)
# バッチ推論
results = model([frame, flipped_frame], verbose=False)
# 元画像の結果を取得
all_boxes = []
all_confs = []
all_classes = []
if results[0].boxes is not None and len(results[0].boxes) > 0:
boxes_orig = results[0].boxes.xyxy
confs_orig = results[0].boxes.conf
classes_orig = results[0].boxes.cls
all_boxes.append(boxes_orig)
all_confs.append(confs_orig)
all_classes.append(classes_orig)
# 反転画像の結果を取得し、座標を元に戻す
if len(results) > 1 and results[1].boxes is not None and len(results[1].boxes) > 0:
boxes_flipped = results[1].boxes.xyxy.clone()
confs_flipped = results[1].boxes.conf
classes_flipped = results[1].boxes.cls
# 水平反転画像での検出結果を元の画像座標系に変換
# x1, x2 の大小関係を保つ必要がある
if boxes_flipped.shape[0] > 0:
x1_flipped = boxes_flipped[:, 0].clone()
x2_flipped = boxes_flipped[:, 2].clone()
# 元の画像座標系での新しい座標
boxes_flipped[:, 0] = frame_width - 1 - x2_flipped # 新しいx1(左端)
boxes_flipped[:, 2] = frame_width - 1 - x1_flipped # 新しいx2(右端)
all_boxes.append(boxes_flipped)
all_confs.append(confs_flipped)
all_classes.append(classes_flipped)
# 結果が空の場合は空リストを返す
if len(all_boxes) == 0:
return []
# 全ての結果を結合
all_boxes = torch.cat(all_boxes, dim=0)
all_confs = torch.cat(all_confs, dim=0)
all_classes = torch.cat(all_classes, dim=0)
# 信頼度閾値でフィルタリング
valid_indices = all_confs > conf
if valid_indices.sum() > 0:
all_boxes = all_boxes[valid_indices]
all_confs = all_confs[valid_indices]
all_classes = all_classes[valid_indices]
# torchvisionのNMSを使用
nms_indices = torchvision.ops.nms(all_boxes, all_confs, iou_threshold=NMS_THRESHOLD)
final_boxes = all_boxes[nms_indices].cpu().numpy()
final_confs = all_confs[nms_indices].cpu().numpy()
final_classes = all_classes[nms_indices].cpu().numpy()
# 信頼度で降順ソート
sorted_indices = np.argsort(final_confs)[::-1]
final_boxes = final_boxes[sorted_indices]
final_confs = final_confs[sorted_indices]
final_classes = final_classes[sorted_indices]
# 結果をリスト形式に変換
objects = []
for i in range(len(final_confs)):
x1, y1, x2, y2 = map(int, final_boxes[i])
class_id = int(final_classes[i])
# TTAで検出された場合、信頼度をブースト
conf_boost = TTA_CONF_BOOST if TTA_ENABLED else 0
object_data = {
'box': (x1, y1, x2, y2),
'detection_conf': min(1.0, final_confs[i] + conf_boost),
'class_id': class_id,
'class_name': COCO_CLASSES[class_id]
}
objects.append(object_data)
return objects
return []
def apply_tta_if_enabled(frame, model, conf):
"""TTA機能を条件付きで適用"""
if not TTA_ENABLED:
return normal_inference(frame, model, conf)
return apply_tta_inference(frame, model, conf)
# ===== トラッキング機能 =====
def apply_bytetrack(objects, frame):
"""ByteTrackerを使用したトラッキング処理"""
global tracker
# 検出結果が0件でもトラッカーの状態更新と予測結果取得を行う
if len(objects) > 0:
dets_array = np.array([[obj['box'][0], obj['box'][1], obj['box'][2], obj['box'][3],
obj['detection_conf'], obj['class_id']]
for obj in objects])
else:
# 検出がない場合は空の配列を渡す
dets_array = np.empty((0, 6))
# 常にトラッカーを更新し、現在のフレームでの追跡結果(または予測結果)を取得する
tracks = tracker.update(dets_array, frame)
tracked_objects = []
# tracker.updateが返す結果を処理する(検出0件でも予測結果が返る可能性がある)
if len(tracks) > 0:
for track in tracks:
if len(track) >= 7:
x1, y1, x2, y2, track_id, conf, cls = track[:7]
name = model.names[int(cls)]
tracked_objects.append({
'box': (int(x1), int(y1), int(x2), int(y2)),
'track_id': int(track_id),
'detection_conf': float(conf),
'class_id': int(cls),
'name': name
})
return tracked_objects
def apply_tracking_if_enabled(objects, frame):
"""トラッキング機能を条件付きで適用"""
if not USE_TRACKER:
return objects
return apply_bytetrack(objects, frame)
# ===== 物体検出タスク固有の処理 =====
def draw_detection_results(frame, objects):
"""物体検出の描画処理"""
# バウンディングボックスを描画(OpenCVで)
for i, obj in enumerate(objects):
x1, y1, x2, y2 = obj['box']
class_id = obj['class_id']
color = CLASS_COLORS[class_id]
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
# 構造化されたテキスト描画を実行
texts_to_draw = []
for obj in objects:
x1, y1, x2, y2 = obj['box']
class_id = obj['class_id']
class_name = obj.get('name') if 'name' in obj else obj.get('class_name')
jp_name = CLASS_NAMES_JP.get(class_name, class_name)
color = CLASS_COLORS[class_id]
track_id = obj.get('track_id', 0) if USE_TRACKER else 0
if USE_TRACKER and track_id > 0:
label = f"ID:{track_id} {jp_name}: {obj['detection_conf']:.1%}"
else:
label = f"{jp_name}: {obj['detection_conf']:.1%}"
texts_to_draw.append({
'text': label,
'org': (x1, y1-10),
'color': bgr_to_rgb(color),
'font_type': 'main'
})
frame = draw_texts_with_pillow(frame, texts_to_draw)
tta_status = "TTA:ON" if TTA_ENABLED else "TTA:OFF"
tracker_status = "ByteTrack:ON" if USE_TRACKER else "ByteTrack:OFF"
info1 = f'YOLOv12 ({MODEL_INFO[MODEL_SIZE]["name"]}) | Frame: {frame_count} | Objects: {len(objects)} | {tta_status} | {tracker_status}'
info2 = 'Press: q=Quit'
cv2.putText(frame, info1, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
cv2.putText(frame, info2, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 1)
return frame
def format_detection_output(objects):
"""物体検出の出力フォーマット"""
if objects:
results = []
for obj in objects:
x1, y1, x2, y2 = obj['box']
class_name = obj.get('name') if 'name' in obj else obj.get('class_name')
conf = obj['detection_conf']
if USE_TRACKER and 'track_id' in obj:
results.append(f'class={class_name},id={obj["track_id"]},conf={conf:.3f},box=[{x1},{y1},{x2},{y2}]')
else:
results.append(f'class={class_name},conf={conf:.3f},box=[{x1},{y1},{x2},{y2}]')
result = f'count={len(objects)}; ' + ' | '.join(results)
else:
result = 'count=0'
return result
def detect_objects(frame):
"""共通の検出処理(CLAHE、推論、検出を実行)"""
global model
H, W, _ = frame.shape
# AIモデルの入力用にCLAHEを適用(YUV色空間で輝度チャンネルのみ処理)
yuv_img = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV)
yuv_img[:,:,0] = clahe.apply(yuv_img[:,:,0])
clahe_frame = cv2.cvtColor(yuv_img, cv2.COLOR_YUV2BGR)
# TTA適用
objects = apply_tta_if_enabled(clahe_frame, model, CONF_THRESH)
return objects
def process_video_frame(frame):
"""動画用ラッパー"""
# 共通の検出処理
objects = detect_objects(frame)
# トラッキングを条件付きで適用
tracked_objects = apply_tracking_if_enabled(objects, frame)
# クラスごとの検出数を更新
global class_counts
for obj in tracked_objects:
name = obj.get('name') if 'name' in obj else obj.get('class_name')
if name not in class_counts:
class_counts[name] = 0
class_counts[name] += 1
# 物体検出固有の描画処理
frame = draw_detection_results(frame, tracked_objects)
# 物体検出固有の出力フォーマット
result = format_detection_output(tracked_objects)
return frame, result
def video_frame_processing(frame, timestamp_ms, is_camera):
"""フレーム処理メイン関数 (CLAHE対応)"""
global frame_count
current_time = time.time()
frame_count += 1
processed_frame, result = process_video_frame(frame)
return processed_frame, result, current_time
# プログラムヘッダー表示
display_program_header()
# ===== モデル選択 =====
while True:
print('使用するYOLOv12モデルを選択してください:')
for key, val in MODEL_INFO.items():
print(f" {key}: {val['name']} ({val['params']} params)")
choice = input("選択 (n/s/m/l/x): ").lower()
if choice in MODEL_INFO:
MODEL_SIZE = choice
break
else:
print("\n無効な選択です。もう一度入力してください。\n")
MODEL_NAME = f'yolo12{MODEL_SIZE}.pt'
print(f"\nモデル '{MODEL_INFO[MODEL_SIZE]['name']}' を使用します。\n")
# システム初期化
print('システム初期化中...')
if device.type == 'cuda':
print(f'GPU検出: {torch.cuda.get_device_name(0)}')
print(f'CUDA バージョン: {torch.version.cuda}')
# YOLOv12モデル初期化
try:
print(f'YOLOv12 ({MODEL_INFO[MODEL_SIZE]["name"]}) モデルを初期化中...')
model = YOLO(MODEL_NAME)
print(f'YOLOv12 ({MODEL_INFO[MODEL_SIZE]["name"]}) モデルの初期化が完了しました')
print(f'モデルサイズ: {MODEL_SIZE} ({MODEL_INFO[MODEL_SIZE]["name"]}={MODEL_INFO[MODEL_SIZE]["desc"]})')
except Exception as e:
print(f'YOLOv12 ({MODEL_INFO[MODEL_SIZE]["name"]}) モデルの初期化に失敗しました')
print(f'エラー: {e}')
print(f"ヒント: '{MODEL_NAME}' ファイルがプログラムと同じディレクトリに配置されているか確認してください。")
exit()
print('初期化完了')
print()
# TTA設定の表示
if TTA_ENABLED:
print("\nTest Time Augmentation (TTA): 有効")
print(" - 水平反転による推論結果の統合")
print(f" - 信頼度ブースト値: {TTA_CONF_BOOST}")
print(f" - NMS閾値: {NMS_THRESHOLD}")
else:
print("\nTest Time Augmentation (TTA): 無効")
# ByteTrack設定の表示
if USE_TRACKER:
print("\nByteTrack: 有効")
print(" - カルマンフィルタによる動き予測")
print("\n入力ソースを選択してください:")
print("0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")
choice = input("選択: ")
is_camera = (choice == '1')
if choice == '0':
root = tk.Tk()
root.withdraw()
path = filedialog.askopenfilename()
if not path:
exit()
cap = cv2.VideoCapture(path)
elif choice == '1':
cap = ThreadedVideoCapture(0, is_camera=True)
else:
print('サンプル動画をダウンロード中...')
urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
cap = cv2.VideoCapture(SAMPLE_FILE)
if not cap.isOpened():
print('動画ファイル・カメラを開けませんでした')
exit()
# フレームレートの取得とタイムスタンプ増分の計算
if is_camera:
actual_fps = cap.get(cv2.CAP_PROP_FPS)
print(f'カメラのfps: {actual_fps}')
timestamp_increment = int(1000 / actual_fps) if actual_fps > 0 else 33
else:
video_fps = cap.get(cv2.CAP_PROP_FPS)
timestamp_increment = int(1000 / video_fps) if video_fps > 0 else 33
# メイン処理
print('\n=== 動画処理開始 ===')
print('操作方法:')
print(' q キー: プログラム終了')
start_time = time.time()
last_info_time = start_time
info_interval = 10.0
timestamp_ms = 0
total_processing_time = 0.0
try:
while True:
ret, frame = cap.read()
if not ret:
break
timestamp_ms += timestamp_increment
processing_start = time.time()
processed_frame, result, current_time = video_frame_processing(frame, timestamp_ms, is_camera)
processing_time = time.time() - processing_start
total_processing_time += processing_time
cv2.imshow(MAIN_FUNC_DESC, processed_frame)
if result:
if is_camera:
timestamp = datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
print(f'{timestamp}, {result}')
else:
print(f'Frame {frame_count}: {result}')
results_log.append(result)
# 情報提供(カメラモードのみ、info_interval秒ごと)
if is_camera:
elapsed = current_time - last_info_time
if elapsed >= info_interval:
total_elapsed = current_time - start_time
actual_fps = frame_count / total_elapsed if total_elapsed > 0 else 0
avg_processing_time = (total_processing_time / frame_count * 1000) if frame_count > 0 else 0
print(f'[情報] 経過時間: {total_elapsed:.1f}秒, 処理フレーム数: {frame_count}, 実測fps: {actual_fps:.1f}, 平均処理時間: {avg_processing_time:.1f}ms')
last_info_time = current_time
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
print('\n=== プログラム終了 ===')
cap.release()
cv2.destroyAllWindows()
if results_log:
with open('result.txt', 'w', encoding='utf-8') as f:
f.write('=== 結果 ===\n')
f.write(f'使用モデル: YOLOv12 {MODEL_INFO[MODEL_SIZE]["name"]} ({MODEL_NAME})\n')
f.write(f'処理フレーム数: {frame_count}\n')
f.write(f'使用デバイス: {str(device).upper()}\n')
if device.type == 'cuda':
f.write(f'GPU: {torch.cuda.get_device_name(0)}\n')
f.write(f'画像処理: CLAHE適用(YUV色空間)\n')
f.write(f'TTA (Test Time Augmentation): {"有効" if TTA_ENABLED else "無効"}\n')
if TTA_ENABLED:
f.write(f' - NMS閾値: {NMS_THRESHOLD}\n')
f.write(f' - 信頼度ブースト: {TTA_CONF_BOOST}\n')
f.write(f'ByteTrack: {"有効" if USE_TRACKER else "無効"}\n')
f.write(f'信頼度閾値: {CONF_THRESH}\n')
f.write(f'\n検出されたクラス一覧:\n')
for class_name, count in sorted(class_counts.items()):
jp_name = CLASS_NAMES_JP.get(class_name, class_name)
f.write(f' {jp_name} ({class_name}): {count}回\n')
f.write('\n')
for i, result in enumerate(results_log, 1):
f.write(f'Frame {i}: {result}\n')
print(f'\n処理結果をresult.txtに保存しました')
print(f'検出されたクラス数: {len(class_counts)}')