feat(perception): add camera ego-motion evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-07-24 13:37:39 +03:00
parent 230cba4b21
commit 7fba39a629
11 changed files with 2348 additions and 0 deletions
@@ -13,6 +13,7 @@ from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
map_points_to_lidar,
project_map_points_kb4,
quaternion_xyzw_to_rotation_matrix,
unproject_pixels_kb4,
)
from k1link.device_plugins.xgrids_k1.analyze.stream_summary import (
DEFAULT_STREAM_SUMMARY_MAX_PAYLOAD_BYTES,
@@ -47,5 +48,6 @@ __all__ = [
"quaternion_xyzw_to_rotation_matrix",
"run_calibrated_overlay_experiment",
"summarize_mqtt_streams",
"unproject_pixels_kb4",
"validate_k1_valid_fov_mask",
]
@@ -205,6 +205,70 @@ def project_map_points_kb4(
)
def unproject_pixels_kb4(
pixels_xy: npt.ArrayLike,
*,
profile: Kb4ProjectionProfile,
) -> FloatArray:
"""Return unit camera-frame rays for finite pixels under the KB4 model."""
pixels = np.asarray(pixels_xy, dtype=np.float64)
if (
pixels.ndim != 2
or pixels.shape[1:] != (2,)
or not np.isfinite(pixels).all()
):
raise CalibratedProjectionError("camera pixels must have shape (N, 2)")
if pixels.size == 0:
return np.empty((0, 3), dtype=np.float64)
fx, fy, cx, cy = profile.intrinsic_fx_fy_cx_cy
if not all(math.isfinite(value) and value > 0.0 for value in (fx, fy)):
raise CalibratedProjectionError("camera focal lengths must be positive")
distorted_x = (pixels[:, 0] - cx) / fx
distorted_y = (pixels[:, 1] - cy) / fy
theta_distorted = np.hypot(distorted_x, distorted_y)
theta = theta_distorted.copy()
k1, k2, k3, k4 = profile.distortion_kb4
for _ in range(12):
squared = theta * theta
polynomial = (
1.0
+ k1 * squared
+ k2 * squared**2
+ k3 * squared**3
+ k4 * squared**4
)
derivative = (
1.0
+ 3.0 * k1 * squared
+ 5.0 * k2 * squared**2
+ 7.0 * k3 * squared**3
+ 9.0 * k4 * squared**4
)
if np.any(np.abs(derivative) < 1e-9):
raise CalibratedProjectionError("KB4 inverse derivative became singular")
theta -= (theta * polynomial - theta_distorted) / derivative
if not np.isfinite(theta).all() or np.any(theta < 0.0) or np.any(theta >= math.pi):
raise CalibratedProjectionError("KB4 inverse produced invalid angles")
scale = np.divide(
np.sin(theta),
theta_distorted,
out=np.ones_like(theta),
where=theta_distorted > 1e-12,
)
directions = np.column_stack(
(
distorted_x * scale,
distorted_y * scale,
np.cos(theta),
)
)
norms = np.linalg.norm(directions, axis=1)
if not np.isfinite(norms).all() or np.any(norms < 1e-9):
raise CalibratedProjectionError("KB4 inverse produced a zero camera ray")
return np.asarray(directions / norms[:, None], dtype=np.float64)
def depth_colors(depths_m: npt.ArrayLike) -> npt.NDArray[np.uint8]:
"""Return deterministic blue→cyan→green→yellow→red diagnostic colors."""
@@ -24,6 +24,7 @@ from k1link.compute import (
publish_e23_lab_instance,
publish_e24_lab_instance,
publish_e25_lab_instance,
publish_e26_lab_instance,
publish_integrated_lab_instance,
)
from k1link.device_plugins.xgrids_k1.analyze import (
@@ -743,6 +744,93 @@ def publish_e25_lab(
)
@lab_app.command("publish-e26")
def publish_e26_lab(
lidar_result: Annotated[
Path,
typer.Option(
"--lidar-result",
exists=True,
file_okay=False,
readable=True,
resolve_path=True,
help="Accepted E25 persistent-support result.",
),
],
camera_result: Annotated[
Path,
typer.Option(
"--camera-result",
exists=True,
file_okay=False,
readable=True,
resolve_path=True,
help="Accepted pre-world-motion result containing every 2D track.",
),
],
profile: Annotated[
Path,
typer.Option(
exists=True,
dir_okay=False,
readable=True,
resolve_path=True,
help="Bounded E26 camera/ego-motion profile.",
),
],
benchmark: Annotated[
Path,
typer.Option(
exists=True,
dir_okay=False,
readable=True,
resolve_path=True,
help="Spatially bound E26 motion benchmark.",
),
],
session_id: Annotated[
str,
typer.Option("--session-id", help="New immutable LAB session id."),
],
lab_id: Annotated[
str,
typer.Option("--lab-id", help="LAB marker, for example 'LAB E26.1'."),
],
display_name: Annotated[
str,
typer.Option("--display-name", help="Operator-facing saved-session title."),
],
) -> None:
"""Build and publish calibrated camera/ego-motion evidence."""
repository_root = Path(__file__).resolve().parents[4]
try:
published = publish_e26_lab_instance(
repository_root=repository_root,
lidar_result_root=lidar_result,
camera_result_root=camera_result,
profile_path=profile,
benchmark_path=benchmark,
lab_session_id=session_id,
lab_id=lab_id,
display_name=display_name,
)
except (OSError, SessionIntegrityError, RuntimeError, ValueError) as exc:
console.print(f"[red]E26 LAB publication failed:[/red] {exc}")
raise typer.Exit(code=2) from exc
metrics = published.build.report["metrics"]
console.print(
"[green]E26 LAB instance published.[/green] "
f"session={published.binding.session_id}; "
f"source={published.binding.source_session_id}; "
f"result={published.binding.result_id}; "
f"benchmark={metrics['benchmark']['passed_events']}/"
f"{metrics['benchmark']['total_events']}; "
f"p95={metrics['runtime']['camera_ego_motion_frame_processing_ms']['p95']:.3f}ms; "
"camera_metric_velocity=false; source_payloads_mutated=false"
)
@app.command("serve")
def serve_console(
port: Annotated[