"""Rotated coordinate system for angle-invariant trajectory encoding. All trajectories are normalised into a frame where: - start → (0, 0) - end → (1, 0) - lateral displacement is perpendicular to start→end axis This makes the model angle-invariant: a 45° diagonal move and a horizontal move look identical in the rotated frame (just "forward from 0 to 1"). """ from __future__ import annotations import math import numpy as np def encode_trajectory( points: np.ndarray, start: tuple[int, int], end: tuple[int, int], ) -> np.ndarray: """Transform pixel coordinates to rotated normalised frame. Args: points: (N, 2) array of (x, y) pixel coordinates. start: (x, y) start position. end: (x, y) end position. Returns: (N, 2) array of (forward, lateral) in normalised rotated frame. """ sx, sy = float(start[0]), float(start[1]) ex, ey = float(end[0]), float(end[1]) dist = math.hypot(ex - sx, ey - sy) if dist < 1e-8: return np.zeros_like(points) ux, uy = (ex - sx) / dist, (ey - sy) / dist vx, vy = -uy, ux dx = points[:, 0] - sx dy = points[:, 1] - sy forward = (dx * ux + dy * uy) / dist lateral = (dx * vx + dy * vy) / dist return np.stack([forward, lateral], axis=1) def decode_trajectory( normalised: np.ndarray, start: tuple[int, int], end: tuple[int, int], ) -> np.ndarray: """Transform rotated normalised frame back to pixel coordinates. Args: normalised: (N, 2) array of (forward, lateral). start: (x, y) start position. end: (x, y) end position. Returns: (N, 2) array of (x, y) pixel coordinates. """ sx, sy = float(start[0]), float(start[1]) ex, ey = float(end[0]), float(end[1]) dist = math.hypot(ex - sx, ey - sy) if dist < 1e-8: return np.full_like(normalised, [sx, sy]) ux, uy = (ex - sx) / dist, (ey - sy) / dist vx, vy = -uy, ux forward = normalised[:, 0] lateral = normalised[:, 1] px = sx + forward * dist * ux + lateral * dist * vx py = sy + forward * dist * uy + lateral * dist * vy return np.stack([px, py], axis=1)