refactor(lab): canonicalize recorded spatial replay
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
RecordedFmp4Player,
|
||||
type RecordedObservationPlayback,
|
||||
} from "../RecordedFmp4Player";
|
||||
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
||||
import type { RecordedCameraAdmissionState } from "../../core/observation/recordedSessionAdmission";
|
||||
import {
|
||||
RecordedEvidenceBoxOverlay,
|
||||
type RecordedEvidenceBox,
|
||||
@@ -33,6 +36,7 @@ export function RecordedEvidenceVideoScene({
|
||||
segmentCount,
|
||||
onPlaybackChange,
|
||||
onPlayingRejected,
|
||||
onAdmissionChange,
|
||||
playbackAuthority = "media",
|
||||
playbackTransport = "segmented",
|
||||
}: {
|
||||
@@ -49,9 +53,22 @@ export function RecordedEvidenceVideoScene({
|
||||
segmentCount?: number;
|
||||
onPlaybackChange?: (playback: RecordedObservationPlayback) => void;
|
||||
onPlayingRejected?: () => void;
|
||||
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
|
||||
playbackAuthority?: "media" | "host";
|
||||
playbackTransport?: "segmented" | "epoch-stream";
|
||||
}) {
|
||||
const generation = source.delivery?.kind === "recorded-fmp4-manifest"
|
||||
? source.delivery.manifestGenerationSha256
|
||||
: "invalid";
|
||||
const [admissionPhase, setAdmissionPhase] = useState<RecordedCameraAdmissionState["phase"]>(
|
||||
"loading",
|
||||
);
|
||||
useEffect(() => setAdmissionPhase("loading"), [generation, source.id]);
|
||||
const handleAdmissionChange = (next: RecordedCameraAdmissionState) => {
|
||||
setAdmissionPhase(next.phase);
|
||||
onAdmissionChange?.(next);
|
||||
};
|
||||
const sourceReady = admissionPhase === "ready";
|
||||
return (
|
||||
<div className="recorded-evidence-video-scene">
|
||||
<RecordedFmp4Player
|
||||
@@ -63,29 +80,32 @@ export function RecordedEvidenceVideoScene({
|
||||
segmentCount={segmentCount}
|
||||
onPlaybackChange={onPlaybackChange}
|
||||
onPlayingRejected={onPlayingRejected}
|
||||
onAdmissionChange={handleAdmissionChange}
|
||||
playbackAuthority={playbackAuthority}
|
||||
playbackTransport={playbackTransport}
|
||||
/>
|
||||
{semanticOverlay ? (
|
||||
{sourceReady && semanticOverlay ? (
|
||||
<RecordedEvidenceSemanticMaskOverlay
|
||||
{...semanticOverlay}
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
/>
|
||||
) : null}
|
||||
{pointCloudOverlay ? (
|
||||
{sourceReady && pointCloudOverlay ? (
|
||||
<RecordedEvidencePointCloudOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
overlay={pointCloudOverlay}
|
||||
/>
|
||||
) : null}
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
boxes={boxes}
|
||||
ariaLabel={ariaLabel}
|
||||
/>
|
||||
{sourceReady ? (
|
||||
<RecordedEvidenceBoxOverlay
|
||||
imageWidth={imageWidth}
|
||||
imageHeight={imageHeight}
|
||||
boxes={boxes}
|
||||
ariaLabel={ariaLabel}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
fetchCanonicalRecordedLabSpatialFrame,
|
||||
type CanonicalRecordedLabSpatialFrame,
|
||||
} from "../../core/laboratory/canonicalRecordedLabSpatial";
|
||||
|
||||
const FRAME_CACHE_LIMIT = 12;
|
||||
|
||||
/**
|
||||
* Shared latest-request-wins scheduler for recorded LAB spatial evidence.
|
||||
*
|
||||
* A feature supplies only the sealed session identity and host-clock time.
|
||||
* Cache ownership, identity fencing and stale-response suppression remain in
|
||||
* the canonical instrument instead of being reimplemented per experiment.
|
||||
*/
|
||||
export function useCanonicalRecordedLabSpatialFrame({
|
||||
sessionId,
|
||||
generationSha256,
|
||||
targetTimeNs,
|
||||
}: {
|
||||
sessionId: string;
|
||||
generationSha256: string | null;
|
||||
targetTimeNs: number;
|
||||
}) {
|
||||
const [frame, setFrame] = useState<CanonicalRecordedLabSpatialFrame | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const desiredRef = useRef<number | null>(null);
|
||||
const runningRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const identityRef = useRef("");
|
||||
const cacheRef = useRef(new Map<number, CanonicalRecordedLabSpatialFrame>());
|
||||
const pumpRef = useRef<() => void>(() => undefined);
|
||||
const identity = `${sessionId}:${generationSha256 ?? "unavailable"}`;
|
||||
identityRef.current = identity;
|
||||
|
||||
pumpRef.current = () => {
|
||||
if (runningRef.current || desiredRef.current === null || !generationSha256) return;
|
||||
runningRef.current = true;
|
||||
const requestIdentity = identity;
|
||||
let settledTimeNs: number | null = null;
|
||||
void (async () => {
|
||||
while (
|
||||
mountedRef.current
|
||||
&& identityRef.current === requestIdentity
|
||||
&& desiredRef.current !== null
|
||||
) {
|
||||
const requestedTimeNs = desiredRef.current;
|
||||
const cached = cacheRef.current.get(requestedTimeNs);
|
||||
try {
|
||||
const next = cached ?? await fetchCanonicalRecordedLabSpatialFrame(
|
||||
sessionId,
|
||||
generationSha256,
|
||||
requestedTimeNs,
|
||||
);
|
||||
if (!mountedRef.current || identityRef.current !== requestIdentity) break;
|
||||
if (!cached) {
|
||||
cacheRef.current.set(requestedTimeNs, next);
|
||||
while (cacheRef.current.size > FRAME_CACHE_LIMIT) {
|
||||
const oldest = cacheRef.current.keys().next().value as number | undefined;
|
||||
if (oldest === undefined) break;
|
||||
cacheRef.current.delete(oldest);
|
||||
}
|
||||
}
|
||||
setFrame(next);
|
||||
setError(null);
|
||||
} catch (caught: unknown) {
|
||||
if (!mountedRef.current || identityRef.current !== requestIdentity) break;
|
||||
setError(caught instanceof Error
|
||||
? caught.message
|
||||
: "Spatial-слои записанной LAB недоступны.");
|
||||
}
|
||||
settledTimeNs = requestedTimeNs;
|
||||
if (desiredRef.current === requestedTimeNs) break;
|
||||
}
|
||||
})().finally(() => {
|
||||
runningRef.current = false;
|
||||
if (
|
||||
mountedRef.current
|
||||
&& desiredRef.current !== null
|
||||
&& (identityRef.current !== requestIdentity || desiredRef.current !== settledTimeNs)
|
||||
) {
|
||||
pumpRef.current();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
desiredRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
cacheRef.current.clear();
|
||||
desiredRef.current = null;
|
||||
setFrame(null);
|
||||
setError(null);
|
||||
}, [identity]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!generationSha256) return;
|
||||
desiredRef.current = targetTimeNs;
|
||||
const cached = cacheRef.current.get(targetTimeNs);
|
||||
if (cached) {
|
||||
setFrame(cached);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
pumpRef.current();
|
||||
}, [generationSha256, identity, targetTimeNs]);
|
||||
|
||||
return { frame, error, loading: Boolean(generationSha256) && !frame && !error };
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
export const CANONICAL_RECORDED_LAB_TGS_HISTORY_SECONDS = 1;
|
||||
export const CANONICAL_RECORDED_LAB_SPATIAL_PROFILE = "source-paced-ground-v2";
|
||||
|
||||
export interface CanonicalRecordedLabPackedCellEvidence {
|
||||
centersBodyXyM: Float32Array;
|
||||
zBoundsM: Float32Array;
|
||||
stateCodes: Uint8Array;
|
||||
}
|
||||
|
||||
export interface CanonicalRecordedLabBodyGroundFrame {
|
||||
originMapXyzM: readonly [number, number, number];
|
||||
sensorOriginMapXyzM: readonly [number, number, number];
|
||||
basisMapFromBody: readonly [
|
||||
readonly [number, number, number],
|
||||
readonly [number, number, number],
|
||||
readonly [number, number, number],
|
||||
];
|
||||
}
|
||||
|
||||
export interface CanonicalRecordedLabTgsCostmap {
|
||||
centersXyM: readonly (readonly [number, number])[];
|
||||
stateCodes: readonly number[];
|
||||
zBoundsM: readonly (readonly [number | null, number | null])[];
|
||||
}
|
||||
|
||||
export function canonicalRecordedLabTgsIsCurrent(
|
||||
currentTimeNs: number,
|
||||
anchorTimeNs: number,
|
||||
historySeconds = CANONICAL_RECORDED_LAB_TGS_HISTORY_SECONDS,
|
||||
): boolean {
|
||||
if (
|
||||
!Number.isSafeInteger(currentTimeNs)
|
||||
|| !Number.isSafeInteger(anchorTimeNs)
|
||||
|| !Number.isFinite(historySeconds)
|
||||
|| historySeconds <= 0
|
||||
) return false;
|
||||
const ageNs = currentTimeNs - anchorTimeNs;
|
||||
return ageNs >= 0 && ageNs <= Math.round(historySeconds * 1_000_000_000);
|
||||
}
|
||||
|
||||
export function canonicalMapGravityLocalPointToBodyGround(
|
||||
point: readonly [number, number, number],
|
||||
anchor: CanonicalRecordedLabBodyGroundFrame,
|
||||
current: CanonicalRecordedLabBodyGroundFrame,
|
||||
): readonly [number, number, number] {
|
||||
// TGS is translation-only map-gravity-local: its axes are map axes and its
|
||||
// origin is the LiDAR at the source frame. It is not an anchor body frame.
|
||||
const map: readonly [number, number, number] = [
|
||||
anchor.sensorOriginMapXyzM[0] + point[0],
|
||||
anchor.sensorOriginMapXyzM[1] + point[1],
|
||||
anchor.sensorOriginMapXyzM[2] + point[2],
|
||||
];
|
||||
const delta: readonly [number, number, number] = [
|
||||
map[0] - current.originMapXyzM[0],
|
||||
map[1] - current.originMapXyzM[1],
|
||||
map[2] - current.originMapXyzM[2],
|
||||
];
|
||||
return [
|
||||
current.basisMapFromBody[0][0] * delta[0]
|
||||
+ current.basisMapFromBody[1][0] * delta[1]
|
||||
+ current.basisMapFromBody[2][0] * delta[2],
|
||||
current.basisMapFromBody[0][1] * delta[0]
|
||||
+ current.basisMapFromBody[1][1] * delta[1]
|
||||
+ current.basisMapFromBody[2][1] * delta[2],
|
||||
current.basisMapFromBody[0][2] * delta[0]
|
||||
+ current.basisMapFromBody[1][2] * delta[1]
|
||||
+ current.basisMapFromBody[2][2] * delta[2],
|
||||
];
|
||||
}
|
||||
|
||||
export function canonicalRecordedLabPackedTgsCells(
|
||||
costmap: CanonicalRecordedLabTgsCostmap,
|
||||
anchor: CanonicalRecordedLabBodyGroundFrame,
|
||||
current: CanonicalRecordedLabBodyGroundFrame,
|
||||
): CanonicalRecordedLabPackedCellEvidence {
|
||||
if (
|
||||
costmap.centersXyM.length !== costmap.stateCodes.length
|
||||
|| costmap.centersXyM.length !== costmap.zBoundsM.length
|
||||
) throw new Error("Canonical recorded LAB TGS accounting changed");
|
||||
const centers: number[] = [];
|
||||
const zBounds: number[] = [];
|
||||
costmap.centersXyM.forEach(([x, y], index) => {
|
||||
const bounds = costmap.zBoundsM[index] ?? [null, null];
|
||||
const center = canonicalMapGravityLocalPointToBodyGround([x, y, 0], anchor, current);
|
||||
centers.push(center[0], center[1]);
|
||||
if (bounds[0] === null || bounds[1] === null) {
|
||||
zBounds.push(Number.NaN, Number.NaN);
|
||||
return;
|
||||
}
|
||||
const bottom = canonicalMapGravityLocalPointToBodyGround([x, y, bounds[0]], anchor, current);
|
||||
const top = canonicalMapGravityLocalPointToBodyGround([x, y, bounds[1]], anchor, current);
|
||||
zBounds.push(Math.min(bottom[2], top[2]), Math.max(bottom[2], top[2]));
|
||||
});
|
||||
return {
|
||||
centersBodyXyM: Float32Array.from(centers),
|
||||
zBoundsM: Float32Array.from(zBounds),
|
||||
stateCodes: Uint8Array.from(costmap.stateCodes),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import type { LaboratoryFetch } from "./advancedResults";
|
||||
import { CANONICAL_RECORDED_LAB_SPATIAL_PROFILE } from "./canonicalRecordedLab";
|
||||
|
||||
const SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
|
||||
export class CanonicalRecordedLabSpatialContractError extends Error {}
|
||||
|
||||
export interface CanonicalRecordedLabSpatialFrame {
|
||||
targetTimeNs: number;
|
||||
sourceTimeNs: number;
|
||||
poseTimeNs: number;
|
||||
trajectoryTimeNs: number;
|
||||
sourcePointCount: number;
|
||||
coordinateFrame: "body-ground";
|
||||
sensorHeight: {
|
||||
meters: number;
|
||||
source: "initial-source-cloud-lower-quantile-median";
|
||||
sampleCount: number;
|
||||
madM: number;
|
||||
authority: "visual-derived";
|
||||
};
|
||||
spatialProfile: {
|
||||
profileId: typeof CANONICAL_RECORDED_LAB_SPATIAL_PROFILE;
|
||||
localSlamHistorySeconds: number;
|
||||
localSlamRadiusM: number;
|
||||
localSlamVoxelSizeM: number;
|
||||
localSlamPointLimit: number;
|
||||
};
|
||||
bodyFrame: {
|
||||
originMapXyzM: readonly [number, number, number];
|
||||
sensorOriginMapXyzM: readonly [number, number, number];
|
||||
basisMapFromBody: readonly [
|
||||
readonly [number, number, number],
|
||||
readonly [number, number, number],
|
||||
readonly [number, number, number],
|
||||
];
|
||||
};
|
||||
sourcePointsBodyXyzM: readonly (readonly [number, number, number])[];
|
||||
localSlamSourceFrameCount: number;
|
||||
localSlamSourcePointCount: number;
|
||||
localSlamBodyXyzM: readonly (readonly [number, number, number])[];
|
||||
}
|
||||
|
||||
function objectValue(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидался объект.`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function arrayValue(value: unknown, label: string): unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидался массив.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function exact(value: unknown, expected: unknown, label: string): void {
|
||||
if (value !== expected) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(`${label}: контракт изменён.`);
|
||||
}
|
||||
}
|
||||
|
||||
function numberValue(value: unknown, label: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидалось число.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integerValue(value: unknown, label: string): number {
|
||||
const parsed = numberValue(value, label);
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(`${label}: ожидалось целое значение.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function pointList(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): readonly (readonly [number, number, number])[] {
|
||||
return arrayValue(value, label).map((entry, index) => {
|
||||
const point = arrayValue(entry, `${label}[${index}]`).map(
|
||||
(channel, channelIndex) => numberValue(channel, `${label}[${index}][${channelIndex}]`),
|
||||
);
|
||||
if (point.length !== 3) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(`${label}[${index}]: размер изменён.`);
|
||||
}
|
||||
return [point[0]!, point[1]!, point[2]!] as const;
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchCanonicalRecordedLabSpatialFrame(
|
||||
sessionId: string,
|
||||
generationSha256: string,
|
||||
targetTimeNs: number,
|
||||
{
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||
): Promise<CanonicalRecordedLabSpatialFrame> {
|
||||
if (
|
||||
!SAFE_SESSION_ID.test(sessionId)
|
||||
|| !SHA256.test(generationSha256)
|
||||
|| !Number.isSafeInteger(targetTimeNs)
|
||||
|| targetTimeNs < 0
|
||||
) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(
|
||||
"Canonical LAB spatial identity недопустима.",
|
||||
);
|
||||
}
|
||||
const query = new URLSearchParams({
|
||||
generation: generationSha256,
|
||||
time_ns: String(targetTimeNs),
|
||||
profile: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
|
||||
});
|
||||
const response = await fetcher(
|
||||
`/api/v1/observation-sessions/${encodeURIComponent(sessionId)}`
|
||||
+ `/canonical-lab/spatial-frame?${query.toString()}`,
|
||||
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(
|
||||
`Canonical LAB spatial frame недоступен: HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
const payload = objectValue(await response.json(), "canonical_lab.spatial_frame");
|
||||
exact(
|
||||
payload.schema_version,
|
||||
"missioncore.canonical-recorded-lab-spatial-frame/v2",
|
||||
"canonical_lab.spatial_frame.schema_version",
|
||||
);
|
||||
exact(payload.coordinate_frame, "body-ground", "canonical_lab.spatial_frame.coordinate_frame");
|
||||
exact(payload.target_time_ns, targetTimeNs, "canonical_lab.spatial_frame.target_time_ns");
|
||||
const sourcePoints = pointList(
|
||||
payload.source_points_body_xyz_m,
|
||||
"canonical_lab.spatial_frame.source_points",
|
||||
);
|
||||
const localSlam = pointList(
|
||||
payload.local_slam_body_xyz_m,
|
||||
"canonical_lab.spatial_frame.local_slam",
|
||||
);
|
||||
const sourcePointCount = integerValue(
|
||||
payload.source_point_count,
|
||||
"canonical_lab.spatial_frame.source_point_count",
|
||||
);
|
||||
const localSlamPointCount = integerValue(
|
||||
payload.local_slam_point_count,
|
||||
"canonical_lab.spatial_frame.local_slam_point_count",
|
||||
);
|
||||
if (
|
||||
sourcePointCount !== sourcePoints.length
|
||||
|| sourcePointCount > 100_000
|
||||
|| localSlamPointCount !== localSlam.length
|
||||
|| localSlam.length > 27_000
|
||||
) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(
|
||||
"Canonical LAB spatial accounting изменён.",
|
||||
);
|
||||
}
|
||||
const bodyFrame = objectValue(payload.body_frame, "canonical_lab.spatial_frame.body_frame");
|
||||
const origin = pointList(
|
||||
[bodyFrame.origin_map_xyz_m],
|
||||
"canonical_lab.spatial_frame.body_frame.origin",
|
||||
)[0]!;
|
||||
const sensorOrigin = pointList(
|
||||
[bodyFrame.sensor_origin_map_xyz_m],
|
||||
"canonical_lab.spatial_frame.body_frame.sensor_origin",
|
||||
)[0]!;
|
||||
const basisRows = pointList(
|
||||
bodyFrame.basis_map_from_body,
|
||||
"canonical_lab.spatial_frame.body_frame.basis",
|
||||
);
|
||||
if (basisRows.length !== 3) {
|
||||
throw new CanonicalRecordedLabSpatialContractError(
|
||||
"Canonical LAB spatial basis изменён.",
|
||||
);
|
||||
}
|
||||
const sensorHeight = objectValue(payload.sensor_height, "canonical_lab.spatial_frame.sensor_height");
|
||||
exact(
|
||||
sensorHeight.source,
|
||||
"initial-source-cloud-lower-quantile-median",
|
||||
"canonical_lab.spatial_frame.sensor_height.source",
|
||||
);
|
||||
exact(
|
||||
sensorHeight.authority,
|
||||
"visual-derived",
|
||||
"canonical_lab.spatial_frame.sensor_height.authority",
|
||||
);
|
||||
const spatialProfile = objectValue(
|
||||
payload.spatial_profile,
|
||||
"canonical_lab.spatial_frame.spatial_profile",
|
||||
);
|
||||
exact(
|
||||
spatialProfile.profile_id,
|
||||
CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
|
||||
"canonical_lab.spatial_frame.spatial_profile.profile_id",
|
||||
);
|
||||
return {
|
||||
targetTimeNs,
|
||||
sourceTimeNs: integerValue(payload.source_time_ns, "canonical_lab.spatial_frame.source_time_ns"),
|
||||
poseTimeNs: integerValue(payload.pose_time_ns, "canonical_lab.spatial_frame.pose_time_ns"),
|
||||
trajectoryTimeNs: integerValue(
|
||||
payload.trajectory_time_ns,
|
||||
"canonical_lab.spatial_frame.trajectory_time_ns",
|
||||
),
|
||||
sourcePointCount,
|
||||
coordinateFrame: "body-ground",
|
||||
sensorHeight: {
|
||||
meters: numberValue(sensorHeight.meters, "canonical_lab.spatial_frame.sensor_height.meters"),
|
||||
source: "initial-source-cloud-lower-quantile-median",
|
||||
sampleCount: integerValue(
|
||||
sensorHeight.sample_count,
|
||||
"canonical_lab.spatial_frame.sensor_height.sample_count",
|
||||
),
|
||||
madM: numberValue(sensorHeight.mad_m, "canonical_lab.spatial_frame.sensor_height.mad_m"),
|
||||
authority: "visual-derived",
|
||||
},
|
||||
spatialProfile: {
|
||||
profileId: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE,
|
||||
localSlamHistorySeconds: numberValue(
|
||||
spatialProfile.local_slam_history_seconds,
|
||||
"canonical_lab.spatial_frame.spatial_profile.history",
|
||||
),
|
||||
localSlamRadiusM: numberValue(
|
||||
spatialProfile.local_slam_radius_m,
|
||||
"canonical_lab.spatial_frame.spatial_profile.radius",
|
||||
),
|
||||
localSlamVoxelSizeM: numberValue(
|
||||
spatialProfile.local_slam_voxel_size_m,
|
||||
"canonical_lab.spatial_frame.spatial_profile.voxel",
|
||||
),
|
||||
localSlamPointLimit: integerValue(
|
||||
spatialProfile.local_slam_point_limit,
|
||||
"canonical_lab.spatial_frame.spatial_profile.limit",
|
||||
),
|
||||
},
|
||||
bodyFrame: {
|
||||
originMapXyzM: origin,
|
||||
sensorOriginMapXyzM: sensorOrigin,
|
||||
basisMapFromBody: [basisRows[0]!, basisRows[1]!, basisRows[2]!],
|
||||
},
|
||||
sourcePointsBodyXyzM: sourcePoints,
|
||||
localSlamSourceFrameCount: integerValue(
|
||||
payload.local_slam_source_frame_count,
|
||||
"canonical_lab.spatial_frame.local_slam_source_frames",
|
||||
),
|
||||
localSlamSourcePointCount: integerValue(
|
||||
payload.local_slam_source_point_count,
|
||||
"canonical_lab.spatial_frame.local_slam_source_points",
|
||||
),
|
||||
localSlamBodyXyzM: localSlam,
|
||||
};
|
||||
}
|
||||
@@ -118,24 +118,6 @@ export interface VegetationRouteTgsAnchor {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CanonicalRecordedLabSpatialFrame {
|
||||
targetTimeNs: number;
|
||||
sourceTimeNs: number;
|
||||
poseTimeNs: number;
|
||||
trajectoryTimeNs: number;
|
||||
sourcePointCount: number;
|
||||
bodyFrame: {
|
||||
originMapXyzM: readonly [number, number, number];
|
||||
basisMapFromBody: readonly [
|
||||
readonly [number, number, number],
|
||||
readonly [number, number, number],
|
||||
readonly [number, number, number],
|
||||
];
|
||||
};
|
||||
sourcePointsBodyXyzM: readonly (readonly [number, number, number])[];
|
||||
localSlamBodyXyzM: readonly (readonly [number, number, number])[];
|
||||
}
|
||||
|
||||
export interface VegetationFullRouteLayer {
|
||||
name: string;
|
||||
resultId: string;
|
||||
@@ -1044,100 +1026,6 @@ export async function fetchVegetationRouteTgsAnchor(
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchCanonicalRecordedLabSpatialFrame(
|
||||
sessionId: string,
|
||||
generationSha256: string,
|
||||
targetTimeNs: number,
|
||||
{
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||
): Promise<CanonicalRecordedLabSpatialFrame> {
|
||||
if (
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(sessionId)
|
||||
|| !SHA256.test(generationSha256)
|
||||
|| !Number.isSafeInteger(targetTimeNs)
|
||||
|| targetTimeNs < 0
|
||||
) {
|
||||
throw new VegetationShadowContractError("Canonical LAB spatial identity недопустима.");
|
||||
}
|
||||
const query = new URLSearchParams({
|
||||
generation: generationSha256,
|
||||
time_ns: String(targetTimeNs),
|
||||
});
|
||||
const response = await fetcher(
|
||||
`/api/v1/observation-sessions/${encodeURIComponent(sessionId)}`
|
||||
+ `/canonical-lab/spatial-frame?${query.toString()}`,
|
||||
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new VegetationShadowContractError(
|
||||
`Canonical LAB spatial frame недоступен: HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
const payload = objectValue(await response.json(), "canonical_lab.spatial_frame");
|
||||
exact(
|
||||
payload.schema_version,
|
||||
"missioncore.canonical-recorded-lab-spatial-frame/v1",
|
||||
"canonical_lab.spatial_frame.schema_version",
|
||||
);
|
||||
exact(payload.target_time_ns, targetTimeNs, "canonical_lab.spatial_frame.target_time_ns");
|
||||
const pointList = (value: unknown, label: string) => arrayValue(value, label).map(
|
||||
(entry, index) => {
|
||||
const point = arrayValue(entry, `${label}[${index}]`).map(
|
||||
(channel, channelIndex) => numberValue(channel, `${label}[${index}][${channelIndex}]`),
|
||||
);
|
||||
if (point.length !== 3) {
|
||||
throw new VegetationShadowContractError(`${label}[${index}]: размер изменён.`);
|
||||
}
|
||||
return [point[0]!, point[1]!, point[2]!] as const;
|
||||
},
|
||||
);
|
||||
const sourcePoints = pointList(
|
||||
payload.source_points_body_xyz_m,
|
||||
"canonical_lab.spatial_frame.source_points",
|
||||
);
|
||||
const localSlam = pointList(
|
||||
payload.local_slam_body_xyz_m,
|
||||
"canonical_lab.spatial_frame.local_slam",
|
||||
);
|
||||
const sourcePointCount = integerValue(
|
||||
payload.source_point_count,
|
||||
"canonical_lab.spatial_frame.source_point_count",
|
||||
);
|
||||
if (sourcePointCount !== sourcePoints.length || sourcePointCount > 100_000 || localSlam.length > 10_000) {
|
||||
throw new VegetationShadowContractError("Canonical LAB spatial accounting изменён.");
|
||||
}
|
||||
const bodyFrame = objectValue(payload.body_frame, "canonical_lab.spatial_frame.body_frame");
|
||||
const origin = pointList(
|
||||
[bodyFrame.origin_map_xyz_m],
|
||||
"canonical_lab.spatial_frame.body_frame.origin",
|
||||
)[0]!;
|
||||
const basisRows = pointList(
|
||||
bodyFrame.basis_map_from_body,
|
||||
"canonical_lab.spatial_frame.body_frame.basis",
|
||||
);
|
||||
if (basisRows.length !== 3) {
|
||||
throw new VegetationShadowContractError("Canonical LAB spatial basis изменён.");
|
||||
}
|
||||
return {
|
||||
targetTimeNs,
|
||||
sourceTimeNs: integerValue(payload.source_time_ns, "canonical_lab.spatial_frame.source_time_ns"),
|
||||
poseTimeNs: integerValue(payload.pose_time_ns, "canonical_lab.spatial_frame.pose_time_ns"),
|
||||
trajectoryTimeNs: integerValue(
|
||||
payload.trajectory_time_ns,
|
||||
"canonical_lab.spatial_frame.trajectory_time_ns",
|
||||
),
|
||||
sourcePointCount,
|
||||
bodyFrame: {
|
||||
originMapXyzM: origin,
|
||||
basisMapFromBody: [basisRows[0]!, basisRows[1]!, basisRows[2]!],
|
||||
},
|
||||
sourcePointsBodyXyzM: sourcePoints,
|
||||
localSlamBodyXyzM: localSlam,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchVegetationShadowResult(
|
||||
resultId: string,
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type LaboratoryMetricPackedCellEvidence,
|
||||
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
|
||||
import { RecordedEvidenceVideoScene } from "../../components/laboratory/RecordedEvidenceVideoScene";
|
||||
import { useCanonicalRecordedLabSpatialFrame } from "../../components/laboratory/useCanonicalRecordedLabSpatialFrame";
|
||||
import { useRecordedEvidencePlayback } from "../../components/laboratory/useRecordedEvidencePlayback";
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
@@ -35,12 +36,14 @@ import {
|
||||
type RecordedEvidenceSemanticPaletteEntry,
|
||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||
import {
|
||||
fetchCanonicalRecordedLabSpatialFrame,
|
||||
canonicalRecordedLabPackedTgsCells,
|
||||
canonicalRecordedLabTgsIsCurrent,
|
||||
} from "../../core/laboratory/canonicalRecordedLab";
|
||||
import {
|
||||
fetchVegetationShadowResult,
|
||||
fetchVegetationRouteTgsAnchor,
|
||||
vegetationFullRouteMaskUrl,
|
||||
vegetationVideoMaskUrl,
|
||||
type CanonicalRecordedLabSpatialFrame,
|
||||
type VegetationFullRouteLayer,
|
||||
type VegetationFullRouteReview,
|
||||
type VegetationMixedRouteCase,
|
||||
@@ -105,9 +108,7 @@ function causalTgsCase(
|
||||
&& (!latest || candidate.sourceSequence > latest.sourceSequence)
|
||||
? candidate
|
||||
: latest
|
||||
), null) ?? cases.reduce((first, candidate) => (
|
||||
candidate.sourceSequence < first.sourceSequence ? candidate : first
|
||||
));
|
||||
), null);
|
||||
}
|
||||
|
||||
function nearestFullRouteFrameIndex(
|
||||
@@ -130,92 +131,6 @@ function nearestFullRouteFrameIndex(
|
||||
: low;
|
||||
}
|
||||
|
||||
function useCanonicalRavSpatialFrame(
|
||||
review: VegetationFullRouteReview,
|
||||
replayLaunch: ObservationSessionReplayLaunch | null,
|
||||
targetTimeNs: number,
|
||||
) {
|
||||
const [frame, setFrame] = useState<CanonicalRecordedLabSpatialFrame | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const desiredRef = useRef<number | null>(null);
|
||||
const runningRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const cacheRef = useRef(new Map<number, CanonicalRecordedLabSpatialFrame>());
|
||||
const pumpRef = useRef<() => void>(() => undefined);
|
||||
|
||||
pumpRef.current = () => {
|
||||
if (runningRef.current || desiredRef.current === null || !replayLaunch) return;
|
||||
runningRef.current = true;
|
||||
let settledTimeNs: number | null = null;
|
||||
void (async () => {
|
||||
while (mountedRef.current && desiredRef.current !== null) {
|
||||
const requestedTimeNs = desiredRef.current;
|
||||
const cached = cacheRef.current.get(requestedTimeNs);
|
||||
try {
|
||||
const next = cached ?? await fetchCanonicalRecordedLabSpatialFrame(
|
||||
review.sessionId,
|
||||
replayLaunch.sha256,
|
||||
requestedTimeNs,
|
||||
);
|
||||
if (!cached) {
|
||||
cacheRef.current.set(requestedTimeNs, next);
|
||||
while (cacheRef.current.size > 12) {
|
||||
const oldest = cacheRef.current.keys().next().value as number | undefined;
|
||||
if (oldest === undefined) break;
|
||||
cacheRef.current.delete(oldest);
|
||||
}
|
||||
}
|
||||
if (!mountedRef.current) break;
|
||||
setFrame(next);
|
||||
setError(null);
|
||||
} catch (caught: unknown) {
|
||||
if (!mountedRef.current) break;
|
||||
setError(caught instanceof Error ? caught.message : "Spatial-слои RAV004 недоступны.");
|
||||
}
|
||||
settledTimeNs = requestedTimeNs;
|
||||
if (desiredRef.current === requestedTimeNs) break;
|
||||
}
|
||||
})().finally(() => {
|
||||
runningRef.current = false;
|
||||
if (
|
||||
mountedRef.current
|
||||
&& desiredRef.current !== null
|
||||
&& desiredRef.current !== settledTimeNs
|
||||
) {
|
||||
pumpRef.current();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
desiredRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
cacheRef.current.clear();
|
||||
setFrame(null);
|
||||
setError(null);
|
||||
}, [replayLaunch?.sha256, review.sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!replayLaunch) return;
|
||||
desiredRef.current = targetTimeNs;
|
||||
const cached = cacheRef.current.get(targetTimeNs);
|
||||
if (cached) {
|
||||
setFrame(cached);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
pumpRef.current();
|
||||
}, [replayLaunch, targetTimeNs]);
|
||||
|
||||
return { frame, error, loading: Boolean(replayLaunch) && !frame && !error };
|
||||
}
|
||||
|
||||
function FullRouteReviewEvidence({
|
||||
resultId,
|
||||
review,
|
||||
@@ -266,7 +181,11 @@ function FullRouteReviewEvidence({
|
||||
const spatialRequestTimeNs = review.frameSourceTimesNs[spatialRequestIndex]
|
||||
?? review.frameSourceTimesNs[sequenceIndex]
|
||||
?? Math.round(playbackController.playback.currentSeconds * 1_000_000_000);
|
||||
const spatialEvidence = useCanonicalRavSpatialFrame(review, replayLaunch, spatialRequestTimeNs);
|
||||
const spatialEvidence = useCanonicalRecordedLabSpatialFrame({
|
||||
sessionId: review.sessionId,
|
||||
generationSha256: replayLaunch?.sha256 ?? null,
|
||||
targetTimeNs: spatialRequestTimeNs,
|
||||
});
|
||||
const layer = review[semanticLayer];
|
||||
const semantic = useMemo(() => semanticPresentation(layer), [layer]);
|
||||
const prefetchSrcs = useMemo(() => showCameraSemantic
|
||||
@@ -343,11 +262,17 @@ function FullRouteReviewEvidence({
|
||||
? review.frameSourceTimesNs[selectedTgsCase.sourceSequence - 1]
|
||||
?? Math.round(selectedTgsCase.sessionSeconds * 1_000_000_000)
|
||||
: spatialRequestTimeNs;
|
||||
const tgsReferenceEvidence = useCanonicalRavSpatialFrame(
|
||||
review,
|
||||
replayLaunch,
|
||||
selectedTgsTimeNs,
|
||||
const currentFrameTimeNs = review.frameSourceTimesNs[sequenceIndex]
|
||||
?? Math.round(playbackController.playback.currentSeconds * 1_000_000_000);
|
||||
const tgsWithinEvidenceWindow = Boolean(
|
||||
selectedTgsCase
|
||||
&& canonicalRecordedLabTgsIsCurrent(currentFrameTimeNs, selectedTgsTimeNs),
|
||||
);
|
||||
const tgsReferenceEvidence = useCanonicalRecordedLabSpatialFrame({
|
||||
sessionId: review.sessionId,
|
||||
generationSha256: replayLaunch?.sha256 ?? null,
|
||||
targetTimeNs: selectedTgsTimeNs,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!showTgs || !selectedTgsCase) {
|
||||
@@ -376,53 +301,24 @@ function FullRouteReviewEvidence({
|
||||
}, [review.linkedRouteReviewResultId, selectedTgsCase?.sourceSequence, showTgs]);
|
||||
|
||||
const packedTgsCells = useMemo<LaboratoryMetricPackedCellEvidence | undefined>(() => {
|
||||
if (!tgsAnchor) return undefined;
|
||||
if (!tgsAnchor || !tgsWithinEvidenceWindow) return undefined;
|
||||
const currentBody = spatialEvidence.frame?.bodyFrame;
|
||||
const anchorBody = tgsReferenceEvidence.frame?.bodyFrame;
|
||||
const transformPoint = (point: readonly [number, number, number]) => {
|
||||
if (!currentBody || !anchorBody) return point;
|
||||
const map = [0, 1, 2].map((row) => (
|
||||
anchorBody.originMapXyzM[row]!
|
||||
+ anchorBody.basisMapFromBody[row]!.reduce(
|
||||
(sum, coefficient, column) => sum + coefficient * point[column]!,
|
||||
0,
|
||||
)
|
||||
));
|
||||
const delta = map.map((value, index) => value - currentBody.originMapXyzM[index]!);
|
||||
return [0, 1, 2].map((column) => (
|
||||
currentBody.basisMapFromBody.reduce(
|
||||
(sum, row, rowIndex) => sum + row[column]! * delta[rowIndex]!,
|
||||
0,
|
||||
)
|
||||
)) as [number, number, number];
|
||||
};
|
||||
const centers: number[] = [];
|
||||
const zBounds: number[] = [];
|
||||
tgsAnchor.costmap.centersXyM.forEach(([x, y], index) => {
|
||||
const bounds = tgsAnchor.costmap.zBoundsM[index] ?? [null, null];
|
||||
const center = transformPoint([x, y, 0]);
|
||||
centers.push(center[0], center[1]);
|
||||
if (bounds[0] === null || bounds[1] === null) {
|
||||
zBounds.push(Number.NaN, Number.NaN);
|
||||
} else {
|
||||
const bottom = transformPoint([x, y, bounds[0]]);
|
||||
const top = transformPoint([x, y, bounds[1]]);
|
||||
zBounds.push(Math.min(bottom[2], top[2]), Math.max(bottom[2], top[2]));
|
||||
}
|
||||
});
|
||||
return {
|
||||
centersBodyXyM: Float32Array.from(centers),
|
||||
zBoundsM: Float32Array.from(zBounds),
|
||||
stateCodes: Uint8Array.from(tgsAnchor.costmap.stateCodes),
|
||||
};
|
||||
}, [spatialEvidence.frame?.bodyFrame, tgsAnchor, tgsReferenceEvidence.frame?.bodyFrame]);
|
||||
if (!currentBody || !anchorBody) return undefined;
|
||||
return canonicalRecordedLabPackedTgsCells(tgsAnchor.costmap, anchorBody, currentBody);
|
||||
}, [
|
||||
spatialEvidence.frame?.bodyFrame,
|
||||
tgsAnchor,
|
||||
tgsReferenceEvidence.frame?.bodyFrame,
|
||||
tgsWithinEvidenceWindow,
|
||||
]);
|
||||
|
||||
const semanticOverlay = showCameraSemantic ? {
|
||||
src: vegetationFullRouteMaskUrl(resultId, semanticLayer, sequenceIndex),
|
||||
prefetchSrcs,
|
||||
classes: semantic.classes,
|
||||
palette: semantic.palette,
|
||||
opacity: 0.76,
|
||||
opacity: 0.46,
|
||||
ariaLabel: `${layer.name} semantic prediction frame ${sequence}`,
|
||||
} : undefined;
|
||||
|
||||
@@ -464,7 +360,11 @@ function FullRouteReviewEvidence({
|
||||
? spatialEvidence.frame.localSlamBodyXyzM
|
||||
: []}
|
||||
obstacles={[]}
|
||||
rig={{ lengthM: 1, widthM: 0.8, nominalSensorHeightM: 0.4 }}
|
||||
rig={{
|
||||
lengthM: 1,
|
||||
widthM: 0.8,
|
||||
nominalSensorHeightM: spatialEvidence.frame.sensorHeight.meters,
|
||||
}}
|
||||
corridor={{ forwardLengthM: 12, rearMarginM: 1, halfWidthM: 0.4 }}
|
||||
occupiedVoxelSizeM={tgsAnchor?.costmap.cellSizeM ?? 0.45}
|
||||
mode={spatialMode}
|
||||
@@ -487,7 +387,9 @@ function FullRouteReviewEvidence({
|
||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||
{tgsAnchorError ?? linkedReviewError ?? (tgsAnchorLoading
|
||||
? `Открываем sealed TGS anchor ${selectedTgsCase.sourceSequence}; source/SLAM и общий clock продолжаются.`
|
||||
: `TGS anchor ${selectedTgsCase.sourceSequence} из 10; source/SLAM и общий clock продолжаются.`)}
|
||||
: tgsWithinEvidenceWindow
|
||||
? `TGS anchor ${selectedTgsCase.sourceSequence} из 10; source/SLAM и общий clock продолжаются.`
|
||||
: `TGS anchor ${selectedTgsCase.sourceSequence} старше доказанного окна 1 с; слой скрыт, playback продолжается.`)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
@@ -597,12 +499,12 @@ function FullRouteReviewEvidence({
|
||||
</div>
|
||||
<div>
|
||||
<span>Spatial evidence</span>
|
||||
<strong>{showTgs && selectedTgsCase
|
||||
<strong>{showTgs && selectedTgsCase && tgsWithinEvidenceWindow
|
||||
? `TGS anchor ${selectedTgsCase.sourceSequence} · ${selectedTgsCase.tgs.occupiedCells} occupied`
|
||||
: "source RRD · points + SLAM"}</strong>
|
||||
: "source RRD · points + bounded Local SLAM"}</strong>
|
||||
<small>{showTgs
|
||||
? "latest causal of 10 sealed anchors · continuous playback retained"
|
||||
: "causal 1 s view · grayscale intensity · recorded source identity"}</small>
|
||||
? "TGS visible only inside sealed 1 s evidence window · playback retained"
|
||||
: "5 s bounded Local SLAM · ground-rebased recorded source"}</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -673,7 +575,7 @@ function FullRouteReviewResult({
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · RAVNOVES004TREE · полный маршрут"
|
||||
description="Общий recorded-LAB шаблон воспроизводит запись с травой и оврагами: RIGHT camera, исходное облако, SLAM trajectory, два независимых semantic-слоя и десять реально просчитанных TGS-якорей."
|
||||
description="Общий recorded-LAB шаблон воспроизводит запись с травой и оврагами: RIGHT camera, исходное облако, ограниченный Local SLAM, два независимых semantic-слоя и десять реально просчитанных TGS-якорей."
|
||||
status="FULL RECORDED REVIEW · truth отсутствует · commands OFF"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
@@ -686,7 +588,7 @@ function FullRouteReviewResult({
|
||||
]}
|
||||
brief={{
|
||||
question: "Что реально видно на полном RAV004-прогоне с высокой травой, оврагами и переходом к городу?",
|
||||
approach: "Одна recorded timeline открывается общим LAB viewer. Camera и RRD синхронизированы; EoMT/DDRNet переключаются на камере, source points и SLAM trajectory — в 3D, TGS — только на десяти запечатанных якорях.",
|
||||
approach: "Одна recorded timeline открывается общим LAB viewer. Camera и RRD синхронизированы; EoMT/DDRNet переключаются на камере, source points и 5-секундный Local SLAM — в 3D, TGS — только в доказанном окне десяти запечатанных якорей.",
|
||||
principalResult: "RAV004 больше не подменяется RAV00: доступна полная исходная запись и её реальные пространственные слои.",
|
||||
limitation: "Ручной truth, continuous TGS и point-aligned 3D semantics отсутствуют. Один повреждённый H.264-пакет на позиции 6092 заменён предыдущим декодированным кадром и отражён в proof.",
|
||||
}}
|
||||
@@ -695,7 +597,7 @@ function FullRouteReviewResult({
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
|
||||
components: [
|
||||
{ kind: "algorithm", name: "Recorded source points + SLAM trajectory", version: "sealed RRD", role: "spatial source evidence", identitySha256: null },
|
||||
{ kind: "algorithm", name: "Recorded source points + bounded Local SLAM", version: "source-paced-ground-v2", role: "spatial source evidence", identitySha256: null },
|
||||
{ kind: "model", name: review.city.name, version: "sealed Worker 006 run", role: "urban semantic review", identitySha256: null },
|
||||
{ kind: "model", name: review.vegetation.name, version: "GOOSE DDRNet-39", role: "vegetation semantic review", identitySha256: null },
|
||||
{ kind: "algorithm", name: "Causal TGS", version: "10 linked route anchors", role: "bounded geometric evidence", identitySha256: null },
|
||||
@@ -725,7 +627,7 @@ function FullRouteReviewResult({
|
||||
{ label: "Spatial evidence", value: "RRD + 10 TGS anchors", hint: "continuous TGS и 3D semantics отсутствуют" },
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Полный RAV004 открывается в каноническом recorded viewer с camera, source points, SLAM trajectory, EoMT, DDRNet и связанными TGS-якорями.",
|
||||
proved: "Полный RAV004 открывается в каноническом recorded viewer с camera, source points, bounded Local SLAM, EoMT, DDRNet и связанными TGS-якорями.",
|
||||
notProved: "Не доказаны truth accuracy, временная стабильность DDRNet, continuous negative-obstacle detection и безопасное управление ровером.",
|
||||
decision: "Использовать как visual audit. Следующий gate — truth-набор овраг/трава/дерево/яма и motion-aware temporal evaluation при целевых ≥10 FPS; navigation/actuation оставить OFF.",
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let canonicalMapGravityLocalPointToBodyGround;
|
||||
let canonicalRecordedLabPackedTgsCells;
|
||||
let canonicalRecordedLabTgsIsCurrent;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
canonicalMapGravityLocalPointToBodyGround,
|
||||
canonicalRecordedLabPackedTgsCells,
|
||||
canonicalRecordedLabTgsIsCurrent,
|
||||
} = await server.ssrLoadModule("/src/core/laboratory/canonicalRecordedLab.ts"));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
const identity = [
|
||||
[1, 0, 0],
|
||||
[0, 1, 0],
|
||||
[0, 0, 1],
|
||||
];
|
||||
|
||||
test("canonical TGS validity never retains a sparse anchor beyond its sealed history", () => {
|
||||
assert.equal(canonicalRecordedLabTgsIsCurrent(2_000_000_000, 1_000_000_000), true);
|
||||
assert.equal(canonicalRecordedLabTgsIsCurrent(2_000_000_001, 1_000_000_000), false);
|
||||
assert.equal(canonicalRecordedLabTgsIsCurrent(999_999_999, 1_000_000_000), false);
|
||||
});
|
||||
|
||||
test("map-gravity-local TGS uses sensor translation and current ground body exactly once", () => {
|
||||
const anchor = {
|
||||
originMapXyzM: [10, 20, 0.68],
|
||||
sensorOriginMapXyzM: [10, 20, 1],
|
||||
basisMapFromBody: identity,
|
||||
};
|
||||
const current = {
|
||||
originMapXyzM: [8, 20, 0],
|
||||
sensorOriginMapXyzM: [8, 20, 0.32],
|
||||
basisMapFromBody: identity,
|
||||
};
|
||||
assert.deepEqual(
|
||||
canonicalMapGravityLocalPointToBodyGround([1, 2, -1], anchor, current),
|
||||
[3, 2, 0],
|
||||
);
|
||||
const packed = canonicalRecordedLabPackedTgsCells({
|
||||
centersXyM: [[1, 2]],
|
||||
stateCodes: [2],
|
||||
zBoundsM: [[-1, 0]],
|
||||
}, anchor, current);
|
||||
assert.deepEqual([...packed.centersBodyXyM], [3, 2]);
|
||||
assert.deepEqual([...packed.zBoundsM], [0, 1]);
|
||||
assert.deepEqual([...packed.stateCodes], [2]);
|
||||
});
|
||||
|
||||
test("recorded LAB spatial loading is shared, profile-bound and experiment-neutral", async () => {
|
||||
const [contract, scheduler, vegetation] = await Promise.all([
|
||||
readFile(new URL("../src/core/laboratory/canonicalRecordedLabSpatial.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/components/laboratory/useCanonicalRecordedLabSpatialFrame.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../src/core/laboratory/vegetationShadow.ts", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(contract, /CANONICAL_RECORDED_LAB_SPATIAL_PROFILE/);
|
||||
assert.match(contract, /profile: CANONICAL_RECORDED_LAB_SPATIAL_PROFILE/);
|
||||
assert.match(scheduler, /Shared latest-request-wins scheduler/);
|
||||
assert.match(scheduler, /identityRef\.current !== requestIdentity/);
|
||||
assert.doesNotMatch(scheduler, /RAVNOVES|vegetation|DDRNet/);
|
||||
assert.doesNotMatch(vegetation, /fetchCanonicalRecordedLabSpatialFrame|CanonicalRecordedLabSpatialFrame/);
|
||||
});
|
||||
@@ -19,13 +19,15 @@ before(async () => {
|
||||
});
|
||||
({
|
||||
fetchVegetationBenchmarkResult,
|
||||
fetchCanonicalRecordedLabSpatialFrame,
|
||||
fetchVegetationShadowResult,
|
||||
fetchVegetationRouteTgsAnchor,
|
||||
vegetationFullRouteMaskUrl,
|
||||
} = await server.ssrLoadModule(
|
||||
"/src/core/laboratory/vegetationShadow.ts",
|
||||
));
|
||||
({ fetchCanonicalRecordedLabSpatialFrame } = await server.ssrLoadModule(
|
||||
"/src/core/laboratory/canonicalRecordedLabSpatial.ts",
|
||||
));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
@@ -405,16 +407,35 @@ test("canonical recorded LAB spatial frame keeps source, SLAM and body identity
|
||||
fetcher: async (url) => {
|
||||
requestedUrl = String(url);
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.canonical-recorded-lab-spatial-frame/v1",
|
||||
schema_version: "missioncore.canonical-recorded-lab-spatial-frame/v2",
|
||||
target_time_ns: 82_770_000_000,
|
||||
source_time_ns: 82_769_535_708,
|
||||
pose_time_ns: 82_769_535_708,
|
||||
trajectory_time_ns: 82_700_000_000,
|
||||
coordinate_frame: "body-ground",
|
||||
sensor_height: {
|
||||
meters: 0.32,
|
||||
source: "initial-source-cloud-lower-quantile-median",
|
||||
sample_count: 20,
|
||||
mad_m: 0.03,
|
||||
authority: "visual-derived",
|
||||
},
|
||||
spatial_profile: {
|
||||
profile_id: "source-paced-ground-v2",
|
||||
local_slam_history_seconds: 5,
|
||||
local_slam_radius_m: 30,
|
||||
local_slam_voxel_size_m: 0.12,
|
||||
local_slam_point_limit: 27000,
|
||||
},
|
||||
source_point_count: 2,
|
||||
source_points_body_xyz_m: [[1, 2, 3], [4, 5, 6]],
|
||||
local_slam_source_frame_count: 2,
|
||||
local_slam_source_point_count: 4,
|
||||
local_slam_point_count: 2,
|
||||
local_slam_body_xyz_m: [[0, 0, 0], [1, 0, 0]],
|
||||
body_frame: {
|
||||
origin_map_xyz_m: [33, 4, 1],
|
||||
sensor_origin_map_xyz_m: [33, 4, 1.32],
|
||||
basis_map_from_body: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
|
||||
},
|
||||
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
@@ -422,10 +443,11 @@ test("canonical recorded LAB spatial frame keeps source, SLAM and body identity
|
||||
});
|
||||
assert.equal(
|
||||
requestedUrl,
|
||||
`/api/v1/observation-sessions/session-004/canonical-lab/spatial-frame?generation=${generation}&time_ns=82770000000`,
|
||||
`/api/v1/observation-sessions/session-004/canonical-lab/spatial-frame?generation=${generation}&time_ns=82770000000&profile=source-paced-ground-v2`,
|
||||
);
|
||||
assert.equal(frame.sourcePointCount, 2);
|
||||
assert.equal(frame.localSlamBodyXyzM.length, 2);
|
||||
assert.equal(frame.sensorHeight.meters, 0.32);
|
||||
assert.deepEqual(frame.bodyFrame.originMapXyzM, [33, 4, 1]);
|
||||
});
|
||||
|
||||
@@ -461,11 +483,13 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
|
||||
assert.match(resultSource, /RecordedEvidenceVideoScene/);
|
||||
assert.match(resultSource, /LaboratoryMetricEvidenceScene/);
|
||||
assert.doesNotMatch(resultSource, /RerunViewport/);
|
||||
assert.match(resultSource, /fetchCanonicalRecordedLabSpatialFrame/);
|
||||
assert.match(resultSource, /useCanonicalRecordedLabSpatialFrame/);
|
||||
assert.doesNotMatch(resultSource, /cacheRef|pumpRef|desiredRef/);
|
||||
assert.match(resultSource, /useCanonicalRecordedLabReplayState/);
|
||||
assert.match(resultSource, /playbackTransport="epoch-stream"/);
|
||||
assert.match(resultSource, /causalTgsCase/);
|
||||
assert.match(resultSource, /latest causal of 10 sealed anchors/);
|
||||
assert.match(resultSource, /TGS visible only inside sealed 1 s evidence window/);
|
||||
assert.match(resultSource, /canonicalRecordedLabPackedTgsCells/);
|
||||
assert.doesNotMatch(resultSource, /LaboratoryRecordedClipPlayer|M48EvidenceModeRail/);
|
||||
assert.doesNotMatch(resultSource, /assets\.tgs|<img/);
|
||||
assert.match(resultSource, /SOURCE POINTS/);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
The LAB viewer must not run an independent Rerun transport beside the camera
|
||||
transport. This adapter reads the immutable recording once, indexes the
|
||||
recorded source cloud, sensor pose and SLAM trajectory, and returns the latest
|
||||
source-paced spatial sample in the current body frame. Camera, spatial layers
|
||||
and the common timeline can therefore be driven by one host clock.
|
||||
recorded source cloud and sensor pose, estimates the session sensor height from
|
||||
the initial stationary cloud, and returns both the current increment and a
|
||||
bounded accumulated local-SLAM cloud in a ground-rebased body frame. Camera,
|
||||
spatial layers and the common timeline can therefore be driven by one host
|
||||
clock without a per-LAB coordinate adapter.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,6 +21,7 @@ from typing import Any, Final
|
||||
import numpy as np
|
||||
import rerun_bindings as rr_bindings
|
||||
|
||||
CANONICAL_LAB_SPATIAL_PROFILE: Final = "source-paced-ground-v2"
|
||||
_POINT_ENTITY: Final = "/world/points"
|
||||
_POSE_ENTITY: Final = "/world/sensor_pose"
|
||||
_TRAJECTORY_ENTITY: Final = "/world/trajectory"
|
||||
@@ -27,6 +30,16 @@ _POSE_TRANSLATION_COMPONENT: Final = "Transform3D:translation"
|
||||
_POSE_QUATERNION_COMPONENT: Final = "Transform3D:quaternion"
|
||||
_TRAJECTORY_COMPONENT: Final = "LineStrips3D:strips"
|
||||
_INDEX_LOCK: Final = Lock()
|
||||
_HEIGHT_CALIBRATION_SECONDS: Final = 60.0
|
||||
_HEIGHT_CALIBRATION_MAX_FRAMES: Final = 120
|
||||
_HEIGHT_NEAR_MIN_RADIUS_M: Final = 1.0
|
||||
_HEIGHT_NEAR_MAX_RADIUS_M: Final = 6.0
|
||||
_HEIGHT_LOWER_QUANTILE: Final = 0.025
|
||||
_LOCAL_SLAM_HISTORY_SECONDS: Final = 5.0
|
||||
_LOCAL_SLAM_RADIUS_M: Final = 30.0
|
||||
_LOCAL_SLAM_VERTICAL_LIMIT_M: Final = 6.0
|
||||
_LOCAL_SLAM_VOXEL_SIZE_M: Final = 0.12
|
||||
_LOCAL_SLAM_POINT_LIMIT: Final = 27_000
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -47,6 +60,9 @@ class _CanonicalSpatialIndex:
|
||||
points: _TimedPoints
|
||||
poses: _TimedPoses
|
||||
trajectories: _TimedPoints
|
||||
sensor_height_m: float
|
||||
sensor_height_sample_count: int
|
||||
sensor_height_mad_m: float
|
||||
|
||||
|
||||
def _session_times(batch: Any) -> Any | None:
|
||||
@@ -159,7 +175,17 @@ def _load_index_cached(
|
||||
)
|
||||
if not points.times_ns or not poses.times_ns or not trajectories.times_ns:
|
||||
raise ValueError("Recorded LAB source has no canonical spatial layers")
|
||||
return _CanonicalSpatialIndex(points=points, poses=poses, trajectories=trajectories)
|
||||
sensor_height_m, sensor_height_sample_count, sensor_height_mad_m = (
|
||||
_estimate_sensor_height(points, poses)
|
||||
)
|
||||
return _CanonicalSpatialIndex(
|
||||
points=points,
|
||||
poses=poses,
|
||||
trajectories=trajectories,
|
||||
sensor_height_m=sensor_height_m,
|
||||
sensor_height_sample_count=sensor_height_sample_count,
|
||||
sensor_height_mad_m=sensor_height_mad_m,
|
||||
)
|
||||
|
||||
|
||||
def _load_index(
|
||||
@@ -207,12 +233,108 @@ def _map_points_to_body(
|
||||
return body.astype(np.float32)
|
||||
|
||||
|
||||
def _estimate_sensor_height(points: _TimedPoints, poses: _TimedPoses) -> tuple[float, int, float]:
|
||||
"""Estimate one session mount height from the initial qualified cloud.
|
||||
|
||||
The K1 recording has no explicit physical mount-height entity. The initial
|
||||
stationary minute is therefore the only admissible automatic calibration
|
||||
source. A low near-field quantile is measured per source increment and the
|
||||
session median rejects vegetation/ravine outliers. The result stays
|
||||
diagnostic and is never promoted to navigation authority by this adapter.
|
||||
"""
|
||||
|
||||
first_time_ns = points.times_ns[0]
|
||||
calibration_end_ns = first_time_ns + round(_HEIGHT_CALIBRATION_SECONDS * 1_000_000_000)
|
||||
candidates = [
|
||||
index
|
||||
for index, timestamp in enumerate(points.times_ns)
|
||||
if timestamp <= calibration_end_ns
|
||||
][:_HEIGHT_CALIBRATION_MAX_FRAMES]
|
||||
estimates: list[float] = []
|
||||
for point_index in candidates:
|
||||
pose_index = _latest_index(poses.times_ns, points.times_ns[point_index])
|
||||
body = _map_points_to_body(
|
||||
points.values[point_index],
|
||||
poses.translations[pose_index],
|
||||
poses.quaternions_xyzw[pose_index],
|
||||
)
|
||||
radius = np.linalg.norm(body[:, :2], axis=1)
|
||||
eligible = body[
|
||||
(radius >= _HEIGHT_NEAR_MIN_RADIUS_M)
|
||||
& (radius <= _HEIGHT_NEAR_MAX_RADIUS_M)
|
||||
& (body[:, 2] >= -2.0)
|
||||
& (body[:, 2] <= 0.5)
|
||||
]
|
||||
if eligible.shape[0] < 100:
|
||||
continue
|
||||
estimate = -float(np.quantile(eligible[:, 2], _HEIGHT_LOWER_QUANTILE))
|
||||
if 0.08 <= estimate <= 2.5:
|
||||
estimates.append(estimate)
|
||||
if len(estimates) < 8:
|
||||
raise ValueError("Recorded LAB sensor height cannot be estimated from source cloud")
|
||||
values = np.asarray(estimates, dtype=np.float64)
|
||||
height = float(np.median(values))
|
||||
mad = float(np.median(np.abs(values - height)))
|
||||
return height, len(estimates), mad
|
||||
|
||||
|
||||
def _ground_origin_map(
|
||||
sensor_origin_map: np.ndarray,
|
||||
basis_map_from_body: np.ndarray,
|
||||
sensor_height_m: float,
|
||||
) -> np.ndarray:
|
||||
return sensor_origin_map - basis_map_from_body[:, 2] * sensor_height_m
|
||||
|
||||
|
||||
def _map_points_to_ground_body(
|
||||
points_map: np.ndarray,
|
||||
ground_origin_map: np.ndarray,
|
||||
basis_map_from_body: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
body = (points_map.astype(np.float64) - ground_origin_map) @ basis_map_from_body
|
||||
return body.astype(np.float32)
|
||||
|
||||
|
||||
def _bounded_local_slam(
|
||||
points: _TimedPoints,
|
||||
target_time_ns: int,
|
||||
ground_origin_map: np.ndarray,
|
||||
basis_map_from_body: np.ndarray,
|
||||
) -> tuple[np.ndarray, int, int]:
|
||||
start_ns = target_time_ns - round(_LOCAL_SLAM_HISTORY_SECONDS * 1_000_000_000)
|
||||
first = bisect_right(points.times_ns, start_ns - 1)
|
||||
last = bisect_right(points.times_ns, target_time_ns)
|
||||
selected = points.values[first:last]
|
||||
if not selected:
|
||||
return np.empty((0, 3), dtype=np.float32), 0, 0
|
||||
source_count = sum(int(value.shape[0]) for value in selected)
|
||||
local = _map_points_to_ground_body(
|
||||
np.concatenate(selected, axis=0),
|
||||
ground_origin_map,
|
||||
basis_map_from_body,
|
||||
)
|
||||
mask = (
|
||||
(np.linalg.norm(local[:, :2], axis=1) <= _LOCAL_SLAM_RADIUS_M)
|
||||
& (np.abs(local[:, 2]) <= _LOCAL_SLAM_VERTICAL_LIMIT_M)
|
||||
)
|
||||
local = local[mask]
|
||||
if local.shape[0] == 0:
|
||||
return local, len(selected), source_count
|
||||
voxel = np.floor(local / _LOCAL_SLAM_VOXEL_SIZE_M).astype(np.int32)
|
||||
_, retained = np.unique(voxel, axis=0, return_index=True)
|
||||
local = local[np.sort(retained)]
|
||||
if local.shape[0] > _LOCAL_SLAM_POINT_LIMIT:
|
||||
stride = int(np.ceil(local.shape[0] / _LOCAL_SLAM_POINT_LIMIT))
|
||||
local = local[::stride][:_LOCAL_SLAM_POINT_LIMIT]
|
||||
return np.ascontiguousarray(local, dtype=np.float32), len(selected), source_count
|
||||
|
||||
|
||||
def canonical_lab_spatial_frame(
|
||||
recording_path: Path,
|
||||
generation_sha256: str,
|
||||
target_time_ns: int,
|
||||
) -> dict[str, object]:
|
||||
"""Return the latest sealed source cloud and SLAM route on one host time."""
|
||||
"""Return the current source cloud and bounded Local SLAM on one host time."""
|
||||
|
||||
if target_time_ns < 0:
|
||||
raise ValueError("Recorded LAB target time is invalid")
|
||||
@@ -229,31 +351,52 @@ def canonical_lab_spatial_frame(
|
||||
translation = index.poses.translations[pose_index]
|
||||
quaternion = index.poses.quaternions_xyzw[pose_index]
|
||||
basis_map_from_body = _rotation_map_from_body(quaternion)
|
||||
points_body = _map_points_to_body(index.points.values[point_index], translation, quaternion)
|
||||
trajectory_body = _map_points_to_body(
|
||||
index.trajectories.values[trajectory_index],
|
||||
ground_origin = _ground_origin_map(
|
||||
translation,
|
||||
quaternion,
|
||||
basis_map_from_body,
|
||||
index.sensor_height_m,
|
||||
)
|
||||
# The canonical local-SLAM layer is bounded around the vehicle. It must
|
||||
# never turn into the full world-route "blob" seen in the raw Rerun view.
|
||||
local_mask = (
|
||||
(np.abs(trajectory_body[:, 0]) <= 30.0)
|
||||
& (np.abs(trajectory_body[:, 1]) <= 30.0)
|
||||
& (np.abs(trajectory_body[:, 2]) <= 6.0)
|
||||
points_body = _map_points_to_ground_body(
|
||||
index.points.values[point_index],
|
||||
ground_origin,
|
||||
basis_map_from_body,
|
||||
)
|
||||
local_slam, local_slam_source_frames, local_slam_source_points = _bounded_local_slam(
|
||||
index.points,
|
||||
index.points.times_ns[point_index],
|
||||
ground_origin,
|
||||
basis_map_from_body,
|
||||
)
|
||||
local_trajectory = trajectory_body[local_mask]
|
||||
return {
|
||||
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v1",
|
||||
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v2",
|
||||
"target_time_ns": target_time_ns,
|
||||
"source_time_ns": index.points.times_ns[point_index],
|
||||
"pose_time_ns": index.poses.times_ns[pose_index],
|
||||
"trajectory_time_ns": index.trajectories.times_ns[trajectory_index],
|
||||
"coordinate_frame": "body-ground",
|
||||
"sensor_height": {
|
||||
"meters": index.sensor_height_m,
|
||||
"source": "initial-source-cloud-lower-quantile-median",
|
||||
"sample_count": index.sensor_height_sample_count,
|
||||
"mad_m": index.sensor_height_mad_m,
|
||||
"authority": "visual-derived",
|
||||
},
|
||||
"spatial_profile": {
|
||||
"profile_id": CANONICAL_LAB_SPATIAL_PROFILE,
|
||||
"local_slam_history_seconds": _LOCAL_SLAM_HISTORY_SECONDS,
|
||||
"local_slam_radius_m": _LOCAL_SLAM_RADIUS_M,
|
||||
"local_slam_voxel_size_m": _LOCAL_SLAM_VOXEL_SIZE_M,
|
||||
"local_slam_point_limit": _LOCAL_SLAM_POINT_LIMIT,
|
||||
},
|
||||
"body_frame": {
|
||||
"origin_map_xyz_m": translation.tolist(),
|
||||
"origin_map_xyz_m": ground_origin.tolist(),
|
||||
"sensor_origin_map_xyz_m": translation.tolist(),
|
||||
"basis_map_from_body": basis_map_from_body.tolist(),
|
||||
},
|
||||
"source_point_count": int(points_body.shape[0]),
|
||||
"source_points_body_xyz_m": points_body.tolist(),
|
||||
"local_slam_body_xyz_m": local_trajectory.tolist(),
|
||||
"local_slam_source_frame_count": local_slam_source_frames,
|
||||
"local_slam_source_point_count": local_slam_source_points,
|
||||
"local_slam_point_count": int(local_slam.shape[0]),
|
||||
"local_slam_body_xyz_m": local_slam.tolist(),
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ from k1link.sessions import (
|
||||
SessionStore,
|
||||
validate_recorded_media_timeline,
|
||||
)
|
||||
from k1link.sessions.canonical_lab_spatial import canonical_lab_spatial_frame
|
||||
from k1link.sessions.canonical_lab_spatial import (
|
||||
CANONICAL_LAB_SPATIAL_PROFILE,
|
||||
canonical_lab_spatial_frame,
|
||||
)
|
||||
from k1link.sessions.plugin_contract import RecordedPointColorRenderer
|
||||
from k1link.viewer.recorded import (
|
||||
APPLICATION_ID as RECORDED_APPLICATION_ID,
|
||||
@@ -832,6 +835,7 @@ def build_session_router(
|
||||
session_id: str,
|
||||
generation: Annotated[str, Query(min_length=64, max_length=64)],
|
||||
time_ns: Annotated[int, Query(ge=0, le=MAX_SAFE_INTEGER)],
|
||||
profile: Literal["source-paced-ground-v2"],
|
||||
) -> JSONResponse:
|
||||
"""Serve one body-frame sample for the canonical recorded-LAB clock.
|
||||
|
||||
@@ -890,7 +894,10 @@ def build_session_router(
|
||||
payload,
|
||||
headers={
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
"ETag": f'"{generation}:{payload["source_time_ns"]}"',
|
||||
"ETag": (
|
||||
f'"{generation}:{CANONICAL_LAB_SPATIAL_PROFILE}:'
|
||||
f'{payload["source_time_ns"]}"'
|
||||
),
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.sessions.canonical_lab_spatial import (
|
||||
_TimedPoints,
|
||||
_TimedPoses,
|
||||
_bounded_local_slam,
|
||||
_estimate_sensor_height,
|
||||
_ground_origin_map,
|
||||
)
|
||||
|
||||
|
||||
def _calibration_cloud(height_m: float, seed: int) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
xy = rng.uniform(-5.5, 5.5, size=(500, 2)).astype(np.float32)
|
||||
radius = np.linalg.norm(xy, axis=1)
|
||||
xy = xy[(radius >= 1.0) & (radius <= 5.5)][:360]
|
||||
ground = np.column_stack((
|
||||
xy,
|
||||
rng.normal(-height_m, 0.006, size=xy.shape[0]),
|
||||
)).astype(np.float32)
|
||||
vegetation = np.column_stack((
|
||||
rng.uniform(-5, 5, size=(300, 2)),
|
||||
rng.uniform(0.0, 1.2, size=300),
|
||||
)).astype(np.float32)
|
||||
return np.concatenate((ground, vegetation), axis=0)
|
||||
|
||||
|
||||
def test_session_sensor_height_is_derived_from_initial_source_cloud() -> None:
|
||||
times = tuple(index * 500_000_000 for index in range(12))
|
||||
points = _TimedPoints(
|
||||
times_ns=times,
|
||||
values=tuple(_calibration_cloud(0.32, index) for index in range(12)),
|
||||
)
|
||||
poses = _TimedPoses(
|
||||
times_ns=times,
|
||||
translations=tuple(np.zeros(3) for _ in times),
|
||||
quaternions_xyzw=tuple(np.asarray([0.0, 0.0, 0.0, 1.0]) for _ in times),
|
||||
)
|
||||
|
||||
height, sample_count, mad = _estimate_sensor_height(points, poses)
|
||||
|
||||
assert height == pytest.approx(0.32, abs=0.02)
|
||||
assert sample_count == 12
|
||||
assert mad < 0.02
|
||||
|
||||
|
||||
def test_local_slam_accumulates_source_increments_in_ground_body_frame() -> None:
|
||||
points = _TimedPoints(
|
||||
times_ns=(0, 1_000_000_000, 2_000_000_000),
|
||||
values=(
|
||||
np.asarray([[1.0, 0.0, -0.32]], dtype=np.float32),
|
||||
np.asarray([[2.0, 0.0, -0.32]], dtype=np.float32),
|
||||
np.asarray([[3.0, 0.0, -0.32]], dtype=np.float32),
|
||||
),
|
||||
)
|
||||
basis = np.eye(3)
|
||||
ground_origin = _ground_origin_map(np.asarray([0.0, 0.0, 0.0]), basis, 0.32)
|
||||
|
||||
local, frame_count, source_count = _bounded_local_slam(
|
||||
points,
|
||||
2_000_000_000,
|
||||
ground_origin,
|
||||
basis,
|
||||
)
|
||||
|
||||
assert frame_count == 3
|
||||
assert source_count == 3
|
||||
assert local[:, 2].tolist() == pytest.approx([0.0, 0.0, 0.0], abs=1e-6)
|
||||
@@ -489,17 +489,36 @@ def test_canonical_lab_spatial_frame_uses_ready_immutable_recording(
|
||||
assert resolved is not None and resolved.recording is not None
|
||||
generation = hashlib.sha256(payload).hexdigest()
|
||||
expected = {
|
||||
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v1",
|
||||
"schema_version": "missioncore.canonical-recorded-lab-spatial-frame/v2",
|
||||
"target_time_ns": 500_000_000,
|
||||
"source_time_ns": 499_000_000,
|
||||
"pose_time_ns": 499_000_000,
|
||||
"trajectory_time_ns": 490_000_000,
|
||||
"coordinate_frame": "body-ground",
|
||||
"sensor_height": {
|
||||
"meters": 0.32,
|
||||
"source": "initial-source-cloud-lower-quantile-median",
|
||||
"sample_count": 20,
|
||||
"mad_m": 0.03,
|
||||
"authority": "visual-derived",
|
||||
},
|
||||
"spatial_profile": {
|
||||
"profile_id": "source-paced-ground-v2",
|
||||
"local_slam_history_seconds": 5.0,
|
||||
"local_slam_radius_m": 30.0,
|
||||
"local_slam_voxel_size_m": 0.12,
|
||||
"local_slam_point_limit": 27000,
|
||||
},
|
||||
"body_frame": {
|
||||
"origin_map_xyz_m": [0.0, 0.0, 0.0],
|
||||
"sensor_origin_map_xyz_m": [0.0, 0.0, 0.32],
|
||||
"basis_map_from_body": [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
|
||||
},
|
||||
"source_point_count": 1,
|
||||
"source_points_body_xyz_m": [[1.0, 2.0, 3.0]],
|
||||
"local_slam_source_frame_count": 1,
|
||||
"local_slam_source_point_count": 1,
|
||||
"local_slam_point_count": 1,
|
||||
"local_slam_body_xyz_m": [[0.0, 0.0, 0.0]],
|
||||
}
|
||||
|
||||
@@ -525,9 +544,12 @@ def test_canonical_lab_spatial_frame_uses_ready_immutable_recording(
|
||||
session_id=session.name,
|
||||
generation=generation,
|
||||
time_ns=500_000_000,
|
||||
profile="source-paced-ground-v2",
|
||||
))
|
||||
assert json.loads(response.body) == expected
|
||||
assert response.headers["etag"] == f'"{generation}:499000000"'
|
||||
assert response.headers["etag"] == (
|
||||
f'"{generation}:source-paced-ground-v2:499000000"'
|
||||
)
|
||||
assert response.headers["cache-control"].endswith("immutable")
|
||||
finally:
|
||||
manager.close()
|
||||
|
||||
Reference in New Issue
Block a user