diff --git a/config/default.yaml b/config/default.yaml index 34f19ea..5510d43 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -337,6 +337,7 @@ performance: skip_frames: 0 fps_average_window: 30 warn_fps_threshold: 10.0 + warmup_frames: 5 debug: enabled: false show_raw_model_output: false diff --git a/src/application.py b/src/application.py index ca36c86..914dcd1 100644 --- a/src/application.py +++ b/src/application.py @@ -143,7 +143,7 @@ class Application: person_state_manager: PersonStateManager | None = None unified_builder: UnifiedEventBuilder | None = None analysis_builder: AnalysisTableBuilder | None = None - fps = FPSCounter(self.config.performance.fps_average_window) + fps = FPSCounter(self.config.performance.fps_average_window, self.config.performance.warmup_frames) visualizer = Visualizer(self.config.display, self.config.performance.warn_fps_threshold, self.config.performance.fps_average_window) frame_number, processed_frames = 0, 0 start = time.perf_counter() @@ -277,10 +277,10 @@ class Application: should_infer = (frame_number - 1) % (self.config.performance.skip_frames + 1) == 0 detections: list[Detection] = detector.detect(frame, region) if should_infer else [] pre_info = FrameInfo(frame_number, datetime.now().astimezone().isoformat(timespec="milliseconds"), - time.perf_counter() - start - paused_total, width, height, fps.average_fps) + time.perf_counter() - start - paused_total, width, height, fps.recent_average_fps) tracked = tracker.update(detections, frame, pre_info) if tracker is not None else [] fps.tick() - measured = fps.average_fps + measured = fps.recent_average_fps now = datetime.now().astimezone() info = FrameInfo(frame_number, now.isoformat(timespec="milliseconds"), time.perf_counter() - start - paused_total, width, height, measured) events = track_manager.update(tracked, info) if track_manager is not None else [] @@ -537,7 +537,7 @@ class Application: self._writer.release() cv2.destroyAllWindows() elapsed = time.perf_counter() - start - LOGGER.info("Run summary: frames=%d elapsed=%.2fs average=%.2f FPS", processed_frames, elapsed, processed_frames / elapsed if elapsed else 0.0) + LOGGER.info("Run summary: frames=%d elapsed=%.2fs average=%.2f FPS", processed_frames, elapsed, fps.average_fps) @staticmethod def _tracking_counters(manager: TrackManager | None) -> dict[str, int] | None: diff --git a/src/config_loader.py b/src/config_loader.py index 78ab96f..9b5e4a4 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -435,6 +435,7 @@ class PerformanceSettings: skip_frames: int fps_average_window: int warn_fps_threshold: float + warmup_frames: float @dataclass(frozen=True, slots=True) @@ -549,6 +550,8 @@ def _validate(config: AppConfig) -> None: raise ConfigurationError("detector image_size/max_detections must be positive and class ID non-negative") if config.performance.fps_average_window <= 0 or config.performance.skip_frames < 0: raise ConfigurationError("performance window must be positive and skip_frames non-negative") + if config.performance.warmup_frames < 0: + raise ConfigurationError("performance warmup_frames must be non-negative") if config.display.resize_scale <= 0: raise ConfigurationError("display.resize_scale must be positive") if config.display.trajectory_max_points <= 0: diff --git a/src/fps_counter.py b/src/fps_counter.py index ed2c1df..baaaf87 100644 --- a/src/fps_counter.py +++ b/src/fps_counter.py @@ -5,25 +5,56 @@ from collections import deque class FPSCounter: - """Calculate instantaneous and moving-average FPS.""" + """Calculate instantaneous, overall-average and recent-average FPS. - def __init__(self, average_window: int) -> None: + - current_fps: 直近1フレームの瞬間FPS(表示用) + - average_fps: ウォームアップ後の「総フレーム数 ÷ 経過時間」 + - recent_average_fps: 直近 average_window フレームの「フレーム数 ÷ 経過時間」 + """ + + def __init__(self, average_window: int, warmup_frames: int) -> None: if average_window <= 0: raise ValueError("average_window must be positive") - self._samples: deque[float] = deque(maxlen=average_window) + if warmup_frames < 0: + raise ValueError("warmup_frames must be non-negative") + self._average_window = average_window + # 起動直後の数フレームは初期化処理で遅くなるため平均から除外する + self._warmup_frames = warmup_frames + # 計測の基準となる tick 番号(1回目の tick は基準時刻の記録のみ) + self._baseline_tick = max(warmup_frames, 1) + self._reset_state() + + def _reset_state(self) -> None: self._last_time: float | None = None self._current_fps = 0.0 + self._tick_count = 0 + self._start_time: float | None = None + self._frames = 0 # 基準時刻以降に経過したフレーム間隔の数 + # 窓内の平均用タイムスタンプ(N個の間隔に N+1 点必要) + self._timestamps: deque[float] = deque(maxlen=self._average_window + 1) def tick(self) -> float: now = time.perf_counter() + self._tick_count += 1 + if self._last_time is None: self._last_time = now - return 0.0 - elapsed = now - self._last_time - self._last_time = now - self._current_fps = 1.0 / elapsed if elapsed > 0.0 else 0.0 - if self._current_fps > 0.0: - self._samples.append(self._current_fps) + self._current_fps = 0.0 + else: + elapsed = now - self._last_time + self._last_time = now + self._current_fps = 1.0 / elapsed if elapsed > 0.0 else 0.0 + + # ウォームアップ期間中は表示用の瞬間FPSのみ更新し、平均には含めない + if self._tick_count == self._baseline_tick: + self._start_time = now + self._frames = 0 + self._timestamps.clear() + self._timestamps.append(now) + elif self._tick_count > self._baseline_tick: + self._frames += 1 + self._timestamps.append(now) + return self._current_fps @property @@ -32,13 +63,24 @@ class FPSCounter: @property def average_fps(self) -> float: - return sum(self._samples) / len(self._samples) if self._samples else 0.0 + """ウォームアップ後の総フレーム数 ÷ 経過時間。""" + if self._start_time is None or self._last_time is None or self._frames == 0: + return 0.0 + elapsed = self._last_time - self._start_time + return self._frames / elapsed if elapsed > 0.0 else 0.0 + + @property + def recent_average_fps(self) -> float: + """直近 average_window フレームのフレーム数 ÷ 経過時間。""" + if len(self._timestamps) < 2: + return 0.0 + elapsed = self._timestamps[-1] - self._timestamps[0] + return (len(self._timestamps) - 1) / elapsed if elapsed > 0.0 else 0.0 @property def sample_count(self) -> int: - return len(self._samples) + """平均に含まれているフレーム数(ウォームアップ除外後)。""" + return self._frames def reset(self) -> None: - self._samples.clear() - self._last_time = None - self._current_fps = 0.0 + self._reset_state()