方位角で補正し,振り向かず歩いた場合に振り向き判定されないように

This commit is contained in:
agomizi1cm committed 2026-10-05 21:30:13 +09:00
1 parent b22d15f105
commit 40591a15a0
5 files changed
+50 -9

No files matched your search

+5
View File
@@ -112,6 +112,11 @@ turn_detection:
enabled: true
signage_yaw_direction: positive
baseline_strategy: first_valid
# Camera-relative position compensation for side-crossing pedestrians.
# corrected_yaw = raw_yaw - bearing_correction_sign * bearing_deg
bearing_correction_enabled: true
horizontal_fov_deg: 70.0
bearing_correction_sign: 1.0
baseline_window_sec: 0.8
baseline_min_samples: 3
subtle_threshold_deg: 10.0
+9
View File
@@ -236,6 +236,9 @@ class TurnDetectionSettings:
enabled: bool
signage_yaw_direction: str
baseline_strategy: str
bearing_correction_enabled: bool
horizontal_fov_deg: float
bearing_correction_sign: float
baseline_window_sec: float
baseline_min_samples: int
subtle_threshold_deg: float
@@ -637,6 +640,12 @@ def _validate(config: AppConfig) -> None:
turn = config.turn_detection
if not isinstance(turn.enabled, bool) or turn.signage_yaw_direction not in {"positive", "negative"}:
raise ConfigurationError("turn enabled/direction settings are invalid")
if not isinstance(turn.bearing_correction_enabled, bool):
raise ConfigurationError("turn bearing_correction_enabled must be boolean")
if not isinstance(turn.horizontal_fov_deg, (int, float)) or not 1.0 <= turn.horizontal_fov_deg < 179.0:
raise ConfigurationError("turn horizontal_fov_deg must be in [1, 179)")
if turn.bearing_correction_sign not in {-1.0, 1.0, -1, 1}:
raise ConfigurationError("turn bearing_correction_sign must be +1 or -1")
if turn.baseline_strategy not in {"first_valid", "before_trigger", "region_entry_window"}:
raise ConfigurationError("invalid turn baseline_strategy")
if turn.baseline_window_sec <= 0 or turn.baseline_min_samples <= 0:
+2
View File
@@ -358,6 +358,8 @@ class TurnFrameResult:
turn_level: str
continuous_duration_sec: float
failure_reason: str = ""
bearing_deg: float | None = None
corrected_yaw: float | None = None
def __post_init__(self) -> None:
if self.track_id < 0 or self.frame_number < 0 or self.continuous_duration_sec < 0:
+29 -7
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import math
import statistics
from collections import deque
@@ -55,6 +56,8 @@ class TurnDetector:
return self._frame(state, pose, None, None, 0.0, pose.failure_reason or "pose_not_estimated"), events
yaw = pose.yaw
bearing_deg = self._estimate_bearing_deg(track, info)
corrected_yaw = self._correct_yaw_for_bearing(yaw, bearing_deg)
if (self.config.face_appearance_enabled and state.missing_started_elapsed is not None
and info.elapsed_time_sec - state.missing_started_elapsed >= self.config.face_missing_before_appearance_sec
and not state.face_appeared_after_missing):
@@ -62,12 +65,12 @@ class TurnDetector:
events.append(self._event("face_appeared", state, info, yaw, None, TurnLevel.FACE_APPEARED.value))
state.missing_started_elapsed = None
state.last_pose_elapsed = info.elapsed_time_sec
state.yaw_history.append((info.elapsed_time_sec, yaw))
self._collect_baseline(state, pose, info, track, events)
state.yaw_history.append((info.elapsed_time_sec, corrected_yaw))
self._collect_baseline(state, pose, info, track, events, corrected_yaw)
if not state.baseline_acquired or state.baseline_yaw is None:
return self._frame(state, pose, None, None, 0.0, "baseline_not_acquired"), events
return self._frame(state, pose, None, None, 0.0, "baseline_not_acquired", bearing_deg, corrected_yaw), events
raw, toward = yaw_delta_toward_signage(yaw, state.baseline_yaw, self.config.signage_yaw_direction)
raw, toward = yaw_delta_toward_signage(corrected_yaw, state.baseline_yaw, self.config.signage_yaw_direction)
if state.max_yaw_delta_toward_signage is None or toward > state.max_yaw_delta_toward_signage:
state.max_yaw_delta_toward_signage = toward
state.max_yaw_delta_frame, state.max_yaw_delta_time = info.frame_number, info.timestamp_iso
@@ -95,11 +98,11 @@ class TurnDetector:
else:
events.extend(self._end_candidate(state, info, yaw))
state.turn_level = state.strongest_level if state.strongest_level != TurnLevel.NONE.value else level
return self._frame(state, pose, raw, toward, duration, ""), events
return self._frame(state, pose, raw, toward, duration, "", bearing_deg, corrected_yaw), events
def _collect_baseline(
self, state: TurnTrackState, pose: HeadPoseResult, info: FrameInfo,
track: TrackState | None, events: list[TurnEvent],
track: TrackState | None, events: list[TurnEvent], corrected_yaw: float,
) -> None:
if state.baseline_acquired or pose.yaw is None:
return
@@ -111,7 +114,7 @@ class TurnDetector:
and 0 <= info.elapsed_time_sec - track.entry_monotonic <= self.config.baseline_window_sec)
if not allowed:
return
state.baseline_samples.append((info.elapsed_time_sec, pose.yaw, info.frame_number, info.timestamp_iso))
state.baseline_samples.append((info.elapsed_time_sec, corrected_yaw, info.frame_number, info.timestamp_iso))
cutoff = info.elapsed_time_sec - self.config.baseline_window_sec
while state.baseline_samples and state.baseline_samples[0][0] < cutoff:
state.baseline_samples.popleft()
@@ -133,6 +136,7 @@ class TurnDetector:
def _frame(
self, state: TurnTrackState, pose: HeadPoseResult, raw: float | None,
toward: float | None, duration: float, reason: str,
bearing_deg: float | None = None, corrected_yaw: float | None = None,
) -> TurnFrameResult:
level = state.turn_level
if not pose.pose_estimated:
@@ -144,8 +148,26 @@ class TurnDetector:
pose.face_detected, pose.pose_estimated, state.baseline_acquired, state.baseline_yaw,
pose.yaw, raw, toward, self.config.signage_yaw_direction,
state.candidate_start_elapsed is not None, state.turn_confirmed, level, duration, reason,
bearing_deg, corrected_yaw,
)
def _estimate_bearing_deg(self, track: TrackState | None, info: FrameInfo) -> float | None:
"""Estimate the person's camera-relative bearing from tracked x position."""
if not self.config.bearing_correction_enabled or track is None or info.width <= 1:
return 0.0 if self.config.bearing_correction_enabled and track is None else None
if not track.trajectory:
return None
center_x = track.trajectory[-1].center_x
x_norm = max(0.0, min(1.0, center_x / float(info.width - 1)))
half_fov_rad = math.radians(self.config.horizontal_fov_deg) / 2.0
phi_rad = math.atan((x_norm - 0.5) * 2.0 * math.tan(half_fov_rad))
return math.degrees(phi_rad)
def _correct_yaw_for_bearing(self, yaw: float, bearing_deg: float | None) -> float:
if not self.config.bearing_correction_enabled or bearing_deg is None:
return yaw
return yaw - self.config.bearing_correction_sign * bearing_deg
def finalize_track(self, track_id: int, frame_info: FrameInfo) -> tuple[TurnTrackState | None, list[TurnEvent]]:
state = self._states.pop(track_id, None)
if state is None:
+5 -2
View File
@@ -18,7 +18,8 @@ class TurnLogger:
FRAME_FIELDS = ["experiment_id", "frame_number", "timestamp", "elapsed_time_sec", "track_id", "face_detected",
"pose_estimated", "baseline_available", "baseline_yaw", "current_yaw", "yaw_delta_raw",
"yaw_delta_toward_signage", "signage_yaw_direction", "turn_candidate", "turn_confirmed",
"yaw_delta_toward_signage", "signage_yaw_direction", "bearing_deg", "corrected_yaw",
"turn_candidate", "turn_confirmed",
"turn_level", "continuous_duration_sec", "failure_reason"]
EVENT_FIELDS = ["experiment_id", "event_type", "track_id", "frame_number", "timestamp", "elapsed_time_sec",
"yaw", "yaw_delta_toward_signage", "turn_level", "metadata_json"]
@@ -73,7 +74,9 @@ class TurnLogger:
"baseline_available": item.baseline_available, "baseline_yaw": self._number(item.baseline_yaw),
"current_yaw": self._number(item.current_yaw), "yaw_delta_raw": self._number(item.yaw_delta_raw),
"yaw_delta_toward_signage": self._number(item.yaw_delta_toward_signage),
"signage_yaw_direction": item.signage_direction, "turn_candidate": item.turn_candidate,
"signage_yaw_direction": item.signage_direction,
"bearing_deg": self._number(item.bearing_deg), "corrected_yaw": self._number(item.corrected_yaw),
"turn_candidate": item.turn_candidate,
"turn_confirmed": item.turn_confirmed, "turn_level": item.turn_level,
"continuous_duration_sec": self._number(item.continuous_duration_sec),
"failure_reason": item.failure_reason,