AsianPlayer's picture
download
raw
12.7 kB
"""AIDE / Driver-WM evaluation protocol constants (arXiv 2605.05092v2, Sec. 4 + App. B).
Everything that defines *what is being measured* lives here so that the cache builder,
the analytic baselines, the trainer and the harness cannot drift apart.
Coordinate frame: HALPE-136 2-D keypoints of the in-cabin ("incar") view, pixels of a
1920x1080 image. Keypoint arrays are stored as [..., 136, 3] = (x, y, conf).
"""
from __future__ import annotations
import math
import os
from pathlib import Path
import numpy as np
# --------------------------------------------------------------------------- paths
AIDE_ROOT = Path(os.environ.get("AIDE_ROOT", "/home/USER/AIDE"))
AIDE_ANN_DIR = AIDE_ROOT / "annotation"
HERE = Path(__file__).resolve().parent
AIDE_PKG = HERE.parent # cwm3/aide
RESULTS = AIDE_PKG / "results"
CACHE_NPZ = RESULTS / "cache" / "aide_kp.npz"
SPLIT_CSV = {s: HERE / f"splits_{n}.csv" for s, n in
(("train", "training"), ("val", "validation"), ("test", "testing"))}
EXPECTED_SPLIT_SIZES = {"train": 1884, "val": 405, "test": 609}
VIEWS = ("front", "left", "right", "incar")
# --------------------------------------------------------------------------- image / clip
W, H = 1920, 1080
DIAG = math.sqrt(W ** 2 + H ** 2) # 2202.907...
FPS = 15
N_FRAMES_STD = 45 # 3 s clips; 13 clips are longer
N_SAMPLE = 10 # frames per clip used by the protocol
T_OBS = 5
T_PRED = 5
assert T_OBS + T_PRED == N_SAMPLE
J = 136
# --------------------------------------------------------------------------- sampling grid
# Protocol grid, LOCKED BY GATE 0 (aide/data/verify_protocol.py, results/cache/verify_protocol.json):
# 'floor' == np.linspace(0, n-1, 10).astype(int) -> for n=45: 0,4,9,14,19,24,29,34,39,44.
# This is the only grid that reproduces the paper's zero-velocity All/HM numbers (52.93/139.23
# vs 52.89/139.19). 'round' (0,5,10,15,20,24,29,34,39,44) gives 53.17/136.17 with the same
# driver rule and is kept as a sensitivity variant. The future block (24,29,34,39,44) is
# identical for floor and round, so the HM threshold (72.05) does not depend on this choice.
SAMPLE_GRID = "floor"
def sample_idx(n: int, mode: str | None = None) -> np.ndarray:
"""Uniformly sample N_SAMPLE frame indices from a clip of n frames (mode=None -> SAMPLE_GRID)."""
mode = SAMPLE_GRID if mode is None else mode
g = np.linspace(0, n - 1, N_SAMPLE)
if mode == "round":
idx = np.round(g)
elif mode == "floor":
idx = np.floor(g)
elif mode == "ceil":
idx = np.ceil(g)
else:
raise ValueError(mode)
idx = idx.astype(int)
assert idx[0] == 0 and idx[-1] == n - 1 and len(idx) == N_SAMPLE
return idx
SAMPLE_GRIDS = ("round", "floor", "ceil")
# --------------------------------------------------------------------------- driver selection
# AIDE annotation frames often contain several detections (17.5 % of frames; passengers,
# mirror reflections). bbox is [x, y, w, h] in pixels (verified: max x+w == 1920).
# Protocol rule, LOCKED BY GATE 0: the driver is the FIRST detection in `result`
# (`result[0]`; identical to "minimum tracking idx"). On the official test split this
# reproduces the paper's zero-velocity numbers (All 52.93 / d-n 2.403 / HM 139.23 / HM
# threshold 72.052 / PCK@0.05 85.95 vs paper 52.89 / 2.40 / 139.19 / 72.05 / 85.95).
# 'largest_bbox' (the rule used in the earlier all-2,898-clip audit, where its 52.75 was a
# coincidence) gives 48.64 / 107.9 on the official test split and is kept as the main
# sensitivity variant (it is arguably the semantically better driver selector).
# If a frame has zero detections the previous frame's selected detection is carried
# forward (and the first frames are back-filled from the first frame with a detection).
DRIVER_RULE = "first"
DRIVER_RULES = ("first", "largest_bbox", "max_score", "iou_track")
ALT_DRIVER_RULE, ALT_SAMPLE_GRID = "largest_bbox", "round" # stored in the cache as S10_alt
def bbox_area(b) -> float:
return float(b[2]) * float(b[3])
def bbox_iou_xywh(a, b) -> float:
ax0, ay0, ax1, ay1 = a[0], a[1], a[0] + a[2], a[1] + a[3]
bx0, by0, bx1, by1 = b[0], b[1], b[0] + b[2], b[1] + b[3]
iw = max(0.0, min(ax1, bx1) - max(ax0, bx0))
ih = max(0.0, min(ay1, by1) - max(ay0, by0))
inter = iw * ih
ua = a[2] * a[3] + b[2] * b[3] - inter
return inter / ua if ua > 0 else 0.0
def select_driver(frames: list, rule: str = DRIVER_RULE):
"""frames: pose_list (list over frames of {'imgname', 'result': [det, ...]}).
Returns (kp [n,136,3] float32, bbox [n,4] float32, n_det [n] int, filled [n] bool).
`filled` marks frames whose detection was carried over from a neighbour.
"""
n = len(frames)
kp = np.full((n, J, 3), np.nan, dtype=np.float32)
bb = np.full((n, 4), np.nan, dtype=np.float32)
ndet = np.zeros(n, dtype=np.int16)
prev_box = None
for t, fr in enumerate(frames):
dets = fr["result"]
ndet[t] = len(dets)
if not dets:
continue
if rule == "largest_bbox":
d = max(dets, key=lambda r: bbox_area(r["bbox"]))
elif rule == "first":
d = dets[0]
elif rule == "max_score":
d = max(dets, key=lambda r: float(r["proposal_score"]))
elif rule == "iou_track":
if prev_box is None:
d = max(dets, key=lambda r: bbox_area(r["bbox"]))
else:
d = max(dets, key=lambda r: bbox_iou_xywh(r["bbox"], prev_box))
else:
raise ValueError(rule)
k = np.asarray(d["keypoints"], dtype=np.float32)
assert k.size == J * 3, k.size
kp[t] = k.reshape(J, 3)
bb[t] = np.asarray(d["bbox"], dtype=np.float32)
prev_box = bb[t]
filled = ndet == 0
# forward fill then back fill frames without any detection
last = None
for t in range(n):
if filled[t]:
if last is not None:
kp[t], bb[t] = kp[last], bb[last]
else:
last = t
nxt = None
for t in range(n - 1, -1, -1):
if filled[t] and np.isnan(kp[t, 0, 0]):
if nxt is not None:
kp[t], bb[t] = kp[nxt], bb[nxt]
elif not filled[t]:
nxt = t
return kp, bb, ndet, filled
# --------------------------------------------------------------------------- HALPE-136 layout
# 0-16 COCO body, 17 head-top, 18 neck, 19 pelvis, 20-25 feet (L/R big toe, small toe, heel),
# 26-93 face (68 iBUG landmarks), 94-114 left hand (21), 115-135 right hand (21).
PARTS = {
"body": np.arange(0, 26),
"face": np.arange(26, 94),
"lhand": np.arange(94, 115),
"rhand": np.arange(115, 136),
}
PART_NAMES = tuple(PARTS)
assert sum(len(v) for v in PARTS.values()) == J
# Kinematic edges. Source: AlphaPose `alphapose/utils/vis.py`, `l_pair` for the 136-keypoint
# Halpe format (github.com/MVIG-SJTU/AlphaPose; Halpe-FullBody repo by Fang-Haoshu).
EDGES_BODY = [
(0, 1), (0, 2), (1, 3), (2, 4), # head
(5, 18), (6, 18), (5, 7), (7, 9), (6, 8), (8, 10), # arms via neck
(17, 18), (18, 19), (19, 11), (19, 12), # spine / pelvis
(11, 13), (12, 14), (13, 15), (14, 16), # legs
(20, 24), (21, 25), (23, 25), (22, 24), (15, 24), (16, 25), # feet
]
_HAND_CHAIN = [(0, 1), (1, 2), (2, 3), (3, 4),
(0, 5), (5, 6), (6, 7), (7, 8),
(0, 9), (9, 10), (10, 11), (11, 12),
(0, 13), (13, 14), (14, 15), (15, 16),
(0, 17), (17, 18), (18, 19), (19, 20)]
EDGES_LHAND = [(94 + a, 94 + b) for a, b in _HAND_CHAIN]
EDGES_RHAND = [(115 + a, 115 + b) for a, b in _HAND_CHAIN]
EDGES_BODY_HANDS = EDGES_BODY + EDGES_LHAND + EDGES_RHAND
# Face edges derived from the standard iBUG-68 topology (not from AlphaPose): jaw chain,
# brows, nose bridge + base, two eye loops, outer and inner lip loops. Optional; the
# geometric losses use EDGES_BODY_HANDS.
def _chain(lo, hi, close=False):
e = [(i, i + 1) for i in range(lo, hi)]
if close:
e.append((hi, lo))
return e
_FACE68 = (_chain(0, 16) + _chain(17, 21) + _chain(22, 26) + _chain(27, 30) + _chain(31, 35)
+ _chain(36, 41, True) + _chain(42, 47, True) + _chain(48, 59, True) + _chain(60, 67, True))
EDGES_FACE = [(26 + a, 26 + b) for a, b in _FACE68]
EDGES_ALL = EDGES_BODY_HANDS + EDGES_FACE
# Left/right flip pairs (for horizontal-flip augmentation; swap indices then mirror x).
FLIP_PAIRS_BODY = [(1, 2), (3, 4), (5, 6), (7, 8), (9, 10), (11, 12), (13, 14), (15, 16),
(20, 21), (22, 23), (24, 25)]
FLIP_PAIRS_HANDS = [(94 + i, 115 + i) for i in range(21)]
_FACE68_FLIP = ([(i, 16 - i) for i in range(8)] + [(17 + i, 26 - i) for i in range(5)]
+ [(31, 35), (32, 34)]
+ [(36, 45), (37, 44), (38, 43), (39, 42), (40, 47), (41, 46)]
+ [(48, 54), (49, 53), (50, 52), (55, 59), (56, 58)]
+ [(60, 64), (61, 63), (65, 67)])
FLIP_PAIRS_FACE = [(26 + a, 26 + b) for a, b in _FACE68_FLIP]
FLIP_PAIRS = FLIP_PAIRS_BODY + FLIP_PAIRS_HANDS + FLIP_PAIRS_FACE
def flip_perm() -> np.ndarray:
p = np.arange(J)
for a, b in FLIP_PAIRS:
p[a], p[b] = b, a
return p
# --------------------------------------------------------------------------- labels
TASKS = ("dbr", "der", "tcr", "vcr")
TASK_KEY = {"dbr": "driver_behavior_label", "der": "emotion_label",
"tcr": "scene_centric_context_label", "vcr": "vehicle_based_context_label"}
CLASSES = {
"dbr": ("Normal Driving", "Looking Around", "Making Phone", "Body Movement",
"Talking", "Smoking", "Dozing Off"),
"der": ("Peace", "Anxiety", "Weariness", "Happiness", "Anger"),
"tcr": ("Smooth Traffic", "Waiting", "Traffic Jam"),
"vcr": ("Forward Moving", "Parking", "Turning", "Backward Moving", "Changing Lane"),
}
# Raw-string variants observed in the annotation files -> canonical class name.
LABEL_NORMALISE = {
"happiness": "Happiness",
"weariness": "Weariness",
"Forward": "Forward Moving",
}
def normalise_label(task: str, raw: str) -> str:
s = str(raw).strip()
s = LABEL_NORMALISE.get(s, s)
if s in CLASSES[task]:
return s
low = {c.lower(): c for c in CLASSES[task]}
if s.lower() in low:
return low[s.lower()]
raise KeyError(f"unknown label {raw!r} for task {task}")
def label_index(task: str, raw: str) -> int:
return CLASSES[task].index(normalise_label(task, raw))
# --------------------------------------------------------------------------- metrics protocol
PCK_THRESHOLDS = (0.05, 0.10)
# PCK normaliser, LOCKED BY GATE 0: the paper's PCK@0.05 = 85.95 is reproduced (85.948) only
# with threshold = 0.05 * max(W, H) = 96 px; 0.05 * DIAG (110.1 px) gives 87.67. d-nMPJPE,
# in contrast, does use DIAG (100*52.93/2202.9 = 2.403 = paper 2.40). The DIAG-normalised
# PCK is reported as a secondary metric (pck05_diag / pck10_diag).
PCK_NORM = float(max(W, H))
def pck_threshold_px(t: float, norm: str = "max_side") -> float:
return t * (PCK_NORM if norm == "max_side" else DIAG)
HM_FRACTION = 0.10 # High-Motion subset = top int(0.1*N) clips
HM_N_TRANSITIONS = 4 # displacement averaged over the 4 future transitions
def hm_top(n_clips: int) -> int:
"""Size of the High-Motion subset for a split with n_clips clips (test: 60)."""
return int(HM_FRACTION * n_clips)
assert hm_top(EXPECTED_SPLIT_SIZES["test"]) == 60
# Published Driver-WM Table-1 numbers on the official test split (for Gate 0 / tables).
PAPER = {
"zero_velocity": {"mpjpe_all": 52.89, "dn_mpjpe": 2.40, "mpjpe_hm": 139.19,
"pck05": 85.95, "hm_threshold": 72.05},
"static_pooling": {"mpjpe_hm": 134.56},
"driver_wm": {"mpjpe_all": 71.47, "dn_mpjpe": 3.24, "mpjpe_hm": 138.03, "pck05": 71.66,
"f1_dbr": 68.07, "f1_der": 72.61, "f1_tcr": 90.15, "f1_vcr": 68.34,
"hm_h5": 155.82},
}
GATE0_TOL = {"mpjpe_all": 0.5, "dn_mpjpe": 0.03, "mpjpe_hm": 2.0, "hm_threshold": 2.0, "pck05": 1.0}
# Qwen3-VL feature-extraction prompt (Driver-WM App. B.2, verbatim).
QWEN_PROMPT = ("Extract multimodal features that capture: (A) external traffic cues (vehicles, "
"signals, road agents), (B) how these cues act as triggers for driver motion "
"responses, and (C) the short-term evolution (1-2 seconds) of driver behavior in "
"response to the changing road context.")

Xet Storage Details

Size:
12.7 kB
·
Xet hash:
c7f20950cef89c5eae6b25e2cc8c26acc5be17bf6ffcf756d811c2e4605b0e77

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.