78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
"""Letterbox resize + curve sampling utilities.
|
|
|
|
Camera-agnostic design note: images of any source resolution/aspect ratio are
|
|
letterboxed (aspect-preserving resize + pad) into a fixed network input size, and all
|
|
lane coordinates are normalized to [0,1] within that canonical frame. `LetterboxTransform`
|
|
carries enough state to map predictions back to original-image pixel coordinates.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
import numpy as np
|
|
|
|
|
|
@dataclass
|
|
class LetterboxTransform:
|
|
scale: float
|
|
pad_x: float
|
|
pad_y: float
|
|
out_w: int
|
|
out_h: int
|
|
src_w: int
|
|
src_h: int
|
|
|
|
def apply_points(self, points: np.ndarray) -> np.ndarray:
|
|
"""points: (N,2) array of (x,y) in source-image pixel coords -> letterboxed pixel coords."""
|
|
out = points.copy().astype(np.float32)
|
|
out[:, 0] = out[:, 0] * self.scale + self.pad_x
|
|
out[:, 1] = out[:, 1] * self.scale + self.pad_y
|
|
return out
|
|
|
|
def invert_points(self, points: np.ndarray) -> np.ndarray:
|
|
"""Inverse of apply_points: letterboxed pixel coords -> source-image pixel coords."""
|
|
out = points.copy().astype(np.float32)
|
|
out[:, 0] = (out[:, 0] - self.pad_x) / self.scale
|
|
out[:, 1] = (out[:, 1] - self.pad_y) / self.scale
|
|
return out
|
|
|
|
|
|
def compute_letterbox(src_w: int, src_h: int, out_w: int, out_h: int) -> LetterboxTransform:
|
|
scale = min(out_w / src_w, out_h / src_h)
|
|
new_w, new_h = src_w * scale, src_h * scale
|
|
pad_x = (out_w - new_w) / 2.0
|
|
pad_y = (out_h - new_h) / 2.0
|
|
return LetterboxTransform(scale=scale, pad_x=pad_x, pad_y=pad_y,
|
|
out_w=out_w, out_h=out_h, src_w=src_w, src_h=src_h)
|
|
|
|
|
|
def sample_lane_at_ys(lane_points: list[tuple[float, float]], sample_ys: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
"""Linearly interpolate a polyline's x(y) at the given y-values.
|
|
|
|
Returns (x_values, valid_mask) — valid_mask is False outside the polyline's
|
|
observed y-range (no extrapolation).
|
|
"""
|
|
pts = np.array(sorted(lane_points, key=lambda p: p[1]), dtype=np.float32)
|
|
ys_src = pts[:, 1]
|
|
xs_src = pts[:, 0]
|
|
|
|
y_min, y_max = ys_src.min(), ys_src.max()
|
|
valid = (sample_ys >= y_min) & (sample_ys <= y_max)
|
|
|
|
xs_out = np.zeros_like(sample_ys, dtype=np.float32)
|
|
if valid.any():
|
|
xs_out[valid] = np.interp(sample_ys[valid], ys_src, xs_src)
|
|
return xs_out, valid
|
|
|
|
|
|
def polyfit_cubic(lane_points: list[tuple[float, float]]) -> np.ndarray:
|
|
"""Least-squares cubic fit x = k*y^3 + m*y^2 + n*y + b (normalized coords expected).
|
|
|
|
Used only as a reference/visualization utility, not inside the training loop
|
|
(the model regresses these coefficients directly).
|
|
"""
|
|
pts = np.array(lane_points, dtype=np.float64)
|
|
ys, xs = pts[:, 1], pts[:, 0]
|
|
coeffs = np.polyfit(ys, xs, deg=3) # k, m, n, b
|
|
return coeffs.astype(np.float32)
|