From acd08951c8b91502f12c03d53d2f83baf102a14b Mon Sep 17 00:00:00 2001 From: KoyoNoguchi Date: Fri, 2 Oct 2026 23:25:01 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=A6=E3=82=A9=E3=83=BC=E3=83=A0=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E6=99=82=E3=81=AEFPS=E3=82=92=E5=B9=B3?= =?UTF-8?q?=E5=9D=87FPS=E3=81=8B=E3=82=89=E9=99=A4=E5=A4=96=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3=E3=81=97?= =?UTF-8?q?=EF=BC=8C=E5=B9=B3=E5=9D=87FPS=E3=82=92=E7=B7=8F=E3=83=95?= =?UTF-8?q?=E3=83=AC=E3=83=BC=E3=83=A0=E6=95=B0/=E7=B5=8C=E9=81=8E?= =?UTF-8?q?=E6=99=82=E9=96=93=E3=81=A7=E8=A8=88=E7=AE=97=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/default.yaml | 1 + src/application.py | 8 ++--- src/config_loader.py | 3 ++ src/fps_counter.py | 70 +++++++++++++++++++++++++++++++++++--------- 4 files changed, 64 insertions(+), 18 deletions(-) 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()