jakies pierdoly
This commit is contained in:
1 parent
6a13b1ef04
commit
ae7dc34053
6 files changed
+136
-67
No files matched your search
@@ -109,14 +109,18 @@ Each run writes `results/gaze_exercises_<timestamp>.csv` plus a sidecar
|
||||
every recording with `--subject-id` and, when varying setup, `--session-tag`:
|
||||
|
||||
```powershell
|
||||
python gaze_exercises.py --subject-id you --session-tag laptop_50cm --screen-diagonal 15.6 --viewing-distance 50
|
||||
python gaze_exercises.py --subject-id you --session-tag monitor_90cm --screen-diagonal 27 --viewing-distance 90
|
||||
python gaze_exercises.py --list-monitors
|
||||
python gaze_exercises.py --subject-id you --session-tag laptop_50cm --monitor 1 --viewing-distance 50
|
||||
python gaze_exercises.py --subject-id you --session-tag monitor_90cm --monitor 2 --viewing-distance 90
|
||||
```
|
||||
|
||||
`--screen-diagonal` (inches) and `--viewing-distance` (cm) must match the
|
||||
physical setup for that recording — they drive the pixel-to-degree conversion
|
||||
used for both the logged error and velocity columns, so an incorrect value
|
||||
silently skews every derived feature in that session.
|
||||
`--monitor` picks which physical display to run on and auto-detects its
|
||||
diagonal; pass `--screen-diagonal` explicitly only if auto-detection reports
|
||||
`unknown size` for that display. `--viewing-distance` (cm) has no way to be
|
||||
auto-detected and must match the physical setup for that recording — it
|
||||
drives the pixel-to-degree conversion used for the logged error and velocity
|
||||
columns, so an incorrect value silently skews every derived feature in that
|
||||
session.
|
||||
|
||||
CSV columns beyond the basic error metrics:
|
||||
|
||||
@@ -162,6 +166,14 @@ Once more subjects are recorded, re-run training; group the split by
|
||||
`subject_id` (leave-one-subject-out) instead of by trial for the real paper
|
||||
evaluation.
|
||||
|
||||
`dispersion_px` and `accel_px_s2` in the raw CSV are in screen pixels, which
|
||||
are not comparable across recordings taken on different physical screens or
|
||||
viewing distances. Training re-derives both in visual-angle units
|
||||
(`dispersion_deg`, `accel_deg_s2`) using each session's `_meta.json`, matching
|
||||
what `adaptive_filter.py`'s `OnlineFeatureTracker` computes online — this
|
||||
matters as soon as you record on more than one screen/distance combo, which
|
||||
is exactly what varying `--monitor`/`--viewing-distance` across sessions does.
|
||||
|
||||
`adaptive_filter.py` defines the real-time filters built on that model:
|
||||
`BaselineFilter` (the existing fixed median+EMA filter), and
|
||||
`MLAdaptiveFilter` in `mode="hard"` (discrete fixation/saccade/pursuit
|
||||
@@ -177,7 +189,7 @@ reports RMS error, mean error, and on-target accuracy % for raw gaze, the
|
||||
fixed baseline filter, and both ML-adaptive modes side by side:
|
||||
|
||||
```powershell
|
||||
python eval_static_fixation.py --subject-id you --duration 60 --viewing-distance 70 --screen-diagonal 31.5
|
||||
python eval_static_fixation.py --subject-id you --duration 60 --viewing-distance 70 --monitor 1
|
||||
```
|
||||
|
||||
Press Space to start the 60-second hold. Results are saved to
|
||||
|
||||
+39
-17
@@ -21,13 +21,11 @@ import tkinter as tk
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from monitor_utils import format_monitor_list
|
||||
from show_apriltag_surface import SurfaceTagWindow, enable_windows_dpi_awareness, validate_args
|
||||
from visualize_surface_gaze import GazeSample, SurfaceReceiver
|
||||
from adaptive_filter import BaselineFilter, MLAdaptiveFilter
|
||||
|
||||
SCREEN_WIDTH = 2560
|
||||
SCREEN_HEIGHT = 1440
|
||||
SCREEN_DIAGONAL_IN = 31.5
|
||||
DEFAULT_VIEWING_DISTANCE_CM = 60.0
|
||||
TARGET_RADIUS = 40
|
||||
|
||||
@@ -42,14 +40,16 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--port", type=int, default=50020)
|
||||
parser.add_argument("--surface", default="screen")
|
||||
parser.add_argument("--confidence", type=float, default=0.9)
|
||||
parser.add_argument("--screen-diagonal", type=float, default=SCREEN_DIAGONAL_IN)
|
||||
parser.add_argument("--screen-diagonal", type=float, default=None,
|
||||
help="physical screen diagonal in inches "
|
||||
"(default: auto-detected from the selected monitor)")
|
||||
parser.add_argument("--viewing-distance", type=float, default=DEFAULT_VIEWING_DISTANCE_CM)
|
||||
parser.add_argument("--subject-id", default="anon")
|
||||
parser.add_argument("--session-tag", default="")
|
||||
parser.add_argument("--duration", type=float, default=60.0,
|
||||
help="seconds to hold the static target (default: 60)")
|
||||
parser.add_argument("--accuracy-radius", type=float, default=60.0,
|
||||
help="pixel radius counted as 'on target' for accuracy % (default: 60)")
|
||||
help="pixel radius counted as 'on target' for accuracy %% (default: 60)")
|
||||
parser.add_argument("--model", default="models/filter_model.joblib",
|
||||
help="path to trained classifier; omit ML columns if not found")
|
||||
parser.add_argument("--show", choices=METHODS, default="ml_soft",
|
||||
@@ -60,6 +60,11 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--margin", type=int, default=0)
|
||||
parser.add_argument("--background", default="#10141c")
|
||||
parser.add_argument("--windowed", action="store_true")
|
||||
parser.add_argument("--monitor", type=int, default=None,
|
||||
help="Windows display number to use, e.g. 2 for \\\\.\\DISPLAY2 "
|
||||
"(default: primary)")
|
||||
parser.add_argument("--list-monitors", action="store_true",
|
||||
help="print detected monitors (id, resolution, position, diagonal) and exit")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -67,16 +72,27 @@ class StaticFixationWindow(SurfaceTagWindow):
|
||||
def __init__(self, root: tk.Tk, args: argparse.Namespace) -> None:
|
||||
super().__init__(root, args)
|
||||
self.args = args
|
||||
self.width = self.monitor.width
|
||||
self.height = self.monitor.height
|
||||
if args.screen_diagonal is not None:
|
||||
self.screen_diagonal = args.screen_diagonal
|
||||
elif self.monitor.diagonal_in is not None:
|
||||
self.screen_diagonal = self.monitor.diagonal_in
|
||||
else:
|
||||
raise SystemExit(
|
||||
"error: could not auto-detect the diagonal for this monitor; "
|
||||
"pass --screen-diagonal explicitly"
|
||||
)
|
||||
self.messages: queue.Queue = queue.Queue(maxsize=256)
|
||||
self.stop_event = threading.Event()
|
||||
self.receiver = SurfaceReceiver(
|
||||
self.messages, self.stop_event, args.host, args.port, args.surface, args.confidence
|
||||
)
|
||||
self.receiver.start()
|
||||
self.target = (SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2)
|
||||
self.target = (self.width / 2, self.height / 2)
|
||||
|
||||
diagonal_px = math.hypot(SCREEN_WIDTH, SCREEN_HEIGHT)
|
||||
self.cm_per_px = args.screen_diagonal * 2.54 / diagonal_px
|
||||
diagonal_px = math.hypot(self.width, self.height)
|
||||
self.cm_per_px = self.screen_diagonal * 2.54 / diagonal_px
|
||||
|
||||
self.filters = {"raw": None, "baseline": BaselineFilter()}
|
||||
self.model_path = Path(args.model)
|
||||
@@ -125,10 +141,10 @@ class StaticFixationWindow(SurfaceTagWindow):
|
||||
return method == "raw" or method in self.filters
|
||||
|
||||
def sx(self, x: float) -> float:
|
||||
return x / SCREEN_WIDTH * self.canvas.winfo_width()
|
||||
return x / self.width * self.canvas.winfo_width()
|
||||
|
||||
def sy(self, y: float) -> float:
|
||||
return y / SCREEN_HEIGHT * self.canvas.winfo_height()
|
||||
return y / self.height * self.canvas.winfo_height()
|
||||
|
||||
def draw(self, event: tk.Event) -> None:
|
||||
super().draw(event)
|
||||
@@ -139,8 +155,8 @@ class StaticFixationWindow(SurfaceTagWindow):
|
||||
|
||||
def position_target(self) -> None:
|
||||
x, y = self.sx(self.target[0]), self.sy(self.target[1])
|
||||
rx = TARGET_RADIUS / SCREEN_WIDTH * self.canvas.winfo_width()
|
||||
ry = TARGET_RADIUS / SCREEN_HEIGHT * self.canvas.winfo_height()
|
||||
rx = TARGET_RADIUS / self.width * self.canvas.winfo_width()
|
||||
ry = TARGET_RADIUS / self.height * self.canvas.winfo_height()
|
||||
self.canvas.coords(self.target_item, x - rx, y - ry, x + rx, y + ry)
|
||||
self.canvas.itemconfigure(self.target_item, fill="#55aaff", outline="white", width=3, state="normal")
|
||||
|
||||
@@ -192,8 +208,8 @@ class StaticFixationWindow(SurfaceTagWindow):
|
||||
|
||||
def position_circle(self, item: int, point: tuple[float, float], radius: int) -> None:
|
||||
x, y = self.sx(point[0]), self.sy(point[1])
|
||||
rx = radius / SCREEN_WIDTH * self.canvas.winfo_width()
|
||||
ry = radius / SCREEN_HEIGHT * self.canvas.winfo_height()
|
||||
rx = radius / self.width * self.canvas.winfo_width()
|
||||
ry = radius / self.height * self.canvas.winfo_height()
|
||||
self.canvas.coords(item, x - rx, y - ry, x + rx, y + ry)
|
||||
|
||||
def tick(self) -> None:
|
||||
@@ -203,7 +219,7 @@ class StaticFixationWindow(SurfaceTagWindow):
|
||||
kind, value = self.messages.get_nowait()
|
||||
if kind == "gaze" and self.started_at is not None:
|
||||
sample: GazeSample = value # type: ignore[assignment]
|
||||
gaze = (sample.x_norm * SCREEN_WIDTH, (1.0 - sample.y_norm) * SCREEN_HEIGHT)
|
||||
gaze = (sample.x_norm * self.width, (1.0 - sample.y_norm) * self.height)
|
||||
self.record_sample(sample, gaze, now - self.started_at)
|
||||
except queue.Empty:
|
||||
pass
|
||||
@@ -240,7 +256,10 @@ class StaticFixationWindow(SurfaceTagWindow):
|
||||
"session_tag": self.args.session_tag,
|
||||
"recorded_at": datetime.now().isoformat(timespec="seconds"),
|
||||
"duration_s": self.args.duration,
|
||||
"screen_diagonal_in": self.args.screen_diagonal,
|
||||
"monitor_id": self.monitor.display_id,
|
||||
"screen_diagonal_in": self.screen_diagonal,
|
||||
"screen_width_px": self.width,
|
||||
"screen_height_px": self.height,
|
||||
"viewing_distance_cm": self.args.viewing_distance,
|
||||
"accuracy_radius_px": self.args.accuracy_radius,
|
||||
"model": str(self.model_path) if self.model_path.exists() else None,
|
||||
@@ -257,10 +276,13 @@ class StaticFixationWindow(SurfaceTagWindow):
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
if args.list_monitors:
|
||||
print(format_monitor_list())
|
||||
return
|
||||
validate_args(args)
|
||||
if not 0 <= args.confidence <= 1:
|
||||
raise SystemExit("error: --confidence must be between 0 and 1")
|
||||
if args.screen_diagonal <= 0:
|
||||
if args.screen_diagonal is not None and args.screen_diagonal <= 0:
|
||||
raise SystemExit("error: --screen-diagonal must be greater than 0")
|
||||
if args.viewing_distance <= 0:
|
||||
raise SystemExit("error: --viewing-distance must be greater than 0")
|
||||
|
||||
+36
-30
@@ -16,17 +16,29 @@ MAX_CONTINUOUS_GAP_S = 0.5
|
||||
|
||||
FEATURE_COLUMNS = [
|
||||
"velocity_deg_s",
|
||||
"accel_px_s2",
|
||||
"dispersion_px",
|
||||
"accel_deg_s2",
|
||||
"dispersion_deg",
|
||||
"confidence",
|
||||
"velocity_deg_s_mean",
|
||||
"velocity_deg_s_std",
|
||||
"dispersion_px_mean",
|
||||
"dispersion_deg_mean",
|
||||
]
|
||||
|
||||
EVENT_LABELS = ("fixation", "saccade", "pursuit")
|
||||
|
||||
|
||||
def px_to_deg(distance_px: float, cm_per_px: float, viewing_distance_cm: float) -> float:
|
||||
"""Convert a pixel distance to visual angle, given screen and viewing-distance calibration.
|
||||
|
||||
Every recording can be a different physical screen at a different
|
||||
distance, so any pixel-unit feature (dispersion, acceleration) is not
|
||||
comparable across sessions unless converted through this. velocity_deg_s
|
||||
already goes through the equivalent conversion at record time.
|
||||
"""
|
||||
distance_cm = distance_px * cm_per_px
|
||||
return math.degrees(2.0 * math.atan2(distance_cm / 2.0, viewing_distance_cm))
|
||||
|
||||
|
||||
def add_rolling_features(df, session_col: str = "session_id"):
|
||||
"""Add the *_mean / *_std rolling columns to a DataFrame in place, returning it.
|
||||
|
||||
@@ -35,11 +47,11 @@ def add_rolling_features(df, session_col: str = "session_id"):
|
||||
samples of a session usable (with a noisier estimate) instead of NaN.
|
||||
"""
|
||||
grouped = df.groupby(session_col, sort=False)
|
||||
roll = grouped["velocity_deg_s"].rolling(FEATURE_WINDOW, min_periods=1)
|
||||
df["velocity_deg_s_mean"] = roll.mean().reset_index(level=0, drop=True)
|
||||
df["velocity_deg_s_std"] = roll.std().reset_index(level=0, drop=True).fillna(0.0)
|
||||
df["dispersion_px_mean"] = (
|
||||
grouped["dispersion_px"].rolling(FEATURE_WINDOW, min_periods=1)
|
||||
velocity_roll = grouped["velocity_deg_s"].rolling(FEATURE_WINDOW, min_periods=1)
|
||||
df["velocity_deg_s_mean"] = velocity_roll.mean().reset_index(level=0, drop=True)
|
||||
df["velocity_deg_s_std"] = velocity_roll.std().reset_index(level=0, drop=True).fillna(0.0)
|
||||
df["dispersion_deg_mean"] = (
|
||||
grouped["dispersion_deg"].rolling(FEATURE_WINDOW, min_periods=1)
|
||||
.mean().reset_index(level=0, drop=True)
|
||||
)
|
||||
return df
|
||||
@@ -60,47 +72,41 @@ class OnlineFeatureTracker:
|
||||
self.velocity_history: deque[float] = deque(maxlen=FEATURE_WINDOW)
|
||||
self.dispersion_history: deque[float] = deque(maxlen=FEATURE_WINDOW)
|
||||
self.prev_sample: tuple[float, float, float] | None = None
|
||||
self.prev_velocity_px_s: float | None = None
|
||||
self.prev_velocity_deg_s: float | None = None
|
||||
|
||||
def reset(self) -> None:
|
||||
self.history.clear()
|
||||
self.velocity_history.clear()
|
||||
self.dispersion_history.clear()
|
||||
self.prev_sample = None
|
||||
self.prev_velocity_px_s = None
|
||||
self.prev_velocity_deg_s = None
|
||||
|
||||
def update(self, timestamp: float, x: float, y: float, confidence: float) -> dict | None:
|
||||
velocity_px_s = accel_px_s2 = None
|
||||
velocity_deg_s = accel_deg_s2 = None
|
||||
if self.prev_sample is not None:
|
||||
prev_t, prev_x, prev_y = self.prev_sample
|
||||
dt = timestamp - prev_t
|
||||
if 0 < dt <= MAX_CONTINUOUS_GAP_S:
|
||||
dist_px = math.dist((prev_x, prev_y), (x, y))
|
||||
velocity_px_s = dist_px / dt
|
||||
if self.prev_velocity_px_s is not None:
|
||||
accel_px_s2 = (velocity_px_s - self.prev_velocity_px_s) / dt
|
||||
velocity_px_s = math.dist((prev_x, prev_y), (x, y)) / dt
|
||||
velocity_deg_s = px_to_deg(velocity_px_s, self.cm_per_px, self.viewing_distance_cm)
|
||||
if self.prev_velocity_deg_s is not None:
|
||||
accel_deg_s2 = (velocity_deg_s - self.prev_velocity_deg_s) / dt
|
||||
self.prev_sample = (timestamp, x, y)
|
||||
self.prev_velocity_px_s = velocity_px_s
|
||||
|
||||
velocity_deg_s = None
|
||||
if velocity_px_s is not None:
|
||||
velocity_cm_s = velocity_px_s * self.cm_per_px
|
||||
velocity_deg_s = math.degrees(
|
||||
2.0 * math.atan2(velocity_cm_s / 2.0, self.viewing_distance_cm)
|
||||
)
|
||||
self.prev_velocity_deg_s = velocity_deg_s
|
||||
|
||||
self.history.append((x, y, timestamp))
|
||||
dispersion_px = None
|
||||
dispersion_deg = None
|
||||
if len(self.history) >= 2:
|
||||
xs = [p[0] for p in self.history]
|
||||
ys = [p[1] for p in self.history]
|
||||
dispersion_px = (max(xs) - min(xs)) + (max(ys) - min(ys))
|
||||
dispersion_deg = px_to_deg(dispersion_px, self.cm_per_px, self.viewing_distance_cm)
|
||||
|
||||
if velocity_deg_s is None or dispersion_px is None:
|
||||
if velocity_deg_s is None or dispersion_deg is None:
|
||||
return None
|
||||
|
||||
self.velocity_history.append(velocity_deg_s)
|
||||
self.dispersion_history.append(dispersion_px)
|
||||
self.dispersion_history.append(dispersion_deg)
|
||||
|
||||
n = len(self.velocity_history)
|
||||
velocity_deg_s_mean = sum(self.velocity_history) / n
|
||||
@@ -109,14 +115,14 @@ class OnlineFeatureTracker:
|
||||
velocity_deg_s_std = math.sqrt(variance)
|
||||
else:
|
||||
velocity_deg_s_std = 0.0
|
||||
dispersion_px_mean = sum(self.dispersion_history) / len(self.dispersion_history)
|
||||
dispersion_deg_mean = sum(self.dispersion_history) / len(self.dispersion_history)
|
||||
|
||||
return {
|
||||
"velocity_deg_s": velocity_deg_s,
|
||||
"accel_px_s2": accel_px_s2 if accel_px_s2 is not None else 0.0,
|
||||
"dispersion_px": dispersion_px,
|
||||
"accel_deg_s2": accel_deg_s2 if accel_deg_s2 is not None else 0.0,
|
||||
"dispersion_deg": dispersion_deg,
|
||||
"confidence": confidence,
|
||||
"velocity_deg_s_mean": velocity_deg_s_mean,
|
||||
"velocity_deg_s_std": velocity_deg_s_std,
|
||||
"dispersion_px_mean": dispersion_px_mean,
|
||||
"dispersion_deg_mean": dispersion_deg_mean,
|
||||
}
|
||||
Binary file not shown.
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"feature_columns": [
|
||||
"velocity_deg_s",
|
||||
"accel_px_s2",
|
||||
"dispersion_px",
|
||||
"accel_deg_s2",
|
||||
"dispersion_deg",
|
||||
"confidence",
|
||||
"velocity_deg_s_mean",
|
||||
"velocity_deg_s_std",
|
||||
"dispersion_px_mean"
|
||||
"dispersion_deg_mean"
|
||||
],
|
||||
"event_labels": [
|
||||
"fixation",
|
||||
@@ -15,8 +15,10 @@
|
||||
],
|
||||
"baseline_velocity_threshold_deg_s": 30.0,
|
||||
"trained_on_sessions": [
|
||||
"PS_20260812_092731"
|
||||
"PS_20260812_092731",
|
||||
"PS_20260812_163039",
|
||||
"PS_20260812_163846"
|
||||
],
|
||||
"train_rows": 48979,
|
||||
"test_rows": 17636
|
||||
"train_rows": 160867,
|
||||
"test_rows": 52233
|
||||
}
|
||||
+34
-7
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import joblib
|
||||
@@ -19,7 +20,7 @@ from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.metrics import classification_report, confusion_matrix
|
||||
from sklearn.model_selection import GroupShuffleSplit
|
||||
|
||||
from gaze_features import EVENT_LABELS, FEATURE_COLUMNS, add_rolling_features
|
||||
from gaze_features import EVENT_LABELS, FEATURE_COLUMNS, add_rolling_features, MAX_CONTINUOUS_GAP_S
|
||||
|
||||
BASELINE_VELOCITY_THRESHOLD_DEG_S = 30.0
|
||||
|
||||
@@ -34,7 +35,7 @@ def parse_args() -> argparse.Namespace:
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_dataset(results_dir: Path) -> pd.DataFrame:
|
||||
def load_dataset(results_dir: Path) -> tuple[pd.DataFrame, dict[str, dict]]:
|
||||
csv_paths = sorted(p for p in results_dir.glob("gaze_exercises_*.csv"))
|
||||
if not csv_paths:
|
||||
raise SystemExit(f"error: no gaze_exercises_*.csv files found in {results_dir}")
|
||||
@@ -43,12 +44,38 @@ def load_dataset(results_dir: Path) -> pd.DataFrame:
|
||||
print(f"Loaded {len(df)} rows from {len(csv_paths)} recording(s): "
|
||||
f"{', '.join(p.name for p in csv_paths)}")
|
||||
print("Sessions:", ", ".join(sorted(df['session_id'].unique())))
|
||||
return df
|
||||
|
||||
session_meta: dict[str, dict] = {}
|
||||
for csv_path in csv_paths:
|
||||
meta_path = csv_path.with_name(csv_path.stem + "_meta.json")
|
||||
if not meta_path.exists():
|
||||
raise SystemExit(f"error: missing {meta_path.name} (needed for screen-size calibration)")
|
||||
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
diagonal_px = math.hypot(meta["screen_width_px"], meta["screen_height_px"])
|
||||
session_meta[meta["session_id"]] = {
|
||||
"cm_per_px": meta["screen_diagonal_in"] * 2.54 / diagonal_px,
|
||||
"viewing_distance_cm": meta["viewing_distance_cm"],
|
||||
}
|
||||
return df, session_meta
|
||||
|
||||
|
||||
def prepare_features(df: pd.DataFrame) -> pd.DataFrame:
|
||||
def prepare_features(df: pd.DataFrame, session_meta: dict[str, dict]) -> pd.DataFrame:
|
||||
df = df.sort_values(["session_id", "pupil_timestamp"]).reset_index(drop=True)
|
||||
df["accel_px_s2"] = df["accel_px_s2"].fillna(0.0)
|
||||
|
||||
# dispersion_px and accel_px_s2 are in raw screen pixels, which are not
|
||||
# comparable across recordings taken on different screens/distances (see
|
||||
# session_meta). Re-derive both in visual-angle units, matching what
|
||||
# OnlineFeatureTracker computes at inference time (gaze_features.py).
|
||||
cm_per_px = df["session_id"].map(lambda s: session_meta[s]["cm_per_px"])
|
||||
viewing_distance_cm = df["session_id"].map(lambda s: session_meta[s]["viewing_distance_cm"])
|
||||
dispersion_cm = df["dispersion_px"] * cm_per_px
|
||||
df["dispersion_deg"] = np.degrees(2.0 * np.arctan2(dispersion_cm / 2.0, viewing_distance_cm))
|
||||
|
||||
dt = df.groupby("session_id")["pupil_timestamp"].diff()
|
||||
accel_deg_s2 = df.groupby("session_id")["velocity_deg_s"].diff() / dt
|
||||
accel_deg_s2[(dt <= 0) | (dt > MAX_CONTINUOUS_GAP_S)] = np.nan
|
||||
df["accel_deg_s2"] = accel_deg_s2.fillna(0.0)
|
||||
|
||||
df = add_rolling_features(df)
|
||||
before = len(df)
|
||||
df = df.dropna(subset=FEATURE_COLUMNS + ["event_label"]).reset_index(drop=True)
|
||||
@@ -111,8 +138,8 @@ def main() -> None:
|
||||
model_dir = Path(args.model_dir)
|
||||
model_dir.mkdir(exist_ok=True)
|
||||
|
||||
df = load_dataset(results_dir)
|
||||
df = prepare_features(df)
|
||||
df, session_meta = load_dataset(results_dir)
|
||||
df = prepare_features(df, session_meta)
|
||||
train_df, test_df = trial_level_split(df, args.test_size, args.random_state)
|
||||
|
||||
X_train, y_train = train_df[FEATURE_COLUMNS], train_df["event_label"]
|
||||
|
||||
Reference in new issue
Block a user