方位角で補正し,振り向かず歩いた場合に振り向き判定されないように
This commit is contained in:
1 parent
b22d15f105
commit
40591a15a0
5 files changed
+50
-9
No files matched your search
@@ -112,6 +112,11 @@ turn_detection:
|
|||||||
enabled: true
|
enabled: true
|
||||||
signage_yaw_direction: positive
|
signage_yaw_direction: positive
|
||||||
baseline_strategy: first_valid
|
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_window_sec: 0.8
|
||||||
baseline_min_samples: 3
|
baseline_min_samples: 3
|
||||||
subtle_threshold_deg: 10.0
|
subtle_threshold_deg: 10.0
|
||||||
|
|||||||
@@ -236,6 +236,9 @@ class TurnDetectionSettings:
|
|||||||
enabled: bool
|
enabled: bool
|
||||||
signage_yaw_direction: str
|
signage_yaw_direction: str
|
||||||
baseline_strategy: str
|
baseline_strategy: str
|
||||||
|
bearing_correction_enabled: bool
|
||||||
|
horizontal_fov_deg: float
|
||||||
|
bearing_correction_sign: float
|
||||||
baseline_window_sec: float
|
baseline_window_sec: float
|
||||||
baseline_min_samples: int
|
baseline_min_samples: int
|
||||||
subtle_threshold_deg: float
|
subtle_threshold_deg: float
|
||||||
@@ -637,6 +640,12 @@ def _validate(config: AppConfig) -> None:
|
|||||||
turn = config.turn_detection
|
turn = config.turn_detection
|
||||||
if not isinstance(turn.enabled, bool) or turn.signage_yaw_direction not in {"positive", "negative"}:
|
if not isinstance(turn.enabled, bool) or turn.signage_yaw_direction not in {"positive", "negative"}:
|
||||||
raise ConfigurationError("turn enabled/direction settings are invalid")
|
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"}:
|
if turn.baseline_strategy not in {"first_valid", "before_trigger", "region_entry_window"}:
|
||||||
raise ConfigurationError("invalid turn baseline_strategy")
|
raise ConfigurationError("invalid turn baseline_strategy")
|
||||||
if turn.baseline_window_sec <= 0 or turn.baseline_min_samples <= 0:
|
if turn.baseline_window_sec <= 0 or turn.baseline_min_samples <= 0:
|
||||||
|
|||||||
@@ -358,6 +358,8 @@ class TurnFrameResult:
|
|||||||
turn_level: str
|
turn_level: str
|
||||||
continuous_duration_sec: float
|
continuous_duration_sec: float
|
||||||
failure_reason: str = ""
|
failure_reason: str = ""
|
||||||
|
bearing_deg: float | None = None
|
||||||
|
corrected_yaw: float | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.track_id < 0 or self.frame_number < 0 or self.continuous_duration_sec < 0:
|
if self.track_id < 0 or self.frame_number < 0 or self.continuous_duration_sec < 0:
|
||||||
|
|||||||
+29
-7
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
import statistics
|
import statistics
|
||||||
from collections import deque
|
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
|
return self._frame(state, pose, None, None, 0.0, pose.failure_reason or "pose_not_estimated"), events
|
||||||
|
|
||||||
yaw = pose.yaw
|
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
|
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 info.elapsed_time_sec - state.missing_started_elapsed >= self.config.face_missing_before_appearance_sec
|
||||||
and not state.face_appeared_after_missing):
|
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))
|
events.append(self._event("face_appeared", state, info, yaw, None, TurnLevel.FACE_APPEARED.value))
|
||||||
state.missing_started_elapsed = None
|
state.missing_started_elapsed = None
|
||||||
state.last_pose_elapsed = info.elapsed_time_sec
|
state.last_pose_elapsed = info.elapsed_time_sec
|
||||||
state.yaw_history.append((info.elapsed_time_sec, yaw))
|
state.yaw_history.append((info.elapsed_time_sec, corrected_yaw))
|
||||||
self._collect_baseline(state, pose, info, track, events)
|
self._collect_baseline(state, pose, info, track, events, corrected_yaw)
|
||||||
if not state.baseline_acquired or state.baseline_yaw is None:
|
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:
|
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_toward_signage = toward
|
||||||
state.max_yaw_delta_frame, state.max_yaw_delta_time = info.frame_number, info.timestamp_iso
|
state.max_yaw_delta_frame, state.max_yaw_delta_time = info.frame_number, info.timestamp_iso
|
||||||
@@ -95,11 +98,11 @@ class TurnDetector:
|
|||||||
else:
|
else:
|
||||||
events.extend(self._end_candidate(state, info, yaw))
|
events.extend(self._end_candidate(state, info, yaw))
|
||||||
state.turn_level = state.strongest_level if state.strongest_level != TurnLevel.NONE.value else level
|
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(
|
def _collect_baseline(
|
||||||
self, state: TurnTrackState, pose: HeadPoseResult, info: FrameInfo,
|
self, state: TurnTrackState, pose: HeadPoseResult, info: FrameInfo,
|
||||||
track: TrackState | None, events: list[TurnEvent],
|
track: TrackState | None, events: list[TurnEvent], corrected_yaw: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
if state.baseline_acquired or pose.yaw is None:
|
if state.baseline_acquired or pose.yaw is None:
|
||||||
return
|
return
|
||||||
@@ -111,7 +114,7 @@ class TurnDetector:
|
|||||||
and 0 <= info.elapsed_time_sec - track.entry_monotonic <= self.config.baseline_window_sec)
|
and 0 <= info.elapsed_time_sec - track.entry_monotonic <= self.config.baseline_window_sec)
|
||||||
if not allowed:
|
if not allowed:
|
||||||
return
|
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
|
cutoff = info.elapsed_time_sec - self.config.baseline_window_sec
|
||||||
while state.baseline_samples and state.baseline_samples[0][0] < cutoff:
|
while state.baseline_samples and state.baseline_samples[0][0] < cutoff:
|
||||||
state.baseline_samples.popleft()
|
state.baseline_samples.popleft()
|
||||||
@@ -133,6 +136,7 @@ class TurnDetector:
|
|||||||
def _frame(
|
def _frame(
|
||||||
self, state: TurnTrackState, pose: HeadPoseResult, raw: float | None,
|
self, state: TurnTrackState, pose: HeadPoseResult, raw: float | None,
|
||||||
toward: float | None, duration: float, reason: str,
|
toward: float | None, duration: float, reason: str,
|
||||||
|
bearing_deg: float | None = None, corrected_yaw: float | None = None,
|
||||||
) -> TurnFrameResult:
|
) -> TurnFrameResult:
|
||||||
level = state.turn_level
|
level = state.turn_level
|
||||||
if not pose.pose_estimated:
|
if not pose.pose_estimated:
|
||||||
@@ -144,8 +148,26 @@ class TurnDetector:
|
|||||||
pose.face_detected, pose.pose_estimated, state.baseline_acquired, state.baseline_yaw,
|
pose.face_detected, pose.pose_estimated, state.baseline_acquired, state.baseline_yaw,
|
||||||
pose.yaw, raw, toward, self.config.signage_yaw_direction,
|
pose.yaw, raw, toward, self.config.signage_yaw_direction,
|
||||||
state.candidate_start_elapsed is not None, state.turn_confirmed, level, duration, reason,
|
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]]:
|
def finalize_track(self, track_id: int, frame_info: FrameInfo) -> tuple[TurnTrackState | None, list[TurnEvent]]:
|
||||||
state = self._states.pop(track_id, None)
|
state = self._states.pop(track_id, None)
|
||||||
if state is None:
|
if state is None:
|
||||||
|
|||||||
+5
-2
@@ -18,7 +18,8 @@ class TurnLogger:
|
|||||||
|
|
||||||
FRAME_FIELDS = ["experiment_id", "frame_number", "timestamp", "elapsed_time_sec", "track_id", "face_detected",
|
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",
|
"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"]
|
"turn_level", "continuous_duration_sec", "failure_reason"]
|
||||||
EVENT_FIELDS = ["experiment_id", "event_type", "track_id", "frame_number", "timestamp", "elapsed_time_sec",
|
EVENT_FIELDS = ["experiment_id", "event_type", "track_id", "frame_number", "timestamp", "elapsed_time_sec",
|
||||||
"yaw", "yaw_delta_toward_signage", "turn_level", "metadata_json"]
|
"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),
|
"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),
|
"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),
|
"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,
|
"turn_confirmed": item.turn_confirmed, "turn_level": item.turn_level,
|
||||||
"continuous_duration_sec": self._number(item.continuous_duration_sec),
|
"continuous_duration_sec": self._number(item.continuous_duration_sec),
|
||||||
"failure_reason": item.failure_reason,
|
"failure_reason": item.failure_reason,
|
||||||
|
|||||||
Reference in new issue
Block a user