AsianPlayer's picture
download
raw
42.6 kB
"""cwm3/baselines_dms/extractors/ov_gaze.py — baseline I1 "ov_gaze": the Intel OpenVINO Open Model Zoo gaze / eye-state pipeline on the driver crop (plan §3, 2026-09-14).
Pipeline (the OMZ gaze_estimation_demo chain, re-implemented in Python on top of openvino.Core, CPU, 4 inference threads; every IR from the registry
after its sha256 check):
driver crop (video_io.driver_crop, +10 %; frame decoded at NATIVE size)
-> face-detection-adas-0001 BGR f32 0..255, cv2.resize INTER_LINEAR, no mean / scale (model.yml has none). fd_mode "reshape" (DEFAULT; the demo's
-fd_reshape option): the network input [1,3,384,672] is reshaped to [1,3,384,w], w = round(384 * W / H) rounded to a
multiple of 32 in [64, 672], so the ~1:2 portrait driver crop is resized (almost) isotropically — one compiled model per
distinct w, cached. fd_mode "default" (the demo's default): anisotropic resize of the crop to 672x384; on the phase-1
frames this squashes the ~60 px faces ~3.8x and loses 14 % of the frames to conf < 0.5 (fd_input_variants.json).
detection_out [1,1,200,7] = (image_id, label, conf, x0, y0, x1, y1) normalised; keep the highest-confidence box with
conf >= 0.5 (FACE_CONF_MIN); none -> NaN row (fail 3)
-> demo adjustBoundingBox x -= 0.067 w, y -= 0.028 h, w *= 1.15, h *= 1.13, then squared on the larger side (gaze_estimation_demo face_detector.cpp),
clipped to the crop
-> head-pose-estimation-adas-0001 face square -> 60x60 BGR f32 0..255 -> angle_y_fc (yaw), angle_p_fc (pitch), angle_r_fc (roll), degrees
-> facial-landmarks-35-adas-0002 same 60x60 face -> align_fc3 [1,70] = 35 normalised (x, y) pairs; points 0-1 = corners of the eye on the IMAGE-LEFT
(the demo's "left eye"), 2-3 = the image-right eye
-> eye squares side = 1.8 x |corner distance| (demo createEyeBoundingBox), centred on the corner midpoint, min side EYE_MIN_PX, border
replicated when the square leaves the crop; both squares rotated by the head roll about their centre (demo roll align,
cv2.getRotationMatrix2D(center, roll, 1) + BORDER_REPLICATE)
-> open-closed-eye-0001 each eye square -> 32x32 BGR f32 0..255 (mean 127 / scale 255 of model.yml are baked into the converted IR) -> '19'
[1,2,1,1] softmax; CLOSED = index 0 (verified: open-eyed sample portrait gives [~0, 1.0]; on driver frames index 1
dominates; see README "eye class order")
-> gaze-estimation-adas-0002 left_eye_image / right_eye_image [1,3,60,60] BGR f32 0..255 + head_pose_angles ('138') [1,3] = (yaw, pitch, roll) deg
-> gaze_vector [1,3]; normalised, rotated back by the roll (demo), yaw = 90 + atan2(z, x) deg, pitch = asin(y) deg
(demo gazeVectorToGazeAngles): yaw > 0 = gaze toward the IMAGE RIGHT, pitch > 0 = up.
Channels (C = 11; f16 in the cache): ov_face_conf [0,1] | ov_head_yaw, ov_head_pitch, ov_head_roll deg | ov_gaze_x, ov_gaze_y, ov_gaze_z unit vector
(camera frame after roll-back; z < 0 = toward the camera) | ov_gaze_yaw, ov_gaze_pitch deg | ov_eye_closed_l, ov_eye_closed_r closed probability of the
image-left / image-right eye [0,1]. Roles (summarise vocabulary): see ROLES. A row is valid iff a face was found (conf >= 0.5) and every channel is finite.
No label is read here; the phase-1 `y` column is only used for a descriptive distribution in measurements.json. No openpilot field is read.
Spans: the manifest windows [win_start, win_end] are snapped to the 10 Hz grid and pulled inwards by GRID_EPS_S = 0.02 s (grid_spans, the same helper
as mp_face.py) before run_route: run_route stores spans as float32 and video_io.frames_in_spans floors / ceils them without tolerance, which
otherwise reads one frame AFTER win_end (19 / 45 routes) or BEFORE win_start (19 / 45) — found by the 2026-09-14 verification and fixed; exactly
the frames k0 .. k1 = round(10 win_start) .. round(10 win_end) are decoded now, none later than the requested window end.
python baselines_dms/extractors/ov_gaze.py --self-test [--uid UID] # 1 local TRAIN route, 30 frames, temp dir; + sample-portrait check
python baselines_dms/extractors/ov_gaze.py --phase1 [--workers 3] [--max-minutes 60] [--fd-mode reshape|default] [--force]
# 50 frozen manifest windows -> results/baselines_dms/phase1/ov_gaze/
"""
from __future__ import annotations
import json
import math
import os
import resource
import sys
import time
from pathlib import Path
import numpy as np
os.environ.setdefault("OMP_NUM_THREADS", "4")
os.environ.setdefault("MKL_NUM_THREADS", "4")
HERE = Path(__file__).resolve().parent
PKG = HERE.parent
CWM3 = PKG.parent
for p in (str(PKG), str(HERE)):
if p not in sys.path:
sys.path.insert(0, p)
import cache_schema as cs # noqa: E402
import video_io as vio # noqa: E402
from base import EXIT_DONE, Extractor, run_route, run_routes # noqa: E402
BASELINE = "ov_gaze"
PHASE1_DIR = cs.RESULTS_ROOT / "phase1"
OUT_DIR = PHASE1_DIR / BASELINE
MANIFEST = PHASE1_DIR / "manifest_v1.csv"
FACE_PACK_DIR = CWM3 / "results" / "targets_face_causal_routes_v2"
N_THREADS = 4
FACE_CONF_MIN = 0.5
EYE_SCALE = 1.8
EYE_MIN_PX = 8
FACE_MIN_PX = 12
FD_H, FD_W_DEFAULT, FD_W_STEP, FD_W_MIN = 384, 672, 32, 64
MODELS = {"fd": ("omz_face_detection_adas_0001", "face-detection-adas-0001.xml"),
"hp": ("omz_head_pose_estimation_adas_0001", "head-pose-estimation-adas-0001.xml"),
"lm": ("omz_facial_landmarks_35_adas_0002", "facial-landmarks-35-adas-0002.xml"),
"oc": ("omz_open_closed_eye_0001", "open-closed-eye-0001.xml"),
"gz": ("omz_gaze_estimation_adas_0002", "gaze-estimation-adas-0002.xml")}
CHANNELS = ["ov_face_conf", "ov_head_yaw", "ov_head_pitch", "ov_head_roll", "ov_gaze_x", "ov_gaze_y", "ov_gaze_z", "ov_gaze_yaw", "ov_gaze_pitch",
"ov_eye_closed_l", "ov_eye_closed_r"]
ROLES = {"ov_face_conf": "conf", "ov_head_yaw": "angle_deg", "ov_head_pitch": "angle_deg", "ov_head_roll": "angle_deg", "ov_gaze_x": "none", "ov_gaze_y": "none",
"ov_gaze_z": "none", "ov_gaze_yaw": "angle_deg", "ov_gaze_pitch": "angle_deg", "ov_eye_closed_l": "eye_closed_prob", "ov_eye_closed_r": "eye_closed_prob"}
UNITS = {"ov_face_conf": "probability [0,1]", "ov_head_yaw": "deg", "ov_head_pitch": "deg", "ov_head_roll": "deg", "ov_gaze_x": "unit-vector component [-1,1]",
"ov_gaze_y": "unit-vector component [-1,1]", "ov_gaze_z": "unit-vector component [-1,1]", "ov_gaze_yaw": "deg", "ov_gaze_pitch": "deg",
"ov_eye_closed_l": "probability [0,1]", "ov_eye_closed_r": "probability [0,1]"}
EYE_CLOSED_INDEX = 0 # open-closed-eye-0001 output '19': [p_closed, p_open] (empirical; see module docstring)
GRID_EPS_S = 0.02 # span ends are snapped to the 10 Hz grid and pulled inwards by this much (see grid_spans)
def grid_spans(spans, hz=cs.HZ, eps=GRID_EPS_S):
"""[[start, end]] -> f64[S,2] with both ends snapped to the hz grid and pulled INWARDS by eps: [k0/hz + eps, k1/hz - eps] (same as mp_face.grid_spans).
run_route stores spans as float32 and video_io.frames_in_spans floors / ceils them without tolerance, so a raw end such as 834.2 (f32 834.20001)
would pull in the frame at 834.3 = one step AFTER the requested window end (and 1336.2 -> f32 1336.19995 one step before the start). With
eps = 0.02 s (>> the f32 spacing, 0.002 s at 5 h) floor / ceil land exactly on k0 .. k1; cache_schema.covers (tol 0.051) still accepts the raw
windows against the stored spans."""
a = np.asarray(spans, np.float64).reshape(-1, 2)
k0, k1 = np.round(a[:, 0] * hz), np.round(a[:, 1] * hz)
return np.stack([k0 / hz + eps, k1 / hz - eps], 1)
def _nchw(img_bgr_u8, wh):
"""u8 BGR HxWx3 -> f32 [1,3,h,w] in 0..255 (cv2.resize INTER_LINEAR as the demo's ie_wrapper does; no mean / scale for the Intel models)."""
import cv2
r = cv2.resize(img_bgr_u8, wh, interpolation=cv2.INTER_LINEAR) if (img_bgr_u8.shape[1], img_bgr_u8.shape[0]) != tuple(wh) else img_bgr_u8
return np.ascontiguousarray(r.transpose(2, 0, 1)[None]).astype(np.float32)
def adjust_face_box(x0, y0, x1, y1, W, H):
"""gaze_estimation_demo face_detector.cpp adjustBoundingBox: loosen (-6.7 % x, -2.8 % y, +15 % w, +13 % h) then square on the larger side; clipped."""
w, h = x1 - x0, y1 - y0
x0 -= 0.067 * w
y0 -= 0.028 * h
w *= 1.15
h *= 1.13
if w < h:
x0 -= (h - w) / 2
w = h
else:
y0 -= (w - h) / 2
h = w
X0, Y0 = int(max(0, round(x0))), int(max(0, round(y0)))
X1, Y1 = int(min(W, round(x0 + w))), int(min(H, round(y0 + h)))
return X0, Y0, X1, Y1
def eye_square(p1, p2, scale=EYE_SCALE, min_px=EYE_MIN_PX):
"""demo createEyeBoundingBox: side = scale x |p1 - p2| (at least min_px), centred on the midpoint -> (x0, y0, side)."""
side = max(int(scale * float(np.hypot(*(p1 - p2)))), int(min_px))
mid = (p1 + p2) / 2.0
return int(round(mid[0] - side / 2.0)), int(round(mid[1] - side / 2.0)), side
def crop_pad(img, x0, y0, side):
"""img[y0:y0+side, x0:x0+side] with BORDER_REPLICATE padding where the square leaves the image -> (u8[side,side,3], padded: bool)."""
import cv2
H, W = img.shape[:2]
cx0, cy0, cx1, cy1 = max(0, x0), max(0, y0), min(W, x0 + side), min(H, y0 + side)
if cx1 <= cx0 or cy1 <= cy0:
return None, True
sub = img[cy0:cy1, cx0:cx1]
pads = (cy0 - y0, (y0 + side) - cy1, cx0 - x0, (x0 + side) - cx1)
if any(p > 0 for p in pads):
return cv2.copyMakeBorder(sub, *pads, cv2.BORDER_REPLICATE), True
return sub, False
def rotate_about_center(img, angle_deg):
import cv2
h, w = img.shape[:2]
M = cv2.getRotationMatrix2D((w / 2.0, h / 2.0), float(angle_deg), 1.0)
return cv2.warpAffine(img, M, (w, h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
def gaze_angles(g):
"""demo gazeVectorToGazeAngles: (yaw, pitch) deg from a gaze vector (normalised inside)."""
r = float(np.linalg.norm(g))
if not np.isfinite(r) or r <= 0:
return float("nan"), float("nan")
yaw = math.degrees(math.pi / 2 + math.atan2(float(g[2]), float(g[0])))
pitch = math.degrees(math.pi / 2 - math.acos(max(-1.0, min(1.0, float(g[1]) / r))))
return yaw, pitch
class OVGazeExtractor(Extractor):
baseline = BASELINE
channels = list(CHANNELS)
roles = dict(ROLES)
units = dict(UNITS)
model_ids = [MODELS[k][0] for k in ("fd", "hp", "lm", "oc", "gz")]
needs_crop = True
decode_wh = None # native frames: the eye squares (~30-60 px) need the full resolution
batch = 32
roll_align = True
fd_mode = "reshape" # "reshape" (demo -fd_reshape; default here) | "default" (demo default anisotropic 672x384)
def describe(self):
d = super().describe()
d.update(fd_mode=self.fd_mode, roll_align=bool(self.roll_align), face_conf_min=FACE_CONF_MIN, eye_scale=EYE_SCALE, eye_min_px=EYE_MIN_PX)
return d
def spec(self):
"""{channel: role} in the summarise vocabulary (angle_deg | eye_closed_prob | eye_open_ratio | prob | logit | embedding | conf | none)."""
return dict(self.roles)
def summarise_spec(self):
"""the summarise.SPECS-style role dict for this baseline (thresholds are the frozen decisions.md constants held by summarise.py)."""
import summarise as SM
return SM.spec_for(self.baseline, self.channels)
def setup(self, registry_entry=None):
import cv2
import openvino as ov
import registry as REG
cv2.setNumThreads(N_THREADS)
self.core = ov.Core()
cfg = {"INFERENCE_NUM_THREADS": N_THREADS, "PERFORMANCE_HINT": "LATENCY"}
self.compiled, self.req, self.weight_sha256, entries = {}, {}, {}, {}
for key, (rid, xml) in MODELS.items():
d = REG.resolve(rid) # asserts the sha256 of every file of the entry
e = REG.get(rid)
entries[rid] = {k: e.get(k) for k in ("id", "name", "version", "sha256", "local_path")}
self.weight_sha256[rid] = e["sha256"]
for f in e.get("files", []):
if f["name"].endswith((".xml", ".bin")):
self.weight_sha256[f"{rid}/{f['name']}"] = f["sha256"]
m = self.core.read_model(str(d / xml))
self.compiled[key] = self.core.compile_model(m, "CPU", cfg)
self.req[key] = self.compiled[key].create_infer_request()
if key == "fd":
self.fd_xml, self.fd_cfg = str(d / xml), cfg
self.fd_models = {FD_W_DEFAULT: (self.compiled[key], self.req[key])}
self.in_names = {k: [i.get_any_name() for i in c.inputs] for k, c in self.compiled.items()}
self.out_names = {k: [o.get_any_name() for o in c.outputs] for k, c in self.compiled.items()}
assert self.in_names["fd"] == ["data"] and self.out_names["fd"] == ["detection_out"], (self.in_names["fd"], self.out_names["fd"])
assert set(self.out_names["hp"]) == {"angle_y_fc", "angle_p_fc", "angle_r_fc"}, self.out_names["hp"]
assert self.out_names["lm"] == ["align_fc3"] and self.in_names["oc"] == ["input.1"], (self.out_names["lm"], self.in_names["oc"])
assert set(self.in_names["gz"]) == {"left_eye_image", "right_eye_image", "138"} and self.out_names["gz"] == ["gaze_vector"], (self.in_names["gz"], self.out_names["gz"])
self.gz_hp_name = "138"
self.lib_versions = {"openvino": ov.__version__, "opencv-python": cv2.__version__, "numpy": np.__version__, "python": sys.version.split()[0]}
self.registry_entry = registry_entry if registry_entry is not None else entries
self.stats = {"n_crops": 0, "n_face": 0, "n_eye_padded": 0, "n_eye_min_side": 0, "n_face_small": 0, "eye_p_open_sum": 0.0, "eye_p_open_n": 0, "fd_mode": self.fd_mode, "fd_widths": {}}
self.infer_ms = [] # per-crop wall ms of the 5-model chain (measurement only)
self.last_debug = None
return self
# ------------------------------------------------------------------ one crop
def _fd_for(self, W, H):
"""(compiled, request, (w, h)) of the face detector for a crop of size W x H: reshaped to the crop aspect (fd_mode reshape) or the stock 672x384."""
if self.fd_mode != "reshape":
return (*self.fd_models[FD_W_DEFAULT], (FD_W_DEFAULT, FD_H))
w = int(round(FD_H * W / max(H, 1)))
w = int(min(FD_W_DEFAULT, max(FD_W_MIN, (w // FD_W_STEP) * FD_W_STEP)))
if w not in self.fd_models:
m = self.core.read_model(self.fd_xml)
m.reshape([1, 3, FD_H, w])
c = self.core.compile_model(m, "CPU", self.fd_cfg)
self.fd_models[w] = (c, c.create_infer_request())
self.stats["fd_widths"][str(w)] = self.stats["fd_widths"].get(str(w), 0) + 1
return (*self.fd_models[w], (w, FD_H))
def _face(self, crop):
H, W = crop.shape[:2]
fd, req, wh = self._fd_for(W, H)
det = req.infer({"data": _nchw(crop, wh)})[fd.output(0)][0, 0]
det = det[(det[:, 0] >= 0) & (det[:, 2] >= FACE_CONF_MIN)]
if len(det) == 0:
return None
b = det[np.argmax(det[:, 2])]
conf = float(b[2])
x0, y0, x1, y1 = np.clip(b[3:7], 0.0, 1.0) * np.array([W, H, W, H], np.float64)
if x1 - x0 < 2 or y1 - y0 < 2:
return None
return conf, (x0, y0, x1, y1)
def infer_one(self, crop, debug=False):
"""u8 BGR driver crop -> f32[C] (NaN row when no face)."""
import cv2
row = np.full(self.C, np.nan, np.float32)
if crop is None or crop.size == 0 or crop.ndim != 3:
return row
H, W = crop.shape[:2]
f = self._face(crop)
if f is None:
return row
conf, raw_box = f
X0, Y0, X1, Y1 = adjust_face_box(*raw_box, W, H)
if X1 - X0 < FACE_MIN_PX or Y1 - Y0 < FACE_MIN_PX:
self.stats["n_face_small"] += 1
return row
self.stats["n_face"] += 1
face = crop[Y0:Y1, X0:X1]
f60 = _nchw(face, (60, 60))
r = self.req["hp"].infer({"data": f60})
ang = {o.get_any_name(): float(r[o].ravel()[0]) for o in self.compiled["hp"].outputs}
yaw, pitch, roll = ang["angle_y_fc"], ang["angle_p_fc"], ang["angle_r_fc"]
pts = self.req["lm"].infer({"data": f60})[self.compiled["lm"].output(0)].reshape(35, 2).astype(np.float64)
P = np.stack([X0 + pts[:, 0] * (X1 - X0), Y0 + pts[:, 1] * (Y1 - Y0)], 1)
eyes, eye_boxes = [], []
for a, b in ((0, 1), (2, 3)):
ex, ey, side = eye_square(P[a], P[b])
if side == EYE_MIN_PX:
self.stats["n_eye_min_side"] += 1
e, padded = crop_pad(crop, ex, ey, side)
if e is None:
return row # eye square entirely outside the crop: treat as "nothing found"
self.stats["n_eye_padded"] += int(padded)
if self.roll_align:
e = rotate_about_center(e, roll)
eyes.append(e)
eye_boxes.append((ex, ey, side))
closed = []
for e in eyes:
p = self.req["oc"].infer({"input.1": _nchw(e, (32, 32))})[self.compiled["oc"].output(0)].ravel()
closed.append(float(p[EYE_CLOSED_INDEX]))
self.stats["eye_p_open_sum"] += float(p[1 - EYE_CLOSED_INDEX])
self.stats["eye_p_open_n"] += 1
g = self.req["gz"].infer({"left_eye_image": _nchw(eyes[0], (60, 60)), "right_eye_image": _nchw(eyes[1], (60, 60)),
self.gz_hp_name: np.array([[yaw, pitch, roll]], np.float32)})[self.compiled["gz"].output(0)].ravel().astype(np.float64)
n = float(np.linalg.norm(g))
if n > 0 and np.isfinite(n):
g = g / n
if self.roll_align: # demo: undo the eye-image roll rotation on the estimated vector
cs_, sn_ = math.cos(math.radians(roll)), math.sin(math.radians(roll))
g = np.array([g[0] * cs_ + g[1] * sn_, -g[0] * sn_ + g[1] * cs_, g[2]])
gyaw, gpitch = gaze_angles(g)
row[:] = [conf, yaw, pitch, roll, g[0], g[1], g[2], gyaw, gpitch, closed[0], closed[1]]
if debug:
self.last_debug = {"face_box": (X0, Y0, X1, Y1), "raw_box": tuple(float(v) for v in raw_box), "eye_boxes": eye_boxes, "landmarks": P, "row": row.copy()}
return row
def infer(self, crops):
out = np.full((len(crops), self.C), np.nan, np.float32)
for i, c in enumerate(crops):
if c is None:
continue
t0 = time.perf_counter()
out[i] = self.infer_one(c)
self.infer_ms.append(1000.0 * (time.perf_counter() - t0))
self.stats["n_crops"] += 1
return out
EXTRACTOR = OVGazeExtractor
# ---------------------------------------------------------------------------------------------------------------- helpers (measurements)
def _pq(v):
v = np.asarray([x for x in v if np.isfinite(x)], np.float64)
return {"p50": float(np.percentile(v, 50)), "p95": float(np.percentile(v, 95)), "n": int(len(v))} if len(v) else {"p50": None, "p95": None, "n": 0}
def _rss_mb():
ru_self = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024.0
ru_child = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss / 1024.0
return {"self_peak_mb": round(ru_self, 1), "children_peak_mb_max_single": round(ru_child, 1)}
def _vram_mb():
"""CPU-only baseline: no CUDA context is created; report the GPU memory used by THIS process as seen by nvidia-smi (expected 0)."""
import subprocess
try:
out = subprocess.run(["nvidia-smi", "--query-compute-apps=pid,used_memory", "--format=csv,noheader,nounits"], capture_output=True, text=True, timeout=10).stdout
mine = [int(l.split(",")[1]) for l in out.splitlines() if l.strip() and int(l.split(",")[0]) == os.getpid()]
return float(sum(mine))
except Exception:
return 0.0
def _read_manifest(path=MANIFEST):
import csv
rows = []
with open(path) as f:
for r in csv.DictReader(f):
rows.append({"idx": int(r["idx"]), "uid": r["uid"], "regime": r["regime"], "y": int(r["y"]), "t_anchor": float(r["t_anchor"]), "row": int(r["row"]),
"win_start": float(r["win_start"]), "win_end": float(r["win_end"]), "dongle": r["dongle"], "video": r["video"]})
return rows
def _spearman(a, b):
from scipy.stats import spearmanr
a, b = np.asarray(a, np.float64), np.asarray(b, np.float64)
m = np.isfinite(a) & np.isfinite(b)
if m.sum() < 5:
return None, int(m.sum())
r = spearmanr(a[m], b[m]).correlation
return (None if not np.isfinite(r) else float(r)), int(m.sum())
def _face_pack_hist(regime, rows_idx):
"""face pack rows -> (t_hist f64[n,80], lm_yaw_dev f32[n,80], eye_open f32[n,80], t_anchor f64[n], uid[n]) for the given anchor rows."""
p = FACE_PACK_DIR / f"facehist_causal_{regime}_train.npz"
with np.load(p, allow_pickle=True) as z:
ch = [str(c) for c in z["channels"]]
j_yaw, j_eye = ch.index("lm_yaw_dev"), ch.index("eye_open")
idx = np.asarray(rows_idx, np.int64)
t = z["t"][idx].astype(np.float64)
Fh = z["Fh8_c"][idx].astype(np.float32)
Fv = z["Fh8valid"][idx].astype(bool)
uid = z["uid"][idx].astype(str)
steps = np.arange(80) - 79 # Fh8_c[:, j] is the value at t + (j - 79) / 10 (t-7.9 .. t)
T = t[:, None] + steps[None, :] / 10.0
yaw, eye = Fh[:, :, j_yaw], Fh[:, :, j_eye]
yaw[~Fv] = np.nan
eye[~Fv] = np.nan
return T, yaw, eye, t, uid
def _lookup(cache, times, tol=0.02):
"""cache rows at the given times (10 Hz grid) -> f32[n, C] (NaN when absent / invalid)."""
k = np.round(cache.t.astype(np.float64) * 10).astype(np.int64)
want = np.round(np.asarray(times, np.float64) * 10).astype(np.int64)
pos = {int(kk): i for i, kk in enumerate(k)}
out = np.full((len(want), cache.C), np.nan, np.float32)
F = cache.feat.astype(np.float32)
for i, w in enumerate(want):
j = pos.get(int(w))
if j is not None:
out[i] = F[j]
return out
def _decode_frames(uid, frame_idx_list, vpath=None):
"""seek + retrieve a handful of native frames (contact sheet only); -> {frame_idx: u8 BGR}."""
import cv2
vp = Path(vpath) if vpath else vio.video_path(uid)
cap = cv2.VideoCapture(str(vp))
out = {}
for fi in sorted(set(int(f) for f in frame_idx_list)):
cap.set(cv2.CAP_PROP_POS_FRAMES, fi)
ok, fr = cap.read()
if ok:
out[fi] = fr
cap.release()
return out
def contact_sheet(ex, picks, out_png, tile=(300, 300)):
"""picks: list of dict(uid, frame_idx, t, box(native), yaw, gyaw, tag) -> PNG grid (3 x 4) of driver crops with the face / eye boxes, the gaze arrow and the yaw printed."""
import cv2
tiles = []
by_uid = {}
for p in picks:
by_uid.setdefault(p["uid"], []).append(p["frame_idx"])
frames = {u: _decode_frames(u, fis) for u, fis in by_uid.items()}
for p in picks:
fr = frames[p["uid"]].get(p["frame_idx"])
crop = vio.driver_crop(fr, np.asarray(p["box"], np.float32)) if fr is not None else None
canvas = np.zeros((tile[1], tile[0], 3), np.uint8)
if crop is not None:
crop = np.ascontiguousarray(crop)
row = ex.infer_one(crop, debug=True)
dbg = ex.last_debug if np.isfinite(row).all() else None
vis = crop.copy()
if dbg is not None:
X0, Y0, X1, Y1 = dbg["face_box"]
cv2.rectangle(vis, (X0, Y0), (X1, Y1), (0, 255, 0), 2)
for (ex_, ey_, side) in dbg["eye_boxes"]:
cv2.rectangle(vis, (ex_, ey_), (ex_ + side, ey_ + side), (255, 200, 0), 2)
P = dbg["landmarks"]
mid = ((P[0] + P[1]) / 2 + (P[2] + P[3]) / 2) / 2
g = row[4:7]
L = 0.6 * (X1 - X0)
cv2.arrowedLine(vis, (int(mid[0]), int(mid[1])), (int(mid[0] + g[0] * L), int(mid[1] - g[1] * L)), (0, 0, 255), 3, tipLength=0.2)
s = min(tile[0] / vis.shape[1], tile[1] / vis.shape[0])
small = cv2.resize(vis, (max(1, int(vis.shape[1] * s)), max(1, int(vis.shape[0] * s))), interpolation=cv2.INTER_AREA)
canvas[:small.shape[0], :small.shape[1]] = small
yaw_now = float(row[1]) if np.isfinite(row[1]) else float("nan")
gyaw_now = float(row[7]) if np.isfinite(row[7]) else float("nan")
else:
yaw_now = gyaw_now = float("nan")
txt1 = f"{p['tag']} head_yaw {yaw_now:+.1f} (cache {p['yaw']:+.1f})"
txt2 = f"gaze_yaw {gyaw_now:+.1f} t={p['t']:.1f}s"
for k, txt in enumerate((txt1, txt2, p["uid"][:34])):
cv2.putText(canvas, txt, (4, tile[1] - 40 + 16 * k), cv2.FONT_HERSHEY_SIMPLEX, 0.42, (0, 0, 0), 3, cv2.LINE_AA)
cv2.putText(canvas, txt, (4, tile[1] - 40 + 16 * k), cv2.FONT_HERSHEY_SIMPLEX, 0.42, (255, 255, 255), 1, cv2.LINE_AA)
tiles.append(canvas)
while len(tiles) % 4:
tiles.append(np.zeros((tile[1], tile[0], 3), np.uint8))
rows = [np.hstack(tiles[i:i + 4]) for i in range(0, len(tiles), 4)]
sheet = np.vstack(rows)
header = np.full((28, sheet.shape[1], 3), 255, np.uint8)
cv2.putText(header, "ov_gaze sign convention: top row = 6 most NEGATIVE ov_head_yaw, bottom rows = 6 most POSITIVE (green face box, cyan eye squares, red gaze arrow = (gaze_x, -gaze_y))",
(6, 19), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 0), 1, cv2.LINE_AA)
sheet = np.vstack([header, sheet])
Path(out_png).parent.mkdir(parents=True, exist_ok=True)
cv2.imwrite(str(out_png), sheet)
return out_png
# ---------------------------------------------------------------------------------------------------------------- CLI modes
def make_extractor(fd_mode="reshape"):
ex = OVGazeExtractor()
ex.fd_mode = fd_mode
return ex.setup(None)
def self_test(uid=None, workers=2, fd_mode="reshape"):
import tempfile
import cv2
t0 = time.time()
ex = make_extractor(fd_mode)
rep = {"self_test": "extractors.ov_gaze", "describe": ex.describe(), "roles": ex.spec()}
# (1) the sample portrait: exactly one face, eyes open, gaze near the camera
im = cv2.imread(str(PKG / "weights" / "_samples" / "portrait.jpg"))
row = ex.infer_one(im, debug=True)
assert np.isfinite(row).all() and row[0] >= 0.9, row
assert row[9] < 0.5 and row[10] < 0.5, ("sample portrait eyes must classify as open", row[9:11])
rep["portrait"] = {c: round(float(v), 4) for c, v in zip(ex.channels, row)}
# (2) an empty / no-face crop -> NaN row
assert np.isnan(ex.infer_one(np.zeros((200, 200, 3), np.uint8))).all()
assert np.isnan(ex.infer([None])).all()
# (3) one local TRAIN route, 30 frames through run_route
if uid is None:
cands = [(vio.video_path(u).stat().st_size, u) for u in vio.anchors_by_uid(("manual_train", "engaged_train")) if vio.has_video(u) and vio.pose_path(u) is not None]
if not cands:
rep["route"] = "none local; skipped"
print(json.dumps(rep, indent=1, default=str))
return rep
uid = min(cands)[1]
ts = vio.anchors_of(uid)
W, H, n = vio.video_info(vio.video_path(uid))
t_a = float(ts[len(ts) // 2]) if len(ts) else min(30.0, n / vio.FPS - 3.1)
span = grid_spans([[max(0.0, t_a - 2.9), t_a]]) # exactly 30 frames at 10 Hz (grid-snapped; none after t_a)
ex.infer_ms.clear()
with tempfile.TemporaryDirectory() as d:
code, path, info = run_route(ex, uid, span, d, max_minutes=10, workers=workers, block=15)
rc = cs.RouteCache.load(path)
assert code == EXIT_DONE and rc.n == 30 and rc.C == ex.C and rc.channels == ex.channels, (code, rc.n, rc.C)
code2, _, info2 = run_route(ex, uid, span, d, max_minutes=10, workers=workers)
assert code2 == EXIT_DONE and info2["status"] == "exists"
F = rc.feat.astype(np.float32)
rep["route"] = {"uid": uid, "native_wh": [W, H], "span": span.tolist(), "n_frames": rc.n, "fail_hist": info["fail_hist"], "valid_rate": info["valid_rate"],
"runtime": rc.runtime, "decode_worker_fps": info["decode_worker_fps"], "wall_s": info["wall_s"], "rerun": info2["status"],
"infer_ms_per_crop": _pq(ex.infer_ms), "stats": dict(ex.stats),
"head_yaw_valid": [round(float(v), 1) for v in F[rc.valid, 1][:10]],
"eye_p_open_mean": (ex.stats["eye_p_open_sum"] / ex.stats["eye_p_open_n"] if ex.stats["eye_p_open_n"] else None),
"meta_keys": sorted(rc.meta), "summarise_spec": ex.summarise_spec()}
rep["wall_s"] = round(time.time() - t0, 1)
rep["rss"] = _rss_mb()
print(json.dumps(rep, indent=1, default=str))
return rep
def phase1(workers=3, max_minutes=90.0, out_dir=OUT_DIR, manifest=MANIFEST, force=False, fd_mode="reshape"):
import cv2
t_start = time.time()
rows = _read_manifest(manifest)
assert len(rows) == 50, len(rows)
uids = sorted({r["uid"] for r in rows})
spans = {u: cs.merge_spans(grid_spans([[r["win_start"], r["win_end"]] for r in rows if r["uid"] == u])) for u in uids} # exactly frames k0..k1, none after win_end
ex = make_extractor(fd_mode)
cache_dir = Path(out_dir) / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
log_lines = []
def log(m):
print(m, flush=True)
log_lines.append(m)
vram_before = _vram_mb()
pre_exist = {u for u in uids if (cache_dir / f"{u}.npz").exists()} if not force else set()
code, summ = run_routes(ex, uids, spans, cache_dir, max_minutes=max_minutes, workers=workers, log=log, block=40, force=force)
wall_run = time.time() - t_start
vram_after = _vram_mb()
# ------------------------------------------------------------------ measurements over the caches
caches = {u: cs.RouteCache.load(cache_dir / f"{u}.npz") for u in uids if (cache_dir / f"{u}.npz").exists()}
n_frames = int(sum(c.n for c in caches.values()))
n_written = int(sum(c.runtime.get("n_frames", 0) for u, c in caches.items() if u not in pre_exist)) # frames computed by THIS run
fail_hist = {}
fail_all = np.concatenate([c.fail for c in caches.values()]) if caches else np.zeros(0, np.uint8)
for k, v in zip(*np.unique(fail_all, return_counts=True)):
fail_hist[cs.FAIL_CODES[int(k)]] = int(v)
valid_all = np.concatenate([c.valid for c in caches.values()]) if caches else np.zeros(0, bool)
box_all = np.concatenate([np.isfinite(c.box).all(1) for c in caches.values()]) if caches else np.zeros(0, bool)
frame_all = fail_all != 2
valid_share = float(valid_all.mean()) if n_frames else None
valid_share_box = float(valid_all[box_all & frame_all].mean()) if (box_all & frame_all).any() else None
# decode ms: frame-weighted percentiles of the per-route p50 / p95 (each route's runtime holds per-block per-frame ms percentiles)
dec50 = [c.runtime.get("decode_ms_p50") for c in caches.values()]
dec95 = [c.runtime.get("decode_ms_p95") for c in caches.values()]
wts = [c.n for c in caches.values()]
def wmed(v, w):
v = np.asarray([x if x is not None else np.nan for x in v], np.float64)
w = np.asarray(w, np.float64)
m = np.isfinite(v)
if not m.any():
return None
o = np.argsort(v[m])
cw = np.cumsum(w[m][o])
return float(v[m][o][np.searchsorted(cw, cw[-1] / 2)])
# ------------------------------------------------------------------ per-anchor values + face-pack correlation
F_all = {}
per_anchor = []
yaw_pairs, gyaw_pairs, eye_pairs, per_anchor_rho = [], [], [], []
yaw_dev_pairs = []
for regime in ("manual", "engaged"):
rr = [r for r in rows if r["regime"] == regime]
T, lm_yaw_dev, eye_open, t_pack, uid_pack = _face_pack_hist(regime, [r["row"] for r in rr])
for i, r in enumerate(rr):
assert abs(t_pack[i] - r["t_anchor"]) < 0.06 and uid_pack[i] == r["uid"], (r["idx"], t_pack[i], r["t_anchor"], uid_pack[i])
c = caches.get(r["uid"])
if c is None:
per_anchor.append({"idx": r["idx"], "uid": r["uid"], "regime": regime, "y": r["y"], "t_anchor": r["t_anchor"], "cache": "missing"})
continue
Fh = _lookup(c, T[i]) # [80, C] over the pack's history grid (t-7.9 .. t)
at = _lookup(c, [r["t_anchor"]])[0]
m3 = slice(50, 80) # last 3 s
win_valid = np.isfinite(Fh[:, 1])
yaw_pairs.append((Fh[:, 1], lm_yaw_dev[i]))
gyaw_pairs.append((Fh[:, 7], lm_yaw_dev[i]))
eye_pairs.append((np.nanmean(Fh[:, 9:11], 1) if win_valid.any() else Fh[:, 9], eye_open[i]))
yaw_dev_pairs.append((Fh[:, 1] - np.nanmedian(Fh[:, 1]) if win_valid.any() else Fh[:, 1], lm_yaw_dev[i] - np.nanmedian(lm_yaw_dev[i])))
rho_i, n_i = _spearman(Fh[:, 1], lm_yaw_dev[i])
per_anchor_rho.append(rho_i)
rec = {"idx": r["idx"], "uid": r["uid"], "regime": regime, "y": r["y"], "t_anchor": r["t_anchor"], "valid_at_anchor": bool(np.isfinite(at).all()),
"valid_share_8s": float(win_valid.mean()), "rho_head_yaw_vs_lm_yaw_dev_8s": rho_i, "n_pairs_8s": n_i,
"abs_head_yaw_max_3s": (float(np.nanmax(np.abs(Fh[m3, 1]))) if np.isfinite(Fh[m3, 1]).any() else None),
"share_abs_head_yaw_gt20_3s": (float(np.nanmean(np.abs(Fh[m3, 1]) > 20)) if np.isfinite(Fh[m3, 1]).any() else None)}
for j, ch in enumerate(ex.channels):
rec[f"{ch}_at_anchor"] = (float(at[j]) if np.isfinite(at[j]) else None)
per_anchor.append(rec)
def pooled(pairs):
a = np.concatenate([p[0] for p in pairs]) if pairs else np.zeros(0)
b = np.concatenate([p[1] for p in pairs]) if pairs else np.zeros(0)
return _spearman(a, b)
rho_yaw, n_yaw = pooled(yaw_pairs)
rho_gyaw, n_gyaw = pooled(gyaw_pairs)
rho_eye, n_eye = pooled(eye_pairs)
rho_yaw_dev, n_yaw_dev = pooled(yaw_dev_pairs)
rhos = [r for r in per_anchor_rho if r is not None]
# descriptive score distribution by y (no tuning; y is not used anywhere else)
desc = {}
for y in (0, 1):
v = [a["abs_head_yaw_max_3s"] for a in per_anchor if a.get("y") == y and a.get("abs_head_yaw_max_3s") is not None]
s = [a["share_abs_head_yaw_gt20_3s"] for a in per_anchor if a.get("y") == y and a.get("share_abs_head_yaw_gt20_3s") is not None]
desc[f"y{y}"] = {"n": len(v), "abs_head_yaw_max_3s_median": (float(np.median(v)) if v else None), "abs_head_yaw_max_3s_mean": (float(np.mean(v)) if v else None),
"share_abs_head_yaw_gt20_3s_mean": (float(np.mean(s)) if s else None)}
# ------------------------------------------------------------------ contact sheet: 6 most negative / 6 most positive head yaw (distinct routes, conf >= 0.9)
cands = []
for u, c in caches.items():
F = c.feat.astype(np.float32)
ok = c.valid & (F[:, 0] >= 0.9)
for j in np.flatnonzero(ok):
cands.append((float(F[j, 1]), u, int(c.frame_idx[j]), float(c.t[j]), c.box[j].tolist(), float(F[j, 7])))
cands.sort(key=lambda z: z[0])
def pick(seq, k):
out, seen = [], set()
for z in seq:
if z[1] in seen:
continue
seen.add(z[1])
out.append(z)
if len(out) == k:
break
return out
neg = pick(cands, 6)
pos = pick(cands[::-1], 6)
picks = [{"uid": u, "frame_idx": fi, "t": t, "box": b, "yaw": yw, "gyaw": g, "tag": "NEG"} for (yw, u, fi, t, b, g) in neg] + \
[{"uid": u, "frame_idx": fi, "t": t, "box": b, "yaw": yw, "gyaw": g, "tag": "POS"} for (yw, u, fi, t, b, g) in pos[::-1]]
sheet = Path(out_dir) / "sign_convention_contact_sheet.png"
if picks:
contact_sheet(ex, picks, sheet)
wall_total = time.time() - t_start
meas = {
"baseline": BASELINE, "written_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "manifest": str(manifest), "n_anchors": len(rows), "n_routes": len(uids),
"n_routes_cached": len(caches), "run_summary": summ, "exit_code": int(code), "workers": int(workers), "decode_wh": "native", "extractor": ex.describe(), "channels": ex.channels, "roles": ex.spec(),
"units": ex.units, "model_ids": ex.model_ids, "weight_sha256": ex.weight_sha256, "lib_versions": ex.lib_versions,
"n_frames": n_frames, "n_frames_written_this_run": n_written, "n_routes_written_this_run": len([u for u in caches if u not in pre_exist]),
"valid_share": valid_share, "valid_share_with_box": valid_share_box, "fail_hist": fail_hist,
"frames_with_box": int((box_all & frame_all).sum()), "frames_no_frame": int((~frame_all).sum()),
"decode_ms_per_frame": {"p50_frame_weighted_median_of_route_p50": wmed(dec50, wts), "p95_frame_weighted_median_of_route_p95": wmed(dec95, wts),
"route_p50_list": [round(v, 2) if v is not None else None for v in dec50]},
"infer_ms_per_frame": _pq(ex.infer_ms), "infer_ms_note": "wall ms of the 5-model chain per crop (face detector + head pose + landmarks + 2 x eye state + gaze), CPU 4 threads; only frames with a crop",
"wall_s_run_routes": round(wall_run, 1), "wall_s_total_incl_measurements": round(wall_total, 1),
"fps_end_to_end": (round(n_written / wall_run, 2) if wall_run > 0 and n_written else None),
"fps_note": "frames written this run / wall seconds of run_routes (decode + crop + infer + cache write; None when every cache already existed)",
"peak_rss_mb": _rss_mb(), "vram_mb_peak": max(vram_before, vram_after), "vram_note": "CPU-only OpenVINO; no CUDA context; nvidia-smi compute-apps for this pid",
"extractor_stats": dict(ex.stats), "eye_class_order": {"closed_index": EYE_CLOSED_INDEX, "mean_p_open_over_eyes": (ex.stats["eye_p_open_sum"] / ex.stats["eye_p_open_n"] if ex.stats["eye_p_open_n"] else None),
"rho_mean_eye_closed_vs_facepack_eye_open": rho_eye, "n_pairs": n_eye},
"spearman_vs_face_pack": {"rho_head_yaw_vs_lm_yaw_dev_pooled": rho_yaw, "n_pairs": n_yaw, "rho_gaze_yaw_vs_lm_yaw_dev_pooled": rho_gyaw, "n_pairs_gaze": n_gyaw,
"rho_head_yaw_minus_window_median_vs_lm_yaw_dev_minus_window_median_pooled": rho_yaw_dev, "n_pairs_dev": n_yaw_dev,
"per_anchor_rho_head_yaw_median": (float(np.median(rhos)) if rhos else None), "per_anchor_rho_head_yaw_iqr": ([float(np.percentile(rhos, 25)), float(np.percentile(rhos, 75))] if rhos else None),
"n_anchors_with_rho": len(rhos), "face_pack": str(FACE_PACK_DIR), "alignment": "Fh8_c[:, j] at t_anchor + (j - 79) / 10 s; cache rows matched on the 10 Hz grid"},
"sign_convention": {"lm_yaw_def": "(nose_x - eye_mid_x) / IOD: > 0 = nose displaced to the IMAGE RIGHT = head turned toward the image right",
"ov_head_yaw": ("same sign as lm_yaw_dev (> 0 = head turned toward the IMAGE RIGHT)" if (rho_yaw or 0) > 0 else "OPPOSITE sign to lm_yaw_dev (> 0 = head turned toward the image left)") if rho_yaw is not None else "undetermined",
"ov_gaze_yaw": "demo gazeVectorToGazeAngles: 90 + atan2(z, x) deg; > 0 = gaze toward the IMAGE RIGHT; ov_gaze_pitch = asin(y) deg, > 0 = up",
"contact_sheet": str(sheet), "contact_sheet_picks": picks},
"descriptive_by_y": desc, "per_anchor": per_anchor, "log": log_lines[-60:],
}
Path(out_dir).mkdir(parents=True, exist_ok=True)
(Path(out_dir) / "measurements.json").write_text(json.dumps(meas, indent=1, default=str))
(Path(out_dir) / "run_log.txt").write_text("\n".join(log_lines))
print(json.dumps({k: meas[k] for k in ("n_frames", "valid_share", "valid_share_with_box", "fail_hist", "infer_ms_per_frame", "fps_end_to_end", "wall_s_run_routes", "peak_rss_mb",
"spearman_vs_face_pack", "eye_class_order", "exit_code")}, indent=1, default=str))
return code, meas
def main():
import argparse
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--self-test", action="store_true")
ap.add_argument("--phase1", action="store_true")
ap.add_argument("--uid", default="")
ap.add_argument("--workers", type=int, default=3)
ap.add_argument("--max-minutes", type=float, default=90.0)
ap.add_argument("--out-dir", default=str(OUT_DIR))
ap.add_argument("--manifest", default=str(MANIFEST))
ap.add_argument("--force", action="store_true", help="recompute caches that already exist under --out-dir/cache")
ap.add_argument("--fd-mode", default="reshape", choices=["reshape", "default"], help="face-detector input handling (see module docstring)")
a = ap.parse_args()
if a.phase1:
code, _ = phase1(workers=min(a.workers, 3), max_minutes=a.max_minutes, out_dir=Path(a.out_dir), manifest=Path(a.manifest), force=a.force, fd_mode=a.fd_mode)
return code
self_test(a.uid or None, workers=min(a.workers, 2), fd_mode=a.fd_mode)
return 0
if __name__ == "__main__":
sys.exit(main())

Xet Storage Details

Size:
42.6 kB
·
Xet hash:
67b02807e01c6712bacea8052b4d094c88ee26487977f3af81d66643e6dce95a

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