YOLOv10による物体検出・ByteTrackによる追跡とTTAの機能付き(COCO 80クラス)(ソースコードと説明と利用ガイド)
【概要】YOLOv10による物体検出システム。動画やウェブカメラから人・車・動物など80クラスの物体をリアルタイム検出する。CLAHE前処理で暗所でも安定動作し、TTAとByteTrackで精度と追跡性能を向上。5種類のモデルサイズから選択可能。日本語表示対応、検出結果の自動保存機能を備える。
プログラム利用ガイド
1. このプログラムの利用シーン
動画ファイルやウェブカメラの映像から、80種類の物体(人、車、動物、家具など)をリアルタイムで検出・追跡するためのプログラムである。監視カメラの映像分析、交通量調査、スポーツ映像の解析などに利用できる。
2. 主な機能
- リアルタイム物体検出: YOLOv10により、フレームごとに80クラスの物体を検出する。
- 物体追跡機能: ByteTrackにより、検出された物体にIDを付与し、フレーム間で追跡する。
- TTA機能: 水平反転画像を用いた推論により、検出精度を向上させる。
- 画像前処理: CLAHE処理により、暗い映像でも物体を検出しやすくする。
- モデル選択: 処理速度と精度のバランスに応じて6種類のモデルから選択できる。
- 結果の保存: 検出結果をresult.txtに自動保存する。
3. 基本的な使い方
- 起動とモデル選択:
プログラムを起動すると、使用するYOLOv10モデルの選択画面が表示される。n(最速)からx(最高精度)まで、用途に応じて選択する。
- 入力ソースの選択:
キーボードで 0(動画ファイル)、1(ウェブカメラ)、2(サンプル動画)のいずれかを入力する。0を選択した場合は、ファイル選択ダイアログが表示される。
- 検出の実行:
映像が再生され、検出された物体に矩形と日本語ラベルが表示される。ByteTrack有効時は、各物体にIDが付与される。
- 終了方法:
映像表示画面でキーボードの q キーを押すと、プログラムが終了し、結果がresult.txtに保存される。
4. 便利な機能
- TTA機能: TTA_ENABLEDをTrueに設定すると、検出精度が向上する(処理時間は約2倍)。
- 追跡機能: USE_TRACKERをTrueに設定すると、物体にIDが付与され、フレーム間で追跡される。
- 信頼度調整: CONF_THRESHの値(0.0~1.0)を変更することで、検出感度を調整できる。
- 統計情報: 画面上部に検出物体数、フレーム番号、検出クラス数、TTA/追跡のステータスが表示される。
- 結果ファイル: 終了時に生成される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
YOLOv10による物体検出プログラム・ByteTrackによる追跡とTTAの機能付き(COCO 80クラス)
概要
このプログラムは、YOLOv10を使用してCOCO 80クラスの物体をリアルタイムで検出する。動画ファイルまたはカメラ映像から物体を検出し、TTA(Test Time Augmentation)とByteTrackによる追跡機能を統合することで、検出精度と追跡の安定性を向上させる。
主要技術
YOLOv10
Wang et al.が2024年に開発した物体検出アルゴリズムである[1]。Non-Maximum Suppression(NMS)を不要とするエンドツーエンド設計により、後処理なしで物体検出を実現する。一貫した二重割り当て(Consistent Dual Assignments)を用いたNMSフリー学習により、推論遅延を削減する。
ByteTrack
Zhang et al.が2022年に開発した多物体追跡アルゴリズムである[2]。カルマンフィルタによる動き予測とハンガリアンアルゴリズムによるデータアソシエーションを用いて、低信頼度の検出も含めた全検出ボックスを関連付けることで、追跡精度を向上させる。
技術的特徴
- NMSフリー検出
YOLOv10は従来のYOLOシリーズが依存していたNon-Maximum Suppressionを排除し、一貫した二重割り当てを用いた学習により推論遅延を削減する。
- CLAHE前処理
Contrast Limited Adaptive Histogram Equalization[3]をYUV色空間のY成分に適用し、局所的なコントラストを強化する。これにより、様々な照明条件下での検出精度が向上する。
- TTA(Test Time Augmentation)
水平反転画像と元画像の両方で推論を実行し、torchvisionのNMSで統合する。検出結果に信頼度ブーストを適用することで、検出の信頼性を向上させる。
- ByteTrackによる追跡
カルマンフィルタで物体の動きを予測し、ハンガリアンアルゴリズムで検出結果と追跡軌跡を関連付ける。高信頼度検出と低信頼度検出の二段階マッチングにより、オクルージョンに対する堅牢性を向上させる。
実装の特色
- 6種類のモデルサイズ選択(nano、small、medium、balanced、large、extra large)
- 3つの入力ソース(動画ファイル、カメラ、サンプル動画)
- GPU/CPU自動選択(PyTorchによるデバイス管理)
- 日本語クラス名表示(Pillowライブラリによる80クラス全対応)
- トラックID管理による物体追跡の可視化
- 処理結果のテキストファイル保存(検出数、クラス別集計を含む)
- 信頼度閾値によるフィルタリング(デフォルト0.5)
- TTA機能のON/OFF切り替え
- ByteTrack追跡のON/OFF切り替え
参考文献
[1] Wang, A., Chen, H., Liu, L., Chen, K., Lin, Z., Han, J., & Ding, G. (2024). YOLOv10: Real-Time End-to-End Object Detection. arXiv:2405.14458. https://arxiv.org/abs/2405.14458
[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 Proceedings of the European Conference on Computer Vision (ECCV).
[3] Zuiderveld, K. (1994). Contrast Limited Adaptive Histogram Equalization. In Graphics Gems IV (pp. 474-485). Academic Press.
ソースコード
# プログラム名: YOLOv10による物体検出プログラム(COCO 80クラス)+ TTA + ByteTrack
# 特徴技術名: YOLOv10
# 出典: Wang, A., Chen, H., Liu, L., Chen, K., Lin, Z., Han, J., & Ding, G. (2024). YOLOv10: Real-Time End-to-End Object Detection. arXiv:2405.14458.
# 特徴機能: NMSフリー検出機能。Non-Maximum Suppressionを不要とする設計により、後処理なしで物体検出を実現。本プログラムではCOCO 80クラス全体を検出。TTA(Test Time Augmentation)とByteTrack追跡機能を追加
# 学習済みモデル: YOLOv10 COCO事前学習済みモデル(80クラス全体)
# モデルサイズ選択可能(デフォルト: n):
# n (nano): yolov10n.pt - 最小
# s (small): yolov10s.pt - 小
# m (medium): yolov10m.pt - 中
# b (balanced): yolov10b.pt - 中上
# l (large): yolov10l.pt - 大
# x (extra large): yolov10x.pt - 最大
# 方式設計:
# - 関連利用技術:
# - PyTorch: 深層学習フレームワーク、CUDA対応
# - OpenCV: 画像処理、カメラ制御、描画処理、動画入出力管理
# - ByteTrack: カルマンフィルタとハンガリアンアルゴリズムによる物体追跡(boxmotパッケージ版)
# - TTA (Test Time Augmentation): 複数の画像変換で推論し結果を統合
# - 入力と出力:
# 入力: 動画(0:動画ファイル,1:カメラ,2:サンプル動画)
# 出力: 各フレームごとに全検出一覧をprint表示し、終了時にresult.txtに保存
# - 処理手順: 1.フレーム取得、2.CLAHE前処理、3.TTA適用、4.YOLOv10推論、5.COCO 80クラス検出、6.ByteTrack追跡、7.信頼度閾値で選別、8.バウンディングボックス描画
# - 前処理/後処理: 前処理はCLAHE(YUV色空間)とYOLO内部処理(640x640リサイズ、正規化)。後処理は信頼度閾値によるフィルタリングとByteTrack追跡による安定化
# - 追加処理: デバイス自動選択(GPU/CPU)、日本語クラス名表示、TTA、ByteTrack追跡
# - 調整可能値: CONF_THRESH(検出信頼度閾値、デフォルト0.5)、TTA_ENABLED(TTAの有効/無効、デフォルトTrue)、USE_TRACKER(トラッカーの使用有無、デフォルトTrue)
# 環境: Windows想定(PillowでMeiryoフォントを使用)
# 前準備:
# - pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
# - pip install ultralytics opencv-python numpy pillow boxmot
# 特徴技術および学習済モデルの利用制限:
# YOLOv10: AGPL-3.0ライセンス。商用利用の場合は、AGPL-3.0の条件(ソースコード公開義務等)を遵守するか、Ultralyticsからエンタープライズライセンスを取得する必要があります。
# boxmot(ByteTrack実装): AGPL-3.0ライセンス。商用利用の場合は、AGPL-3.0の条件を遵守する必要があります。
# 必ず利用者自身でライセンス条件を確認してください。
import os
import time
import urllib.request
import tkinter as tk
from tkinter import filedialog
from datetime import datetime
import sys
import io
import threading
import cv2
import numpy as np
import torch
import torchvision
from ultralytics import YOLO
from PIL import Image, ImageDraw, ImageFont
from boxmot import ByteTrack
# 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': '最小'},
's': {'name': 'Small', 'desc': '小'},
'm': {'name': 'Medium', 'desc': '中'},
'b': {'name': 'Balanced', 'desc': '中上'},
'l': {'name': 'Large', 'desc': '大'},
'x': {'name': 'Extra Large', 'desc': '最大'}
}
# 調整可能な設定値
CONF_THRESH = 0.5 # 信頼度閾値 - 検出感度制御
IMG_SIZE = 640 # 推論画像サイズ
CLAHE_CLIP_LIMIT = 2.0 # CLAHE制限値
CLAHE_TILE_SIZE = (8, 8) # CLAHEタイルサイズ
WINDOW_NAME = "YOLOv10物体検出" # OpenCVウィンドウ名
TTA_ENABLED = True # TTA(Test Time Augmentation)の有効/無効
TTA_CONF_BOOST = 0.03 # TTA使用時の信頼度ブースト値
NMS_THRESHOLD = 0.6 # TTA用のNMS閾値(独立管理)
USE_TRACKER = True # トラッカーの使用有無
# CLAHEオブジェクトをグローバルスコープで一度だけ定義(AIモデルの入力用にCLAHEを適用)
clahe = cv2.createCLAHE(clipLimit=CLAHE_CLIP_LIMIT, tileGridSize=CLAHE_TILE_SIZE)
# ByteTrackトラッカーを初期化
tracker = ByteTrack() if USE_TRACKER else None
# BGR→RGB色変換のヘルパー関数
def bgr_to_rgb(color_bgr):
"""BGRカラーをRGBカラーに変換"""
return (int(color_bgr[2]), int(color_bgr[1]), int(color_bgr[0]))
# クラス色生成(HSV→BGR)
def generate_class_colors(num_classes):
colors = []
for i in range(num_classes):
hue = int(180.0 * i / max(1, 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_class_colors(80)
# 日本語クラス名マッピング
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': '歯ブラシ'
}
# 日本語フォント設定
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE_MAIN = 20
FONT_SIZE_SMALL = 14
font_main = ImageFont.truetype(FONT_PATH, FONT_SIZE_MAIN)
font_small = ImageFont.truetype(FONT_PATH, FONT_SIZE_SMALL)
# グローバル変数
frame_count = 0
results_log = []
class_counts = {}
model = None
CLASS_NAMES = 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('=== YOLOv10物体検出プログラム ===')
print('概要: フレームごとに物体を検出し、矩形とラベルを描画して表示します')
print('機能: YOLOv10によるCOCO 80クラス検出(NMSフリー設計)+ TTA + ByteTrack追跡')
print('技術: CLAHE (コントラスト強化) + TTA (Test Time Augmentation) + ByteTrack')
print('操作: qキーで終了')
print('出力: 各フレームの全検出一覧をprint表示し、終了時にresult.txtに保存')
print('注意: YOLOv10モデルファイル(yolov10n.pt等)が必要です')
print()
# ===== TTA機能を独立化 =====
def apply_tta_inference(frame, model, conf):
"""Test Time Augmentation (TTA)を適用した推論"""
frame_width = frame.shape[1]
# 水平反転画像を作成
flipped_frame = cv2.flip(frame, 1)
# バッチ推論(元画像と反転画像を同時に処理、デバイスを直接指定)
results = model.predict([frame, flipped_frame], conf=conf, imgsz=IMG_SIZE, verbose=False, device=str(device))
# 元画像の結果を取得
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)
# 信頼度閾値でフィルタリング(NMS前に実施)
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()
# 結果をリスト形式に変換
detections = []
for i in range(len(final_confs)):
# TTAで検出された場合、信頼度をブースト
conf_boost = TTA_CONF_BOOST if TTA_ENABLED else 0
detections.append({
'x1': final_boxes[i][0], 'y1': final_boxes[i][1],
'x2': final_boxes[i][2], 'y2': final_boxes[i][3],
'conf': min(1.0, final_confs[i] + conf_boost),
'class': int(final_classes[i])
})
# nameフィールドを追加
for det in detections:
det['name'] = CLASS_NAMES.get(det['class'], str(det['class']))
# 整数座標に変換
for det in detections:
det['x1'] = int(det['x1'])
det['y1'] = int(det['y1'])
det['x2'] = int(det['x2'])
det['y2'] = int(det['y2'])
return detections
return []
def normal_inference(frame, model, conf):
"""通常の推論処理"""
results = model.predict(frame, conf=conf, imgsz=IMG_SIZE, verbose=False, device=str(device))
curr_dets = []
res = results[0]
if res.boxes is not None:
boxes = res.boxes.xyxy.cpu().numpy()
confs = res.boxes.conf.cpu().numpy()
clses = res.boxes.cls.cpu().numpy()
for i in range(len(boxes)):
box = boxes[i].astype(int)
conf_score = float(confs[i])
cls = int(clses[i])
name = CLASS_NAMES.get(cls, str(cls))
curr_dets.append({
'x1': box[0], 'y1': box[1],
'x2': box[2], 'y2': box[3],
'conf': conf_score,
'class': cls,
'name': name
})
return curr_dets
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(detections, frame):
"""ByteTrackerを使用したトラッキング処理"""
global tracker
# 検出結果が0件でもトラッカーの状態更新と予測結果取得を行う
if len(detections) > 0:
dets_array = np.array([[d['x1'], d['y1'], d['x2'], d['y2'], d['conf'], d['class']]
for d in detections])
else:
# 検出がない場合は空の配列を渡す
dets_array = np.empty((0, 6))
# 常にトラッカーを更新し、現在のフレームでの追跡結果(または予測結果)を取得する
tracks = tracker.update(dets_array, frame)
tracked_dets = []
# 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 = CLASS_NAMES.get(int(cls), str(int(cls)))
tracked_dets.append({
'x1': int(x1), 'y1': int(y1),
'x2': int(x2), 'y2': int(y2),
'track_id': int(track_id),
'conf': float(conf),
'class': int(cls),
'name': name
})
return tracked_dets
def apply_tracking_if_enabled(detections, frame):
"""トラッキング機能を条件付きで適用"""
if not USE_TRACKER:
return detections
return apply_bytetrack(detections, frame)
# ===== 物体検出タスク固有の処理 =====
def draw_detection_results(frame, detections):
"""物体検出の描画処理"""
# バウンディングボックスを描画(OpenCVで)
for det in detections:
color = CLASS_COLORS[det['class'] % len(CLASS_COLORS)]
cv2.rectangle(frame, (det['x1'], det['y1']),
(det['x2'], det['y2']), color, 2)
# 構造化されたテキスト描画を実行(日本語クラス名で表示)
texts_to_draw = []
for det in detections:
color = CLASS_COLORS[det['class'] % len(CLASS_COLORS)]
jp_name = CLASS_NAMES_JP.get(det['name'], det['name'])
if USE_TRACKER and 'track_id' in det:
label = f"ID:{det['track_id']} {jp_name}"
else:
label = jp_name
texts_to_draw.append({
'text': label,
'org': (det['x1'], max(0, det['y1'] - 22)),
'color': bgr_to_rgb(color),
'font_type': 'main'
})
texts_to_draw.append({
'text': f"Conf:{det['conf']:.2f}",
'org': (det['x1'], det['y2'] + 4),
'color': (255, 255, 255),
'font_type': 'small'
})
tta_status = "TTA:ON" if TTA_ENABLED else "TTA:OFF"
tracker_status = "ByteTrack:ON" if USE_TRACKER else "ByteTrack:OFF"
texts_to_draw.append({
'text': f'YOLOv10 ({device.type}) | Frame: {frame_count} | Dets: {len(detections)} | {tta_status} | {tracker_status}',
'org': (10, 10),
'color': (255, 255, 255),
'font_type': 'main'
})
texts_to_draw.append({
'text': '操作: q=終了',
'org': (10, 36),
'color': (255, 255, 0),
'font_type': 'small'
})
frame = draw_texts_with_pillow(frame, texts_to_draw)
return frame
def format_detection_output(detections):
"""物体検出の出力フォーマット(英語クラス名で出力)"""
parts = [f'count={len(detections)}']
for det in detections:
x1, y1, x2, y2 = det['x1'], det['y1'], det['x2'], det['y2']
name = det['name']
conf = det['conf']
if USE_TRACKER and 'track_id' in det:
parts.append(f'class={name},ID={det["track_id"]},conf={conf:.3f},box=[{x1},{y1},{x2},{y2}]')
else:
parts.append(f'class={name},conf={conf:.3f},box=[{x1},{y1},{x2},{y2}]')
return '; '.join(parts) if len(parts) == 1 else parts[0] + '; ' + ' | '.join(parts[1:])
# ===== 共通処理関数 =====
def draw_texts_with_pillow(bgr_frame, texts):
"""Pillowを使用した日本語テキスト描画, 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
font_type = item.get('font_type', 'main')
font = font_main if font_type == 'main' else font_small
draw.text((x, y), text, font=font, fill=color)
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
def detect_objects(frame):
"""共通の検出処理(CLAHE前処理、YOLOv10推論、検出を実行)"""
global model
# AIモデルの入力用にCLAHEを適用(YUV色空間で輝度チャンネルのみ処理)
yuv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV)
yuv_frame[:, :, 0] = clahe.apply(yuv_frame[:, :, 0])
enh_frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2BGR)
# TTA適用
curr_dets = apply_tta_if_enabled(enh_frame, model, CONF_THRESH)
return curr_dets
def process_video_frame(frame):
"""動画フレーム処理用ラッパー"""
# 共通の検出処理
detections = detect_objects(frame)
# トラッキングを条件付きで適用
tracked_dets = apply_tracking_if_enabled(detections, frame)
# クラスごとの検出数を更新
global class_counts
for det in tracked_dets:
name = det['name']
if name not in class_counts:
class_counts[name] = 0
class_counts[name] += 1
# 物体検出固有の描画処理
frame = draw_detection_results(frame, tracked_dets)
# 物体検出固有の出力フォーマット
result = format_detection_output(tracked_dets)
return frame, result
def video_frame_processing(frame):
"""動画フレーム処理のメイン関数"""
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()
# モデル選択(対話的実装)
print("=== モデル選択 ===")
print('使用するYOLOv10モデルを選択してください:')
for key, info in MODEL_INFO.items():
print(f'{key}: {info["name"]} - {info["desc"]}')
print()
model_choice = ''
while model_choice not in MODEL_INFO.keys():
model_choice = input("選択 (n/s/m/b/l/x) [デフォルト: n]: ").strip().lower()
if model_choice == '':
model_choice = 'n'
print('デフォルト(n)を使用します')
break
if model_choice not in MODEL_INFO.keys():
print("無効な選択です。もう一度入力してください。")
MODEL_NAME = f'yolov10{model_choice}.pt'
print(f'選択されたモデル: {MODEL_NAME}')
# モデルの初期化
print(f"\nモデル初期化中...")
try:
model = YOLO(MODEL_NAME)
model.to(device)
CLASS_NAMES = model.names
print('モデル初期化が完了しました')
except Exception as e:
print('モデルの初期化に失敗しました')
print(f'エラー: {e}')
print('注意: モデルファイルの存在とパスを確認してください')
raise SystemExit(1)
# 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:
raise SystemExit(0)
cap = cv2.VideoCapture(path)
elif choice == '1':
cap = ThreadedVideoCapture(0, is_camera=True)
else:
# サンプル動画ダウンロード
print("サンプル動画をダウンロード中...")
SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
SAMPLE_FILE = 'vtest.avi'
urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
cap = cv2.VideoCapture(SAMPLE_FILE)
if not cap.isOpened():
print('動画ファイル・カメラを開けませんでした')
raise SystemExit(1)
# フレームレートの取得とタイムスタンプ増分の計算
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)
processing_time = time.time() - processing_start
total_processing_time += processing_time
cv2.imshow(WINDOW_NAME, processed_frame)
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('=== YOLOv10物体検出結果 ===\n')
f.write(f'処理フレーム数: {frame_count}\n')
f.write(f'使用モデル: {MODEL_NAME}\n')
f.write(f'モデル情報: {MODEL_INFO[model_choice]["name"]} - {MODEL_INFO[model_choice]["desc"]}\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')
f.write('\n'.join(results_log))
print(f'\n処理結果をresult.txtに保存しました')
print(f'検出されたクラス数: {len(class_counts)}')