feat(lidar): add RAVNOVES field review

This commit is contained in:
DCCONSTRUCTIONS 2026-07-25 10:26:07 +03:00
parent 2dfb34ef21
commit 3333e9ac0f
13 changed files with 2775 additions and 14 deletions

View File

@ -147,6 +147,106 @@ export interface LidarGroundFrame {
groundTruth: false; groundTruth: false;
} }
export interface LidarFieldReviewWindowSummary {
index: number;
key: string;
label: string;
startSeconds: number;
endSeconds: number;
midpointSeconds: number;
sourceLidarSamples: number;
sourcePointCount: number;
displayPointCount: number;
sourceFrameStart: number;
sourceFrameEnd: number;
previewSourceFrameIndex: number;
previewSessionSeconds: number;
}
export interface LidarFieldReview {
reviewId: string;
displayName: string;
sessionId: string;
sourcePackId: string;
status: "diagnostic-only";
source: {
timelineStartSeconds: number;
timelineEndSeconds: number;
availableLidarFrames: number;
pointCount: number;
representation: "legacy-e10-vendor-map-with-pose";
intensityAvailable: false;
rawScanAccepted: false;
};
selection: {
purpose: "operator-readable-central-urban-field-review";
defaultWindowIndex: number;
accumulation: "per-source-frame masks accumulated in map frame";
maximumPointsPerWindow: number;
};
windows: LidarFieldReviewWindowSummary[];
metrics: {
sourceSamples: number;
current: {
provider: LidarGroundProviderSummary;
groundFraction: LidarDistribution;
latencyMs: LidarDistribution;
};
candidate: {
provider: LidarGroundProviderSummary;
groundFraction: LidarDistribution;
latencyMs: LidarDistribution;
};
comparison: {
algorithmGroundIou: LidarDistribution;
groundDisagreementFraction: LidarDistribution;
isAccuracyMetric: false;
};
};
decision: {
status: "visual-review-only";
productionPromotion: false;
reasons: string[];
};
createdAtUtc: string | null;
groundTruth: false;
authority: {
commandsEnabled: false;
navigationOrSafetyAccepted: false;
};
}
export interface LidarFieldReviewCatalog {
configured: boolean;
validTotal: number;
invalidTotal: number;
items: LidarFieldReview[];
}
export interface LidarFieldReviewWindow {
reviewId: string;
displayName: string;
sessionId: string;
sourcePackId: string;
windowIndex: number;
windowCount: number;
window: LidarFieldReviewWindowSummary;
pointCount: number;
coordinateFrame: "map";
distanceUnit: "m";
pointsXyzM: Array<[number, number, number]>;
intensity0To255: null;
intensity: {
available: false;
reason: string;
};
masks: LidarGroundFrame["masks"];
counts: LidarGroundFrame["counts"];
previewUrl: string;
groundTruth: false;
authority: LidarFieldReview["authority"];
}
export class LidarReplayContractError extends Error {} export class LidarReplayContractError extends Error {}
export class LidarReplayApiError extends Error { export class LidarReplayApiError extends Error {
@ -162,7 +262,10 @@ type LidarFetch = (
const SAFE_PACK_ID = /^lidar-replay-pack-[a-f0-9]{64}$/; const SAFE_PACK_ID = /^lidar-replay-pack-[a-f0-9]{64}$/;
const SAFE_GROUND_BENCHMARK_ID = /^ground-benchmark-[a-f0-9]{64}$/; const SAFE_GROUND_BENCHMARK_ID = /^ground-benchmark-[a-f0-9]{64}$/;
const SAFE_FIELD_REVIEW_ID = /^lidar-field-review-[a-f0-9]{64}$/;
const SAFE_E10_PACK_ID = /^e10-lidar-pack-[a-f0-9]{64}$/;
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/; const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
const SAFE_FIELD_KEY = /^[a-z0-9][a-z0-9-]{0,63}$/;
const SHA256 = /^[a-f0-9]{64}$/; const SHA256 = /^[a-f0-9]{64}$/;
const GIT_SHA1 = /^[a-f0-9]{40}$/; const GIT_SHA1 = /^[a-f0-9]{40}$/;
@ -641,6 +744,358 @@ export function parseLidarGroundFrame(value: unknown): LidarGroundFrame {
}; };
} }
function fieldReviewWindowSummary(
value: unknown,
expectedIndex: number,
): LidarFieldReviewWindowSummary {
const source = record(value, `field-review window ${expectedIndex}`);
const index = integer(source.index, "window.index");
const startSeconds = number(source.start_seconds, "window.start_seconds") ?? 0;
const endSeconds = number(source.end_seconds, "window.end_seconds") ?? 0;
if (index !== expectedIndex || startSeconds >= endSeconds) {
throw new LidarReplayContractError("LiDAR field-review window несовместим");
}
return {
index,
key: string(source.key, "window.key", SAFE_FIELD_KEY),
label: string(source.label, "window.label"),
startSeconds,
endSeconds,
midpointSeconds:
number(source.midpoint_seconds, "window.midpoint_seconds") ?? 0,
sourceLidarSamples: integer(
source.source_lidar_samples,
"window.source_lidar_samples",
),
sourcePointCount: integer(
source.source_point_count,
"window.source_point_count",
),
displayPointCount: integer(
source.display_point_count,
"window.display_point_count",
),
sourceFrameStart: integer(
source.source_frame_start,
"window.source_frame_start",
),
sourceFrameEnd: integer(
source.source_frame_end,
"window.source_frame_end",
),
previewSourceFrameIndex: integer(
source.preview_source_frame_index,
"window.preview_source_frame_index",
),
previewSessionSeconds:
number(
source.preview_session_seconds,
"window.preview_session_seconds",
) ?? 0,
};
}
function fieldReviewBranch(
value: unknown,
label: string,
): LidarFieldReview["metrics"]["current"] {
const source = record(value, label);
return {
provider: groundProvider(source.provider, `${label}.provider`),
groundFraction: distribution(
source.ground_fraction,
`${label}.ground_fraction`,
),
latencyMs: distribution(source.latency_ms, `${label}.latency_ms`),
};
}
function fieldReview(value: unknown): LidarFieldReview {
const source = record(value, "LiDAR field review");
const sourceEvidence = record(source.source, "field-review source");
const selection = record(source.selection, "field-review selection");
const sampling = record(selection.sampling, "field-review sampling");
const metrics = record(source.metrics, "field-review metrics");
const comparison = record(metrics.comparison, "field-review comparison");
const decision = record(source.decision, "field-review decision");
const authority = record(source.authority, "field-review authority");
if (
source.status !== "diagnostic-only"
|| source.ground_truth !== false
|| sourceEvidence.representation !== "legacy-e10-vendor-map-with-pose"
|| sourceEvidence.intensity_available !== false
|| sourceEvidence.raw_scan_accepted !== false
|| selection.purpose !== "operator-readable-central-urban-field-review"
|| selection.accumulation !== "per-source-frame masks accumulated in map frame"
|| sampling.method !== "uniform-point-index-per-window"
|| comparison.is_accuracy_metric !== false
|| decision.status !== "visual-review-only"
|| decision.production_promotion !== false
|| authority.commands_enabled !== false
|| authority.navigation_or_safety_accepted !== false
) {
throw new LidarReplayContractError(
"LiDAR field review завышает readiness или меняет evidence",
);
}
const windows = array(source.windows, "field-review windows").map(
fieldReviewWindowSummary,
);
const defaultWindowIndex = integer(
selection.default_window_index,
"selection.default_window_index",
);
const maximumPointsPerWindow = integer(
sampling.maximum_points_per_window,
"sampling.maximum_points_per_window",
);
if (
windows.length < 1
|| defaultWindowIndex >= windows.length
|| maximumPointsPerWindow < 1
|| maximumPointsPerWindow > 80_000
|| windows.some(
(window) =>
window.sourceLidarSamples < 1
|| window.sourcePointCount < window.displayPointCount
|| window.displayPointCount < 1
|| window.displayPointCount > maximumPointsPerWindow,
)
) {
throw new LidarReplayContractError("LiDAR field-review selection несовместим");
}
return {
reviewId: string(source.review_id, "review_id", SAFE_FIELD_REVIEW_ID),
displayName: string(source.display_name, "display_name"),
sessionId: string(source.session_id, "session_id", SAFE_ID),
sourcePackId: string(
source.source_pack_id,
"source_pack_id",
SAFE_E10_PACK_ID,
),
status: "diagnostic-only",
source: {
timelineStartSeconds:
number(
sourceEvidence.timeline_start_seconds,
"source.timeline_start_seconds",
) ?? 0,
timelineEndSeconds:
number(
sourceEvidence.timeline_end_seconds,
"source.timeline_end_seconds",
) ?? 0,
availableLidarFrames: integer(
sourceEvidence.available_lidar_frames,
"source.available_lidar_frames",
),
pointCount: integer(sourceEvidence.point_count, "source.point_count"),
representation: "legacy-e10-vendor-map-with-pose",
intensityAvailable: false,
rawScanAccepted: false,
},
selection: {
purpose: "operator-readable-central-urban-field-review",
defaultWindowIndex,
accumulation: "per-source-frame masks accumulated in map frame",
maximumPointsPerWindow,
},
windows,
metrics: {
sourceSamples: integer(metrics.source_samples, "metrics.source_samples"),
current: fieldReviewBranch(metrics.current, "metrics.current"),
candidate: fieldReviewBranch(metrics.candidate, "metrics.candidate"),
comparison: {
algorithmGroundIou: distribution(
comparison.algorithm_to_algorithm_ground_iou,
"metrics.comparison.algorithm_ground_iou",
),
groundDisagreementFraction: distribution(
comparison.ground_disagreement_fraction,
"metrics.comparison.ground_disagreement_fraction",
),
isAccuracyMetric: false,
},
},
decision: {
status: "visual-review-only",
productionPromotion: false,
reasons: array(decision.reasons, "decision.reasons").map((reason) =>
string(reason, "decision.reason")
),
},
createdAtUtc:
source.created_at_utc === null || source.created_at_utc === undefined
? null
: string(source.created_at_utc, "created_at_utc"),
groundTruth: false,
authority: {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
},
};
}
export function parseLidarFieldReviewCatalog(
value: unknown,
): LidarFieldReviewCatalog {
const source = record(value, "LiDAR field-review catalog");
if (
source.schema_version !== "missioncore.lidar-field-review-catalog/v1"
|| source.access !== "read-only"
) {
throw new LidarReplayContractError("LiDAR field-review catalog несовместим");
}
return {
configured: boolean(source.configured, "configured"),
validTotal: integer(source.valid_total, "valid_total"),
invalidTotal: integer(source.invalid_total, "invalid_total"),
items: array(source.items, "items").map(fieldReview),
};
}
export function parseLidarFieldReviewWindow(
value: unknown,
): LidarFieldReviewWindow {
const source = record(value, "LiDAR field-review window");
const reviewId = string(
source.review_id,
"review_id",
SAFE_FIELD_REVIEW_ID,
);
if (
source.schema_version !== "missioncore.lidar-field-review-window/v1"
|| source.access !== "read-only"
|| source.ground_truth !== false
|| source.coordinate_frame !== "map"
|| source.distance_unit !== "m"
) {
throw new LidarReplayContractError("LiDAR field-review window несовместим");
}
const authority = record(source.authority, "authority");
const intensity = record(source.intensity, "intensity");
if (
authority.commands_enabled !== false
|| authority.navigation_or_safety_accepted !== false
|| intensity.available !== false
) {
throw new LidarReplayContractError("LiDAR field-review authority несовместим");
}
const pointCount = integer(source.point_count, "point_count");
if (pointCount < 1 || pointCount > 80_000) {
throw new LidarReplayContractError("LiDAR field-review window слишком большой");
}
const points = array(source.points_xyz_m, "points_xyz_m");
if (points.length !== pointCount) {
throw new LidarReplayContractError("Количество field-review points не совпадает");
}
const pointsXyzM = points.map((value, index): [number, number, number] => {
const tuple = array(value, `points_xyz_m[${index}]`);
if (tuple.length !== 3) {
throw new LidarReplayContractError("LiDAR point должен содержать XYZ");
}
return [
number(tuple[0], `points_xyz_m[${index}].x`) ?? 0,
number(tuple[1], `points_xyz_m[${index}].y`) ?? 0,
number(tuple[2], `points_xyz_m[${index}].z`) ?? 0,
];
});
const masks = record(source.masks, "masks");
const currentGround = groundMask(
masks.current_ground,
"masks.current_ground",
pointCount,
);
const currentAssigned = groundMask(
masks.current_assigned,
"masks.current_assigned",
pointCount,
);
const candidateGround = groundMask(
masks.candidate_ground,
"masks.candidate_ground",
pointCount,
);
const candidateAssigned = groundMask(
masks.candidate_assigned,
"masks.candidate_assigned",
pointCount,
);
const disagreement = groundMask(
masks.disagreement,
"masks.disagreement",
pointCount,
);
const counts = record(source.counts, "counts");
const parsedCounts = {
currentGround: integer(counts.current_ground, "counts.current_ground"),
candidateGround: integer(
counts.candidate_ground,
"counts.candidate_ground",
),
disagreement: integer(counts.disagreement, "counts.disagreement"),
};
const windowIndex = integer(source.window_index, "window_index");
const windowCount = integer(source.window_count, "window_count");
const window = fieldReviewWindowSummary(source.window, windowIndex);
const expectedPreviewUrl =
`/api/v1/lidar/field-reviews/${reviewId}/windows/${windowIndex}/preview`;
if (
windowCount < 1
|| windowIndex >= windowCount
|| window.displayPointCount !== pointCount
|| parsedCounts.currentGround
!== currentGround.reduce((sum, item) => sum + item, 0)
|| parsedCounts.candidateGround
!== candidateGround.reduce((sum, item) => sum + item, 0)
|| parsedCounts.disagreement
!== disagreement.reduce((sum, item) => sum + item, 0)
|| disagreement.some(
(item, index) =>
item !== Number(currentGround[index] !== candidateGround[index]),
)
|| source.preview_url !== expectedPreviewUrl
) {
throw new LidarReplayContractError("LiDAR field-review content несовместим");
}
return {
reviewId,
displayName: string(source.display_name, "display_name"),
sessionId: string(source.session_id, "session_id", SAFE_ID),
sourcePackId: string(
source.source_pack_id,
"source_pack_id",
SAFE_E10_PACK_ID,
),
windowIndex,
windowCount,
window,
pointCount,
coordinateFrame: "map",
distanceUnit: "m",
pointsXyzM,
intensity0To255: null,
intensity: {
available: false,
reason: string(intensity.reason, "intensity.reason"),
},
masks: {
currentGround,
currentAssigned,
candidateGround,
candidateAssigned,
disagreement,
},
counts: parsedCounts,
previewUrl: expectedPreviewUrl,
groundTruth: false,
authority: {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
},
};
}
async function responseJson( async function responseJson(
response: Response, response: Response,
fallback: string, fallback: string,
@ -739,3 +1194,48 @@ export async function fetchLidarGroundFrame(
await responseJson(response, "Не удалось получить LiDAR ground frame."), await responseJson(response, "Не удалось получить LiDAR ground frame."),
); );
} }
export async function fetchLidarFieldReviews(
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
): Promise<LidarFieldReviewCatalog> {
const fetcher = options.fetcher ?? fetch;
const response = await fetcher("/api/v1/lidar/field-reviews?limit=10", {
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
});
return parseLidarFieldReviewCatalog(
await responseJson(response, "Не удалось получить полевой LiDAR review."),
);
}
export async function fetchLidarFieldReviewWindow(
reviewId: string,
windowIndex: number,
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
): Promise<LidarFieldReviewWindow> {
if (
!SAFE_FIELD_REVIEW_ID.test(reviewId)
|| !Number.isInteger(windowIndex)
|| windowIndex < 0
) {
throw new LidarReplayContractError(
"Некорректное окно полевого LiDAR review",
);
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/lidar/field-reviews/${reviewId}/windows/${windowIndex}`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseLidarFieldReviewWindow(
await responseJson(
response,
"Не удалось получить окно полевого LiDAR review.",
),
);
}

View File

@ -18,6 +18,23 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.lidar-field-review__source {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lidar-field-window-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lidar-field-stage {
grid-template-columns: 1fr;
}
.lidar-field-camera img {
min-height: 0;
aspect-ratio: 4 / 3;
}
.polygon-run-providers { .polygon-run-providers {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@ -202,6 +219,21 @@
display: none; display: none;
} }
.lidar-field-review__source,
.lidar-field-window-list {
grid-template-columns: 1fr;
}
.lidar-field-cloud > header,
.lidar-field-review__explanation {
align-items: stretch;
grid-template-columns: 1fr;
}
.lidar-field-cloud > header {
flex-direction: column;
}
.polygon-run-identity dl { .polygon-run-identity dl {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@ -954,6 +954,218 @@
margin-top: 0.8rem; margin-top: 0.8rem;
} }
.lidar-field-review {
min-width: 0;
border-color: rgb(74 215 255 / 0.22);
background:
radial-gradient(circle at 8% 0%, rgb(56 124 255 / 0.13), transparent 30rem),
var(--station-panel);
}
.lidar-field-review__heading p,
.lidar-field-review__explanation p,
.lidar-field-review__empty p {
margin: 0.28rem 0 0;
max-width: 49rem;
color: var(--nodedc-text-muted);
font-size: 0.64rem;
line-height: 1.5;
}
.lidar-field-review__source {
display: grid;
grid-template-columns: minmax(13rem, 1.4fr) repeat(3, minmax(0, 0.72fr));
gap: 0.55rem;
margin-top: 1rem;
}
.lidar-field-review__source > div {
display: grid;
min-width: 0;
gap: 0.28rem;
border: 1px solid var(--station-hairline);
border-radius: 0.72rem;
background: rgb(255 255 255 / 0.025);
padding: 0.64rem 0.7rem;
}
.lidar-field-review__source span,
.lidar-field-window-list small,
.lidar-field-stage__label span,
.lidar-field-camera footer,
.lidar-field-stage__pending {
color: var(--nodedc-text-muted);
font-size: 0.59rem;
line-height: 1.4;
}
.lidar-field-review__source strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.68rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.lidar-field-window-list {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0.48rem;
margin-top: 0.65rem;
}
.lidar-field-window-list > button {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 0.5rem;
border: 1px solid var(--station-hairline);
border-radius: 0.78rem;
background: rgb(255 255 255 / 0.02);
padding: 0.62rem;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.lidar-field-window-list > button:hover,
.lidar-field-window-list > button[data-selected="true"] {
border-color: rgb(74 215 255 / 0.48);
background: rgb(74 215 255 / 0.08);
}
.lidar-field-window-list > button > span {
display: grid;
flex: 0 0 auto;
width: 1.35rem;
height: 1.35rem;
place-items: center;
border-radius: 50%;
background: rgb(74 215 255 / 0.12);
color: #78e3ff;
font-size: 0.59rem;
font-weight: 700;
}
.lidar-field-window-list > button > div {
display: grid;
min-width: 0;
gap: 0.18rem;
}
.lidar-field-window-list strong {
color: var(--nodedc-text-primary);
font-size: 0.61rem;
line-height: 1.35;
}
.lidar-field-stage {
display: grid;
grid-template-columns: minmax(18rem, 0.68fr) minmax(0, 1.32fr);
gap: 0.65rem;
margin-top: 0.7rem;
}
.lidar-field-camera,
.lidar-field-cloud {
overflow: hidden;
min-width: 0;
border: 1px solid rgb(255 255 255 / 0.09);
border-radius: 0.9rem;
background: #071018;
}
.lidar-field-camera {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
}
.lidar-field-camera > .lidar-field-stage__label,
.lidar-field-cloud > header {
min-height: 3.45rem;
border-bottom: 1px solid rgb(255 255 255 / 0.08);
padding: 0.66rem 0.72rem;
}
.lidar-field-stage__label {
display: grid;
gap: 0.18rem;
}
.lidar-field-stage__label strong {
color: var(--nodedc-text-primary);
font-size: 0.68rem;
}
.lidar-field-camera img {
display: block;
width: 100%;
height: 100%;
min-height: 25rem;
object-fit: contain;
}
.lidar-field-camera footer {
display: flex;
justify-content: space-between;
gap: 0.5rem;
border-top: 1px solid rgb(255 255 255 / 0.08);
padding: 0.52rem 0.7rem;
}
.lidar-field-stage__pending {
display: grid;
min-height: 25rem;
place-items: center;
padding: 1rem;
text-align: center;
}
.lidar-field-cloud > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.65rem;
}
.lidar-field-cloud .lidar-ground-scene {
min-height: 28rem;
border: 0;
border-radius: 0;
}
.lidar-field-cloud .lidar-ground-scene-placeholder {
min-height: 28rem;
border: 0;
border-radius: 0;
}
.lidar-field-review__explanation {
display: grid;
grid-template-columns: auto minmax(15rem, 1fr) auto;
align-items: center;
gap: 0.65rem;
margin-top: 0.68rem;
border-top: 1px solid var(--station-hairline);
padding-top: 0.68rem;
}
.lidar-field-review__explanation p {
margin: 0;
}
.lidar-field-review__empty {
display: flex;
align-items: center;
gap: 0.65rem;
margin-top: 0.8rem;
}
.lidar-field-review__empty p {
margin: 0;
}
.lidar-ground-benchmark { .lidar-ground-benchmark {
min-width: 0; min-width: 0;
} }

View File

@ -2,16 +2,27 @@ import { useEffect, useRef, useState } from "react";
import * as THREE from "three"; import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import type { LidarGroundFrame } from "../core/lidar/replayQuality";
export type LidarGroundViewMode = export type LidarGroundViewMode =
| "intensity" | "intensity"
| "current" | "current"
| "candidate" | "candidate"
| "disagreement"; | "disagreement";
export interface LidarGroundPointCloudFrame {
pointCount: number;
pointsXyzM: Array<[number, number, number]>;
intensity0To255: number[] | null;
masks: {
currentGround: number[];
currentAssigned: number[];
candidateGround: number[];
candidateAssigned: number[];
disagreement: number[];
};
}
interface LidarGroundPointCloudProps { interface LidarGroundPointCloudProps {
frame: LidarGroundFrame; frame: LidarGroundPointCloudFrame;
mode: LidarGroundViewMode; mode: LidarGroundViewMode;
} }
@ -28,7 +39,7 @@ function setRgb(
} }
function frameColors( function frameColors(
frame: LidarGroundFrame, frame: LidarGroundPointCloudFrame,
mode: LidarGroundViewMode, mode: LidarGroundViewMode,
): Float32Array { ): Float32Array {
const colors = new Float32Array(frame.pointCount * 3); const colors = new Float32Array(frame.pointCount * 3);
@ -38,7 +49,7 @@ function frameColors(
const candidate = frame.masks.candidateGround[index] === 1; const candidate = frame.masks.candidateGround[index] === 1;
const candidateAssigned = frame.masks.candidateAssigned[index] === 1; const candidateAssigned = frame.masks.candidateAssigned[index] === 1;
if (mode === "intensity") { if (mode === "intensity") {
const intensity = frame.intensity0To255[index] / 255; const intensity = (frame.intensity0To255?.[index] ?? 96) / 255;
setRgb( setRgb(
colors, colors,
offset, offset,
@ -88,6 +99,8 @@ export function LidarGroundPointCloud({
const materialRef = useRef<THREE.PointsMaterial | null>(null); const materialRef = useRef<THREE.PointsMaterial | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null); const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null); const controlsRef = useRef<OrbitControls | null>(null);
const fogRef = useRef<THREE.FogExp2 | null>(null);
const gridRef = useRef<THREE.GridHelper | null>(null);
const [renderError, setRenderError] = useState<string | null>(null); const [renderError, setRenderError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
@ -115,7 +128,9 @@ export function LidarGroundPointCloud({
host.prepend(renderer.domElement); host.prepend(renderer.domElement);
const scene = new THREE.Scene(); const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x071018, 0.035); const fog = new THREE.FogExp2(0x071018, 0.035);
scene.fog = fog;
fogRef.current = fog;
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 1_000); const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 1_000);
camera.position.set(6, 4.5, 6); camera.position.set(6, 4.5, 6);
cameraRef.current = camera; cameraRef.current = camera;
@ -147,6 +162,7 @@ export function LidarGroundPointCloud({
scene.add(new THREE.Points(geometry, material)); scene.add(new THREE.Points(geometry, material));
const grid = new THREE.GridHelper(24, 48, 0x3c7cff, 0x233747); const grid = new THREE.GridHelper(24, 48, 0x3c7cff, 0x233747);
gridRef.current = grid;
const gridMaterials = Array.isArray(grid.material) const gridMaterials = Array.isArray(grid.material)
? grid.material ? grid.material
: [grid.material]; : [grid.material];
@ -198,6 +214,8 @@ export function LidarGroundPointCloud({
materialRef.current = null; materialRef.current = null;
cameraRef.current = null; cameraRef.current = null;
controlsRef.current = null; controlsRef.current = null;
fogRef.current = null;
gridRef.current = null;
}; };
}, []); }, []);
@ -206,7 +224,9 @@ export function LidarGroundPointCloud({
const material = materialRef.current; const material = materialRef.current;
const camera = cameraRef.current; const camera = cameraRef.current;
const controls = controlsRef.current; const controls = controlsRef.current;
if (!geometry || !material || !camera || !controls) return; const fog = fogRef.current;
const grid = gridRef.current;
if (!geometry || !material || !camera || !controls || !fog || !grid) return;
const positions = new Float32Array(frame.pointCount * 3); const positions = new Float32Array(frame.pointCount * 3);
let minimumX = Number.POSITIVE_INFINITY; let minimumX = Number.POSITIVE_INFINITY;
@ -235,6 +255,8 @@ export function LidarGroundPointCloud({
geometry.computeBoundingSphere(); geometry.computeBoundingSphere();
const radius = Math.max(geometry.boundingSphere?.radius ?? 1, 0.2); const radius = Math.max(geometry.boundingSphere?.radius ?? 1, 0.2);
material.size = THREE.MathUtils.clamp(radius / 155, 0.014, 0.075); material.size = THREE.MathUtils.clamp(radius / 155, 0.014, 0.075);
fog.density = THREE.MathUtils.clamp(0.18 / radius, 0.0008, 0.035);
grid.scale.setScalar(Math.max(radius / 12, 1));
const targetHeight = Math.max((maximumZ - minimumZ) * 0.35, 0.15); const targetHeight = Math.max((maximumZ - minimumZ) * 0.35, 0.15);
const distance = Math.max(radius * 1.8, 1.2); const distance = Math.max(radius * 1.8, 1.2);

View File

@ -6,10 +6,14 @@ import {
} from "@nodedc/ui-react"; } from "@nodedc/ui-react";
import { import {
fetchLidarFieldReviews,
fetchLidarFieldReviewWindow,
fetchLidarGroundFrame, fetchLidarGroundFrame,
fetchLidarGroundBenchmarks, fetchLidarGroundBenchmarks,
fetchLidarReplayCatalog, fetchLidarReplayCatalog,
fetchLidarReplayDetail, fetchLidarReplayDetail,
type LidarFieldReview,
type LidarFieldReviewWindow,
type LidarGroundBenchmark, type LidarGroundBenchmark,
type LidarGroundFrame, type LidarGroundFrame,
type LidarReplayCatalog, type LidarReplayCatalog,
@ -69,6 +73,14 @@ export function LidarQualityWorkspace({
const [groundFrameError, setGroundFrameError] = useState<string | null>(null); const [groundFrameError, setGroundFrameError] = useState<string | null>(null);
const [groundViewMode, setGroundViewMode] = const [groundViewMode, setGroundViewMode] =
useState<LidarGroundViewMode>("disagreement"); useState<LidarGroundViewMode>("disagreement");
const [fieldReview, setFieldReview] = useState<LidarFieldReview | null>(null);
const [fieldWindow, setFieldWindow] =
useState<LidarFieldReviewWindow | null>(null);
const [fieldWindowIndex, setFieldWindowIndex] = useState(0);
const [fieldLoading, setFieldLoading] = useState(true);
const [fieldError, setFieldError] = useState<string | null>(null);
const [fieldViewMode, setFieldViewMode] =
useState<LidarGroundViewMode>("disagreement");
const [selectedPackId, setSelectedPackId] = useState<string | null>(null); const [selectedPackId, setSelectedPackId] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -146,7 +158,56 @@ export function LidarQualityWorkspace({
return () => controller.abort(); return () => controller.abort();
}, [groundBenchmark, groundFrameIndex]); }, [groundBenchmark, groundFrameIndex]);
useEffect(() => {
const controller = new AbortController();
setFieldLoading(true);
setFieldError(null);
void fetchLidarFieldReviews({ signal: controller.signal })
.then((nextCatalog) => {
if (controller.signal.aborted) return;
const nextReview = nextCatalog.items[0] ?? null;
setFieldReview(nextReview);
setFieldWindow(null);
setFieldWindowIndex(nextReview?.selection.defaultWindowIndex ?? 0);
})
.catch((loadError) => {
if (controller.signal.aborted) return;
setFieldReview(null);
setFieldWindow(null);
setFieldError(errorMessage(loadError));
})
.finally(() => {
if (!controller.signal.aborted) setFieldLoading(false);
});
return () => controller.abort();
}, [reloadGeneration]);
useEffect(() => {
if (!fieldReview) return;
const controller = new AbortController();
setFieldLoading(true);
setFieldError(null);
void fetchLidarFieldReviewWindow(
fieldReview.reviewId,
fieldWindowIndex,
{ signal: controller.signal },
)
.then((window) => {
if (!controller.signal.aborted) setFieldWindow(window);
})
.catch((loadError) => {
if (controller.signal.aborted) return;
setFieldWindow(null);
setFieldError(errorMessage(loadError));
})
.finally(() => {
if (!controller.signal.aborted) setFieldLoading(false);
});
return () => controller.abort();
}, [fieldReview, fieldWindowIndex]);
const groundNormalization = groundBenchmark?.inputDomain.normalization ?? null; const groundNormalization = groundBenchmark?.inputDomain.normalization ?? null;
const selectedFieldWindow = fieldReview?.windows[fieldWindowIndex] ?? null;
return ( return (
<div className="standard-workspace lidar-quality-workspace"> <div className="standard-workspace lidar-quality-workspace">
@ -191,12 +252,213 @@ export function LidarQualityWorkspace({
</GlassSurface> </GlassSurface>
) : ( ) : (
<> <>
<GlassSurface className="lidar-field-review" padding="lg">
<header className="panel-heading lidar-field-review__heading">
<div>
<span className="section-eyebrow">ПОЛЕВОЙ REVIEW · RAVNOVES00</span>
<h2>
{fieldReview?.displayName
?? "Центральный городской интервал"}
</h2>
<p>
Дорога, дома, автомобили и растительность. Облако накоплено
по исходным LiDAR-сэмплам выбранного окна, а кадр камеры
фиксирует контекст сцены.
</p>
</div>
<StatusBadge tone={fieldError ? "danger" : "warning"}>
{fieldError ? "Review недоступен" : "Visual review only"}
</StatusBadge>
</header>
{fieldReview ? (
<>
<div
className="lidar-field-review__source"
aria-label="Источник полевого LiDAR review"
>
<div>
<span>Запись</span>
<strong>{fieldReview.sessionId}</strong>
</div>
<div>
<span>Интервал записи</span>
<strong>
{formatNumber(fieldReview.source.timelineStartSeconds, 2)}
{""}
{formatNumber(fieldReview.source.timelineEndSeconds, 2)} с
</strong>
</div>
<div>
<span>LiDAR-сэмплов</span>
<strong>
{fieldReview.source.availableLidarFrames.toLocaleString("ru-RU")}
</strong>
</div>
<div>
<span>Исходных точек</span>
<strong>
{fieldReview.source.pointCount.toLocaleString("ru-RU")}
</strong>
</div>
</div>
<div
className="lidar-field-window-list"
role="group"
aria-label="Полевые сцены RAVNOVES00"
>
{fieldReview.windows.map((window) => (
<button
type="button"
key={window.key}
data-selected={
window.index === fieldWindowIndex ? "true" : undefined
}
onClick={() => {
setFieldWindow(null);
setFieldWindowIndex(window.index);
}}
>
<span>{window.index + 1}</span>
<div>
<strong>{window.label}</strong>
<small>
{formatNumber(window.startSeconds, 0)}
{""}
{formatNumber(window.endSeconds, 0)} с ·{" "}
{window.sourceLidarSamples} сканов
</small>
</div>
</button>
))}
</div>
<section className="lidar-field-stage">
<div className="lidar-field-camera">
<div className="lidar-field-stage__label">
<span>КОНТЕКСТ КАМЕРЫ</span>
<strong>
{selectedFieldWindow?.label ?? "Выбранная сцена"}
</strong>
</div>
{fieldWindow ? (
<img
src={fieldWindow.previewUrl}
alt={`Кадр камеры: ${fieldWindow.window.label}`}
/>
) : (
<div className="lidar-field-stage__pending">
{fieldError ?? "Загружаем кадр выбранной сцены…"}
</div>
)}
<footer>
<span>
Кадр {selectedFieldWindow?.previewSourceFrameIndex ?? "—"}
</span>
<span>
t = {formatNumber(
selectedFieldWindow?.previewSessionSeconds ?? null,
2,
)}{" "}
с
</span>
</footer>
</div>
<div className="lidar-field-cloud">
<header>
<div className="lidar-field-stage__label">
<span>НАКОПЛЕННОЕ MAP-ОБЛАКО</span>
<strong>
{selectedFieldWindow
? `${selectedFieldWindow.sourcePointCount.toLocaleString(
"ru-RU",
)} исходных · ${selectedFieldWindow.displayPointCount.toLocaleString(
"ru-RU",
)} показано`
: "Ожидание данных"}
</strong>
</div>
<div
className="lidar-ground-modes"
role="group"
aria-label="Режим окраски полевого LiDAR"
>
{([
["current", "Current"],
["candidate", "Patchwork++"],
["disagreement", "Расхождения"],
] as const).map(([mode, label]) => (
<button
type="button"
key={mode}
data-active={
fieldViewMode === mode ? "true" : undefined
}
onClick={() => setFieldViewMode(mode)}
>
{label}
</button>
))}
</div>
</header>
{fieldWindow ? (
<LidarGroundPointCloud
frame={fieldWindow}
mode={fieldViewMode}
/>
) : (
<div className="lidar-ground-scene-placeholder">
<StatusBadge tone={fieldError ? "danger" : "accent"}>
{fieldError ? "Ошибка" : "Загрузка"}
</StatusBadge>
<span>
{fieldError ?? "Готовим накопленное облако…"}
</span>
</div>
)}
</div>
</section>
<div className="lidar-field-review__explanation">
<StatusBadge tone="warning">Не accuracy</StatusBadge>
<p>
Маски вычислены отдельно на каждом исходном скане и только
затем сведены в map frame. Это legacy E10 vendor-map
derivative без intensity и независимой ground-разметки,
поэтому результат предназначен для визуального разбора, а
не для production-gate.
</p>
<div className="lidar-ground-legend">
<span><i data-color="shared" />Ground у обоих</span>
<span><i data-color="current" />Только current</span>
<span><i data-color="candidate" />Только Patchwork++</span>
<span><i data-color="non-ground" />Оба non-ground</span>
</div>
</div>
</>
) : (
<div className="lidar-field-review__empty">
<StatusBadge tone={fieldError ? "danger" : "accent"}>
{fieldError ? "Ошибка загрузки" : "Проверка evidence"}
</StatusBadge>
<p>
{fieldError
?? (fieldLoading
? "Читаем RAVNOVES00 field-review артефакт…"
: "Field-review артефакт ещё не опубликован.")}
</p>
</div>
)}
</GlassSurface>
<section className="metrics-grid" aria-label="Качество LiDAR replay"> <section className="metrics-grid" aria-label="Качество LiDAR replay">
<MetricCard <MetricCard
featured featured
eyebrow="ТОЧЕЧНЫХ КАДРОВ" eyebrow="ТЕХНИЧЕСКИХ КАДРОВ"
value={detail.pack.pointFrames.toLocaleString("ru-RU")} value={detail.pack.pointFrames.toLocaleString("ru-RU")}
detail={`${detail.pack.points.toLocaleString("ru-RU")} точек сохранено`} detail={`${detail.pack.points.toLocaleString("ru-RU")} точек · indoor contract slice`}
/> />
<MetricCard <MetricCard
eyebrow="СРЕДНЕЕ ТОЧЕК" eyebrow="СРЕДНЕЕ ТОЧЕК"
@ -358,12 +620,13 @@ export function LidarQualityWorkspace({
<header> <header>
<div> <div>
<span className="section-eyebrow"> <span className="section-eyebrow">
POINT-ALIGNED REVIEW ТЕХНИЧЕСКИЙ CONTRACT SLICE · VIEWER_LIVE
</span> </span>
<h3>Покадровое облако и маски</h3> <h3>Indoor-проверка point-aligned контракта</h3>
<p> <p>
Один и тот же map-frame XYZ, разные диагностические Этот короткий indoor-срез подтверждает выравнивание XYZ
раскраски. Маски не изменяют replay. и масок, но не является полевой оценкой качества. Для
улицы используйте RAVNOVES00 выше.
</p> </p>
</div> </div>
<div className="lidar-ground-frame-status"> <div className="lidar-ground-frame-status">

View File

@ -8,10 +8,14 @@ let parseLidarReplayCatalog;
let parseLidarReplayDetail; let parseLidarReplayDetail;
let parseLidarGroundBenchmarkCatalog; let parseLidarGroundBenchmarkCatalog;
let parseLidarGroundFrame; let parseLidarGroundFrame;
let parseLidarFieldReviewCatalog;
let parseLidarFieldReviewWindow;
let fetchLidarReplayCatalog; let fetchLidarReplayCatalog;
let fetchLidarReplayDetail; let fetchLidarReplayDetail;
let fetchLidarGroundBenchmarks; let fetchLidarGroundBenchmarks;
let fetchLidarGroundFrame; let fetchLidarGroundFrame;
let fetchLidarFieldReviews;
let fetchLidarFieldReviewWindow;
let LidarReplayContractError; let LidarReplayContractError;
let workspaceById; let workspaceById;
@ -26,10 +30,14 @@ before(async () => {
parseLidarReplayDetail, parseLidarReplayDetail,
parseLidarGroundBenchmarkCatalog, parseLidarGroundBenchmarkCatalog,
parseLidarGroundFrame, parseLidarGroundFrame,
parseLidarFieldReviewCatalog,
parseLidarFieldReviewWindow,
fetchLidarReplayCatalog, fetchLidarReplayCatalog,
fetchLidarReplayDetail, fetchLidarReplayDetail,
fetchLidarGroundBenchmarks, fetchLidarGroundBenchmarks,
fetchLidarGroundFrame, fetchLidarGroundFrame,
fetchLidarFieldReviews,
fetchLidarFieldReviewWindow,
LidarReplayContractError, LidarReplayContractError,
} = await server.ssrLoadModule("/src/core/lidar/replayQuality.ts")); } = await server.ssrLoadModule("/src/core/lidar/replayQuality.ts"));
({ workspaceById } = await server.ssrLoadModule("/src/productModel.ts")); ({ workspaceById } = await server.ssrLoadModule("/src/productModel.ts"));
@ -40,6 +48,8 @@ after(async () => {
}); });
const packId = `lidar-replay-pack-${"a".repeat(64)}`; const packId = `lidar-replay-pack-${"a".repeat(64)}`;
const fieldReviewId = `lidar-field-review-${"d".repeat(64)}`;
const e10PackId = `e10-lidar-pack-${"e".repeat(64)}`;
function summary(overrides = {}) { function summary(overrides = {}) {
return { return {
@ -267,6 +277,145 @@ function groundFrame(overrides = {}) {
}; };
} }
function fieldReviewWindowSummary(overrides = {}) {
return {
index: 0,
key: "intersection-facades",
label: "Перекрёсток, дорога и фасады",
start_seconds: 145,
end_seconds: 151,
midpoint_seconds: 148,
source_lidar_samples: 56,
source_point_count: 98538,
display_point_count: 3,
source_frame_start: 1097,
source_frame_end: 1156,
preview_source_frame_index: 1120,
preview_session_seconds: 147.451857292,
...overrides,
};
}
function fieldReviewCatalog(overrides = {}) {
const provider = (providerId, sourceCommit = undefined) => ({
provider_id: providerId,
source_commit: sourceCommit,
});
const branch = (providerValue) => ({
provider: providerValue,
ground_fraction: distribution(),
latency_ms: distribution(),
});
return {
schema_version: "missioncore.lidar-field-review-catalog/v1",
configured: true,
valid_total: 1,
invalid_total: 0,
access: "read-only",
items: [{
review_id: fieldReviewId,
display_name: "RAVNOVES00 · центральный городской интервал",
session_id: "20260720T065719Z_viewer_live",
source_pack_id: e10PackId,
status: "diagnostic-only",
source: {
timeline_start_seconds: 135.365857292,
timeline_end_seconds: 195.334857292,
available_lidar_frames: 526,
point_count: 1182292,
representation: "legacy-e10-vendor-map-with-pose",
intensity_available: false,
raw_scan_accepted: false,
},
selection: {
purpose: "operator-readable-central-urban-field-review",
default_window_index: 0,
accumulation: "per-source-frame masks accumulated in map frame",
sampling: {
method: "uniform-point-index-per-window",
maximum_points_per_window: 80000,
},
},
windows: [fieldReviewWindowSummary()],
metrics: {
source_samples: 56,
current: branch(
provider("missioncore-local-percentile-ground/v1"),
),
candidate: branch(
provider(
"patchworkpp/v1.4.1",
"3e6903a1d5537a4cc2ace897b0bbb98a92d6014c",
),
),
comparison: {
algorithm_to_algorithm_ground_iou: distribution(),
ground_disagreement_fraction: distribution(),
is_accuracy_metric: false,
},
},
decision: {
status: "visual-review-only",
production_promotion: false,
reasons: ["legacy derivative does not retain intensity"],
},
created_at_utc: "2026-07-25T02:00:00Z",
ground_truth: false,
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
},
}],
...overrides,
};
}
function fieldReviewWindow(overrides = {}) {
return {
schema_version: "missioncore.lidar-field-review-window/v1",
review_id: fieldReviewId,
display_name: "RAVNOVES00 · центральный городской интервал",
session_id: "20260720T065719Z_viewer_live",
source_pack_id: e10PackId,
window_index: 0,
window_count: 1,
window: fieldReviewWindowSummary(),
point_count: 3,
coordinate_frame: "map",
distance_unit: "m",
intensity: {
available: false,
reason: "E10 derivative did not retain rgbi/intensity",
},
points_xyz_m: [
[0, 0, 0],
[1, 0, 0.1],
[0, 1, 0.5],
],
masks: {
current_ground: [1, 1, 0],
current_assigned: [1, 1, 1],
candidate_ground: [1, 0, 0],
candidate_assigned: [1, 1, 1],
disagreement: [0, 1, 0],
},
counts: {
current_ground: 2,
candidate_ground: 1,
disagreement: 1,
},
preview_url:
`/api/v1/lidar/field-reviews/${fieldReviewId}/windows/0/preview`,
access: "read-only",
ground_truth: false,
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
},
...overrides,
};
}
function jsonResponse(payload, status = 200) { function jsonResponse(payload, status = 200) {
return new Response(JSON.stringify(payload), { return new Response(JSON.stringify(payload), {
status, status,
@ -359,6 +508,49 @@ test("ground frame stays point-aligned, bounded and path-free", () => {
); );
}); });
test("field review identifies RAVNOVES00 source and stays visual-only", () => {
const parsedCatalog = parseLidarFieldReviewCatalog(fieldReviewCatalog());
const parsedWindow = parseLidarFieldReviewWindow(fieldReviewWindow());
assert.equal(parsedCatalog.items[0].sourcePackId, e10PackId);
assert.equal(parsedCatalog.items[0].source.availableLidarFrames, 526);
assert.equal(parsedCatalog.items[0].selection.defaultWindowIndex, 0);
assert.equal(parsedWindow.window.label, "Перекрёсток, дорога и фасады");
assert.equal(parsedWindow.intensity0To255, null);
assert.equal(parsedWindow.counts.disagreement, 1);
assert.equal("path" in parsedWindow, false);
const promoted = fieldReviewCatalog();
promoted.items[0].decision.production_promotion = true;
assert.throws(
() => parseLidarFieldReviewCatalog(promoted),
LidarReplayContractError,
);
});
test("field-review window refuses forged preview and point masks", () => {
assert.throws(
() => parseLidarFieldReviewWindow(fieldReviewWindow({
preview_url: "https://example.invalid/frame.jpg",
})),
LidarReplayContractError,
);
assert.throws(
() => parseLidarFieldReviewWindow(fieldReviewWindow({
masks: {
...fieldReviewWindow().masks,
disagreement: [0, 0, 0],
},
counts: {
current_ground: 2,
candidate_ground: 1,
disagreement: 0,
},
})),
LidarReplayContractError,
);
});
test("LiDAR fetchers use read-only endpoints and workspace is registered", async () => { test("LiDAR fetchers use read-only endpoints and workspace is registered", async () => {
const calls = []; const calls = [];
const fetcher = async (input, init) => { const fetcher = async (input, init) => {
@ -368,6 +560,11 @@ test("LiDAR fetchers use read-only endpoints and workspace is registered", async
? jsonResponse(groundFrame()) ? jsonResponse(groundFrame())
: jsonResponse(groundCatalog()); : jsonResponse(groundCatalog());
} }
if (String(input).includes("field-reviews")) {
return String(input).includes("/windows/")
? jsonResponse(fieldReviewWindow())
: jsonResponse(fieldReviewCatalog());
}
return String(input).includes(packId) return String(input).includes(packId)
? jsonResponse(detail()) ? jsonResponse(detail())
: jsonResponse(catalog()); : jsonResponse(catalog());
@ -380,11 +577,19 @@ test("LiDAR fetchers use read-only endpoints and workspace is registered", async
0, 0,
{ fetcher }, { fetcher },
); );
const fieldCatalog = await fetchLidarFieldReviews({ fetcher });
const fieldWindow = await fetchLidarFieldReviewWindow(
fieldReviewId,
0,
{ fetcher },
);
assert.equal(parsedCatalog.validTotal, 1); assert.equal(parsedCatalog.validTotal, 1);
assert.equal(parsedDetail.pack.packId, packId); assert.equal(parsedDetail.pack.packId, packId);
assert.equal(ground.validTotal, 1); assert.equal(ground.validTotal, 1);
assert.equal(frame.pointCount, 3); assert.equal(frame.pointCount, 3);
assert.equal(fieldCatalog.items[0].windows[0].sourceLidarSamples, 56);
assert.equal(fieldWindow.previewUrl.endsWith("/preview"), true);
assert.deepEqual(calls, [ assert.deepEqual(calls, [
{ input: "/api/v1/lidar/replay-packs?limit=50", method: "GET" }, { input: "/api/v1/lidar/replay-packs?limit=50", method: "GET" },
{ input: `/api/v1/lidar/replay-packs/${packId}`, method: "GET" }, { input: `/api/v1/lidar/replay-packs/${packId}`, method: "GET" },
@ -396,6 +601,14 @@ test("LiDAR fetchers use read-only endpoints and workspace is registered", async
input: `/api/v1/lidar/ground-benchmarks/ground-benchmark-${"c".repeat(64)}/frames/0`, input: `/api/v1/lidar/ground-benchmarks/ground-benchmark-${"c".repeat(64)}/frames/0`,
method: "GET", method: "GET",
}, },
{
input: "/api/v1/lidar/field-reviews?limit=10",
method: "GET",
},
{
input: `/api/v1/lidar/field-reviews/${fieldReviewId}/windows/0`,
method: "GET",
},
]); ]);
assert.equal(workspaceById("lidar-quality").root, "data"); assert.equal(workspaceById("lidar-quality").root, "data");
assert.equal(workspaceById("lidar-quality").kind, "lidar-quality"); assert.equal(workspaceById("lidar-quality").kind, "lidar-quality");

View File

@ -31,6 +31,17 @@ that evidence in Three.js with unrestricted orbit, pan and zoom plus
intensity/current/candidate/disagreement color modes. The browser never runs intensity/current/candidate/disagreement color modes. The browser never runs
Patchwork++, parses private firmware or receives a filesystem path. Patchwork++, parses private firmware or receives a filesystem path.
`missioncore.lidar-field-review/v1` is the separate operator-readable field
surface. The read-only `/api/v1/lidar/field-reviews` catalog names the exact
RAVNOVES00 source, timeline and five central urban windows. The bounded
`/api/v1/lidar/field-reviews/{review_id}/windows/{window_index}` response
contains a deterministic accumulated map cloud plus masks that were computed
per original LiDAR sample before accumulation. Its sibling `/preview` endpoint
returns the content-bound camera JPEG for that window. No source path,
Patchwork++ binary, worker control or command authority crosses this boundary.
The legacy E10 source has no intensity, so the API declares it unavailable
instead of synthesizing a value.
Static K1 3.0.2 firmware evidence confirms that the appliance internally uses Static K1 3.0.2 firmware evidence confirms that the appliance internally uses
a Livox MID-360 point/IMU path with richer timestamp/ring semantics and a a Livox MID-360 point/IMU path with richer timestamp/ring semantics and a
configured MQTT point-cloud downsample factor of four. This creates a concrete configured MQTT point-cloud downsample factor of four. This creates a concrete

View File

@ -226,6 +226,8 @@ quality line. It is not repaired or hidden by replay.
source point; raw replay remains unchanged. source point; raw replay remains unchanged.
- [x] Add a bounded point-aligned frame API and browser 3D review for - [x] Add a bounded point-aligned frame API and browser 3D review for
intensity, current ground, candidate ground and disagreement. intensity, current ground, candidate ground and disagreement.
- [x] Separate the 66-frame indoor contract slice from a named RAVNOVES00
field review with five camera-bound urban windows and accumulated map clouds.
- [x] Record the K1 3.0.2 internal MID-360/LIO field path from static, - [x] Record the K1 3.0.2 internal MID-360/LIO field path from static,
redacted firmware evidence without changing device state. redacted firmware evidence without changing device state.
- [x] Bind physical height, applied vertical-origin offset and evidence class - [x] Bind physical height, applied vertical-origin offset and evidence class
@ -255,6 +257,26 @@ latency remains 0.42 ms. Algorithm IoU is 4.07% p50 and disagreement is 19.13%
p50. The run is useful for visual review, but its evidence class is p50. The run is useful for visual review, but its evidence class is
`operator-estimated`, so input acceptance and production promotion stay false. `operator-estimated`, so input acceptance and production promotion stay false.
The operator-facing field generation is
`lidar-field-review-57f359dae336f06962e3a29e69e2da1bb8365f9af46d5d3173609f92d43db1ef`.
It uses the immutable RAVNOVES00 E10 derivative
`e10-lidar-pack-5da0396d32a27f9d1ca537cc2e8a371d386078d6f0dc71737b78620992af9625`
over `135.365857292195.334857292` session seconds. The source has 526
available LiDAR samples and 1,182,292 points. Five central urban windows cover
roads, facades, sidewalks, parked vehicles and vegetation. Each window:
- retains the exact source interval and camera preview identity;
- computes both ground masks independently on every original LiDAR sample;
- accumulates those point-aligned results only afterwards in the map frame;
- reports 5368 source samples and 98,538172,027 source points;
- publishes a deterministic uniform display sample of at most 80,000 points.
This field review fixes the operator-context problem of the indoor technical
slice; it does not upgrade evidence. The E10 derivative remains intensity-free,
vendor-mapped and without independent ground labels. Its contract is therefore
`missioncore.lidar-field-review/v1`, `status=diagnostic-only`,
`decision.status=visual-review-only` and `production_promotion=false`.
The result is an evidence-backed **do-not-promote** decision for the current K1 The result is an evidence-backed **do-not-promote** decision for the current K1
feed. Patchwork++ is fast, but its input model assumes a sensor-centric scan feed. Patchwork++ is fast, but its input model assumes a sensor-centric scan
and physical sensor height. K1 `lio_pcl` is a vendor-mapped increment, its and physical sensor height. K1 `lio_pcl` is a vendor-mapped increment, its

View File

@ -0,0 +1,182 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
import tempfile
from pathlib import Path
from k1link.compute import (
PATCHWORKPP_SOURCE_COMMIT,
PATCHWORKPP_SOURCE_TAG,
RAVNOVES00_CENTRAL_WINDOWS,
E10LidarFieldSource,
GroundBenchmarkProfile,
LidarFieldReviewV1,
PatchworkPPGroundSegmenter,
build_lidar_field_review,
)
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Build accumulated RAVNOVES00 central-urban LiDAR ground review "
"with content-bound camera context previews."
)
)
parser.add_argument("source_pack", type=Path)
parser.add_argument("camera_epoch", type=Path)
parser.add_argument("output_root", type=Path)
parser.add_argument("--ffmpeg", default="ffmpeg")
parser.add_argument("--patchwork-module", default="pypatchworkpp")
parser.add_argument("--patchwork-source-tag", default=PATCHWORKPP_SOURCE_TAG)
parser.add_argument(
"--patchwork-source-commit",
default=PATCHWORKPP_SOURCE_COMMIT,
)
parser.add_argument("--sensor-height-m", type=float, default=1.27)
parser.add_argument("--map-vertical-origin-offset-m", type=float, default=1.27)
return parser.parse_args()
def _preview_segments(epoch: Path) -> dict[str, Path]:
resolved = epoch.expanduser().resolve(strict=True)
init = resolved / "init.mp4"
index_path = resolved / "index.jsonl"
if not init.is_file() or init.is_symlink() or not index_path.is_file():
raise RuntimeError("Camera epoch is incomplete")
required = {
window.preview_source_frame_index + 1: window.key for window in RAVNOVES00_CENTRAL_WINDOWS
}
found: dict[str, Path] = {}
with index_path.open(encoding="utf-8") as stream:
for line in stream:
value = json.loads(line)
sequence = value.get("sequence") if isinstance(value, dict) else None
if sequence not in required:
continue
relative = value.get("path")
expected_sha256 = value.get("sha256")
expected_length = value.get("length")
if (
value.get("schema_version") != "missioncore.camera-recording-index/v1"
or relative != f"segments/{sequence}.m4s"
or not isinstance(expected_sha256, str)
or not isinstance(expected_length, int)
):
raise RuntimeError("Camera preview segment index is invalid")
segment = resolved / relative
if (
segment.is_symlink()
or not segment.is_file()
or segment.stat().st_size != expected_length
or _sha256(segment) != expected_sha256
):
raise RuntimeError("Camera preview segment failed integrity")
found[required[sequence]] = segment
if set(found) != set(required.values()):
raise RuntimeError("Camera preview segments are incomplete")
return found
def _extract_previews(
ffmpeg: str,
epoch: Path,
output: Path,
) -> dict[str, Path]:
init = epoch.expanduser().resolve(strict=True) / "init.mp4"
segments = _preview_segments(epoch)
previews: dict[str, Path] = {}
for key, segment in segments.items():
target = output / f"{key}.jpg"
subprocess.run(
[
ffmpeg,
"-hide_banner",
"-loglevel",
"error",
"-i",
f"concat:{init}|{segment}",
"-frames:v",
"1",
"-vf",
"scale=640:480",
"-q:v",
"3",
str(target),
],
check=True,
)
if not target.is_file() or target.stat().st_size < 1_000:
raise RuntimeError("Camera preview extraction failed")
previews[key] = target
return previews
def main() -> int:
arguments = _arguments()
profile = GroundBenchmarkProfile(
profile_id="ravnoves00-central-urban-operator-height-ground-review/v1",
patchwork_sensor_height_proxy_m=arguments.sensor_height_m,
patchwork_map_vertical_origin_offset_m=(arguments.map_vertical_origin_offset_m),
patchwork_height_evidence="operator-estimated",
)
source = E10LidarFieldSource(arguments.source_pack)
try:
patchwork = PatchworkPPGroundSegmenter.load(
profile=profile,
module_name=arguments.patchwork_module,
source_tag=arguments.patchwork_source_tag,
source_commit=arguments.patchwork_source_commit,
)
with tempfile.TemporaryDirectory(prefix="missioncore-lidar-field-review-") as value:
previews = _extract_previews(
arguments.ffmpeg,
arguments.camera_epoch,
Path(value),
)
output = build_lidar_field_review(
source,
arguments.output_root,
patchwork=patchwork,
profile=profile,
preview_paths=previews,
)
finally:
source.close()
review = LidarFieldReviewV1(output)
try:
print(
json.dumps(
{
"review_id": review.review_id,
"display_name": review.report["display_name"],
"session_id": review.report["session_id"],
"source": review.report["source"],
"selection": review.report["selection"],
"windows": review.report["windows"],
"metrics": review.report["metrics"],
"decision": review.report["decision"],
},
ensure_ascii=False,
indent=2,
)
)
finally:
review.close()
return 0
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -69,6 +69,22 @@ from .lidar_contract import (
lidar_readiness_document, lidar_readiness_document,
sensor_frame_xyzi, sensor_frame_xyzi,
) )
from .lidar_field_review import (
E10_LIDAR_PACK_SCHEMA,
FIELD_REVIEW_ARRAYS_NAME,
FIELD_REVIEW_MANIFEST_NAME,
FIELD_REVIEW_REPORT_NAME,
LIDAR_FIELD_REVIEW_REPORT_SCHEMA,
LIDAR_FIELD_REVIEW_SCHEMA,
LIDAR_FIELD_REVIEW_WINDOW_SCHEMA,
MAX_FIELD_REVIEW_WINDOW_POINTS,
RAVNOVES00_CENTRAL_WINDOWS,
E10LidarFieldSource,
FieldReviewWindowSpec,
LidarFieldReviewV1,
build_lidar_field_review,
lidar_field_review_catalog_item,
)
from .lidar_ground import ( from .lidar_ground import (
DEFAULT_GROUND_BENCHMARK_PROFILE, DEFAULT_GROUND_BENCHMARK_PROFILE,
LIDAR_GROUND_ANNOTATION_TEMPLATE_SCHEMA, LIDAR_GROUND_ANNOTATION_TEMPLATE_SCHEMA,
@ -179,6 +195,9 @@ __all__ = [
"LIDAR_GROUND_BENCHMARK_REPORT_SCHEMA", "LIDAR_GROUND_BENCHMARK_REPORT_SCHEMA",
"LIDAR_GROUND_BENCHMARK_SCHEMA", "LIDAR_GROUND_BENCHMARK_SCHEMA",
"LIDAR_GROUND_FRAME_SCHEMA", "LIDAR_GROUND_FRAME_SCHEMA",
"LIDAR_FIELD_REVIEW_REPORT_SCHEMA",
"LIDAR_FIELD_REVIEW_SCHEMA",
"LIDAR_FIELD_REVIEW_WINDOW_SCHEMA",
"LIDAR_EVIDENCE_PROFILE_SCHEMA", "LIDAR_EVIDENCE_PROFILE_SCHEMA",
"LIDAR_EQUIVALENCE_REPORT_SCHEMA", "LIDAR_EQUIVALENCE_REPORT_SCHEMA",
"LIDAR_QUALITY_REPORT_SCHEMA", "LIDAR_QUALITY_REPORT_SCHEMA",
@ -262,6 +281,17 @@ __all__ = [
"build_lidar_ground_annotation_template", "build_lidar_ground_annotation_template",
"build_lidar_ground_benchmark", "build_lidar_ground_benchmark",
"lidar_ground_frame_detail", "lidar_ground_frame_detail",
"E10_LIDAR_PACK_SCHEMA",
"FIELD_REVIEW_ARRAYS_NAME",
"FIELD_REVIEW_MANIFEST_NAME",
"FIELD_REVIEW_REPORT_NAME",
"MAX_FIELD_REVIEW_WINDOW_POINTS",
"RAVNOVES00_CENTRAL_WINDOWS",
"E10LidarFieldSource",
"FieldReviewWindowSpec",
"LidarFieldReviewV1",
"build_lidar_field_review",
"lidar_field_review_catalog_item",
"DetectionFrame", "DetectionFrame",
"ObjectDetection", "ObjectDetection",
"RecordedPerceptionOverlayError", "RecordedPerceptionOverlayError",

View File

@ -0,0 +1,891 @@
from __future__ import annotations
import hashlib
import json
import os
import re
import shutil
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final
import numpy as np
import numpy.typing as npt
from k1link.device_plugins.xgrids_k1.analyze import (
CalibratedProjectionError,
map_points_to_lidar,
)
from .lidar_ground import (
GroundBenchmarkProfile,
GroundSegmenter,
LidarGroundError,
LocalPercentileGroundSegmenter,
)
LIDAR_FIELD_REVIEW_SCHEMA: Final = "missioncore.lidar-field-review/v1"
LIDAR_FIELD_REVIEW_REPORT_SCHEMA: Final = "missioncore.lidar-field-review-report/v1"
LIDAR_FIELD_REVIEW_WINDOW_SCHEMA: Final = "missioncore.lidar-field-review-window/v1"
E10_LIDAR_PACK_SCHEMA: Final = "missioncore.e10-lidar-replay-pack/v1"
FIELD_REVIEW_ARRAYS_NAME: Final = "field-review.npz"
FIELD_REVIEW_REPORT_NAME: Final = "field-review.json"
FIELD_REVIEW_MANIFEST_NAME: Final = "manifest.json"
MAX_FIELD_REVIEW_WINDOW_POINTS: Final = 80_000
_E10_PACK_ID = re.compile(r"^e10-lidar-pack-[a-f0-9]{64}$")
_FIELD_REVIEW_ID = re.compile(r"^lidar-field-review-[a-f0-9]{64}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_SAFE_KEY = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
@dataclass(frozen=True, slots=True)
class FieldReviewWindowSpec:
key: str
label: str
start_seconds: float
end_seconds: float
preview_source_frame_index: int
def __post_init__(self) -> None:
if (
_SAFE_KEY.fullmatch(self.key) is None
or not self.label.strip()
or len(self.label) > 120
or not np.isfinite((self.start_seconds, self.end_seconds)).all()
or not 0 <= self.start_seconds < self.end_seconds
or self.preview_source_frame_index < 0
):
raise LidarGroundError("LiDAR field-review window is invalid")
def to_dict(self) -> dict[str, object]:
return {
"key": self.key,
"label": self.label,
"start_seconds": self.start_seconds,
"end_seconds": self.end_seconds,
"preview_source_frame_index": self.preview_source_frame_index,
}
RAVNOVES00_CENTRAL_WINDOWS: Final = (
FieldReviewWindowSpec(
key="intersection-facades",
label="Перекрёсток, дорога и фасады",
start_seconds=145.0,
end_seconds=151.0,
preview_source_frame_index=1120,
),
FieldReviewWindowSpec(
key="crossing-parked-vehicles",
label="Переход и припаркованные машины",
start_seconds=153.0,
end_seconds=159.0,
preview_source_frame_index=1200,
),
FieldReviewWindowSpec(
key="long-street",
label="Длинный фасад, тротуар и улица",
start_seconds=167.0,
end_seconds=175.0,
preview_source_frame_index=1350,
),
FieldReviewWindowSpec(
key="sidewalk-vehicles",
label="Тротуар, дома и автомобили",
start_seconds=180.0,
end_seconds=187.0,
preview_source_frame_index=1480,
),
FieldReviewWindowSpec(
key="street-vegetation",
label="Продолжение улицы и растительность",
start_seconds=188.0,
end_seconds=194.0,
preview_source_frame_index=1550,
),
)
class E10LidarFieldSource:
"""Strict reader for the immutable, intensity-free RAVNOVES00 E10 pack."""
def __init__(self, root: Path) -> None:
candidate = root.expanduser().absolute()
if candidate.is_symlink():
raise LidarGroundError("E10 LiDAR source cannot be a symlink")
self.root = candidate.resolve(strict=True)
if not self.root.is_dir() or _E10_PACK_ID.fullmatch(self.root.name) is None:
raise LidarGroundError("E10 LiDAR source id is invalid")
self.manifest = _read_json(self.root / "manifest.json")
self.identity = _object(self.manifest.get("identity"), "E10 LiDAR identity")
identity_sha256 = self.manifest.get("identity_sha256")
artifact = _object(self.manifest.get("artifact"), "E10 LiDAR artifact")
artifact_path = artifact.get("path")
if (
self.manifest.get("schema_version") != E10_LIDAR_PACK_SCHEMA
or self.identity.get("schema_version") != E10_LIDAR_PACK_SCHEMA
or not isinstance(identity_sha256, str)
or _SHA256.fullmatch(identity_sha256) is None
or hashlib.sha256(_canonical_json(self.identity)).hexdigest() != identity_sha256
or self.root.name != f"e10-lidar-pack-{identity_sha256}"
or self.manifest.get("pack_id") != self.root.name
or artifact_path != "lidar-pack.npz"
):
raise LidarGroundError("E10 LiDAR source identity is invalid")
arrays_path = self.root / artifact_path
if (
arrays_path.is_symlink()
or not arrays_path.is_file()
or arrays_path.stat().st_size != artifact.get("byte_length")
or _sha256(arrays_path) != artifact.get("sha256")
):
raise LidarGroundError("E10 LiDAR source artifact is invalid")
self.arrays = np.load(arrays_path, allow_pickle=False)
try:
self._validate_arrays()
except BaseException:
self.close()
raise
self.pack_id = self.root.name
@property
def frame_count(self) -> int:
return int(self.identity["frame_count"])
@property
def point_count(self) -> int:
return int(self.identity["point_count"])
def close(self) -> None:
self.arrays.close()
def _validate_arrays(self) -> None:
required = {
"frame_indices",
"source_frame_indices",
"session_seconds",
"sample_available",
"cloud_offsets",
"cloud_points_map",
"pose_positions_map",
"pose_quaternions_map_from_lidar",
"lidar_camera_delta_ms",
"pose_point_delta_ms",
"intrinsic_fx_fy_cx_cy",
"distortion_kb4",
"t_camera_from_lidar",
}
frame_count = _nonnegative_int(self.identity.get("frame_count"), "E10 frame count")
point_count = _nonnegative_int(self.identity.get("point_count"), "E10 point count")
available = self.arrays["sample_available"]
offsets = self.arrays["cloud_offsets"]
points = self.arrays["cloud_points_map"]
positions = self.arrays["pose_positions_map"]
quaternions = self.arrays["pose_quaternions_map_from_lidar"]
if (
set(self.arrays.files) != required
or self.arrays["frame_indices"].shape != (frame_count,)
or self.arrays["source_frame_indices"].shape != (frame_count,)
or self.arrays["session_seconds"].shape != (frame_count,)
or available.shape != (frame_count,)
or offsets.shape != (frame_count + 1,)
or points.shape != (point_count, 3)
or positions.shape != (frame_count, 3)
or quaternions.shape != (frame_count, 4)
or self.arrays["frame_indices"].dtype != np.dtype("<i8")
or self.arrays["source_frame_indices"].dtype != np.dtype("<i8")
or self.arrays["session_seconds"].dtype != np.dtype("<f8")
or available.dtype != np.dtype("?")
or offsets.dtype != np.dtype("<i8")
or points.dtype != np.dtype("<f4")
or positions.dtype != np.dtype("<f8")
or quaternions.dtype != np.dtype("<f8")
or not np.array_equal(
self.arrays["frame_indices"],
np.arange(frame_count, dtype=np.int64),
)
or not np.all(np.diff(self.arrays["source_frame_indices"]) > 0)
or not np.isfinite(self.arrays["session_seconds"]).all()
or not np.all(np.diff(self.arrays["session_seconds"]) > 0)
or offsets[0] != 0
or offsets[-1] != point_count
or np.any(np.diff(offsets) < 0)
or not np.isfinite(points).all()
or int(np.count_nonzero(available)) != self.identity.get("available_lidar_frames")
):
raise LidarGroundError("E10 LiDAR source arrays are invalid")
counts = np.diff(offsets)
if (
np.any(counts[available] <= 0)
or np.any(counts[~available] != 0)
or not np.isfinite(positions[available]).all()
or not np.isfinite(quaternions[available]).all()
):
raise LidarGroundError("E10 LiDAR source availability is invalid")
class LidarFieldReviewV1:
"""Strict reader for accumulated, path-free LiDAR field-review evidence."""
def __init__(self, root: Path) -> None:
candidate = root.expanduser().absolute()
if candidate.is_symlink():
raise LidarGroundError("LiDAR field review cannot be a symlink")
self.root = candidate.resolve(strict=True)
if not self.root.is_dir() or _FIELD_REVIEW_ID.fullmatch(self.root.name) is None:
raise LidarGroundError("LiDAR field-review id is invalid")
self.manifest = _read_json(self.root / FIELD_REVIEW_MANIFEST_NAME)
self.identity = _object(self.manifest.get("identity"), "field-review identity")
identity_sha256 = self.manifest.get("identity_sha256")
if (
self.manifest.get("schema_version") != LIDAR_FIELD_REVIEW_SCHEMA
or self.identity.get("schema_version") != LIDAR_FIELD_REVIEW_SCHEMA
or not isinstance(identity_sha256, str)
or _SHA256.fullmatch(identity_sha256) is None
or hashlib.sha256(_canonical_json(self.identity)).hexdigest() != identity_sha256
or self.root.name != f"lidar-field-review-{identity_sha256}"
or self.manifest.get("review_id") != self.root.name
):
raise LidarGroundError("LiDAR field-review identity is invalid")
artifacts = _validate_artifacts(self.root, self.manifest.get("artifacts"))
self.arrays = np.load(artifacts["field-review"], allow_pickle=False)
self.report = _read_json(artifacts["field-review-report"])
self.preview_paths = {
role.removeprefix("preview-"): path
for role, path in artifacts.items()
if role.startswith("preview-")
}
try:
self._validate()
except BaseException:
self.close()
raise
self.review_id = self.root.name
def close(self) -> None:
self.arrays.close()
def _validate(self) -> None:
windows = _list(self.report.get("windows"), "field-review windows")
parsed_windows = [
_object(item, f"field-review window {index}") for index, item in enumerate(windows)
]
source = _object(self.report.get("source"), "field-review source")
decision = _object(self.report.get("decision"), "field-review decision")
authority = _object(self.report.get("authority"), "field-review authority")
offsets = self.arrays["window_offsets"]
points = self.arrays["points_xyz_map"]
point_count = _nonnegative_int(
self.identity.get("display_point_count"),
"field-review point count",
)
required = {
"window_offsets",
"points_xyz_map",
"current_ground",
"current_assigned",
"candidate_ground",
"candidate_assigned",
}
if (
self.report.get("schema_version") != LIDAR_FIELD_REVIEW_REPORT_SCHEMA
or self.report.get("review_id") != self.root.name
or self.report.get("status") != "diagnostic-only"
or self.report.get("ground_truth") is not False
or source.get("representation") != "legacy-e10-vendor-map-with-pose"
or source.get("intensity_available") is not False
or source.get("raw_scan_accepted") is not False
or decision.get("status") != "visual-review-only"
or decision.get("production_promotion") is not False
or authority.get("commands_enabled") is not False
or authority.get("navigation_or_safety_accepted") is not False
or len(windows) != self.identity.get("window_count")
or set(self.preview_paths) != {str(item.get("key")) for item in parsed_windows}
or set(self.arrays.files) != required
or offsets.shape != (len(windows) + 1,)
or offsets.dtype != np.dtype("<i8")
or offsets[0] != 0
or offsets[-1] != point_count
or np.any(np.diff(offsets) <= 0)
or points.shape != (point_count, 3)
or points.dtype != np.dtype("<f4")
or not np.isfinite(points).all()
or _logical_sha256(self.arrays) != self.identity.get("logical_content_sha256")
):
raise LidarGroundError("LiDAR field-review content is invalid")
for name in (
"current_ground",
"current_assigned",
"candidate_ground",
"candidate_assigned",
):
value = self.arrays[name]
if value.shape != (point_count,) or value.dtype != np.dtype("u1") or np.any(value > 1):
raise LidarGroundError("LiDAR field-review mask is invalid")
for index, window in enumerate(parsed_windows):
start = int(offsets[index])
end = int(offsets[index + 1])
if (
window.get("index") != index
or _SAFE_KEY.fullmatch(str(window.get("key"))) is None
or window.get("display_point_count") != end - start
or not 0 < end - start <= MAX_FIELD_REVIEW_WINDOW_POINTS
or _nonnegative_int(
window.get("source_lidar_samples"),
"field-review source samples",
)
< 1
or _nonnegative_int(
window.get("source_point_count"),
"field-review source points",
)
< end - start
or not isinstance(window.get("label"), str)
or not window["label"].strip()
):
raise LidarGroundError("LiDAR field-review window is invalid")
def window_detail(self, window_index: int) -> dict[str, object]:
windows = _list(self.report["windows"], "field-review windows")
if not 0 <= window_index < len(windows):
raise IndexError(window_index)
window = _object(windows[window_index], "field-review window")
offsets = self.arrays["window_offsets"]
start = int(offsets[window_index])
end = int(offsets[window_index + 1])
current_ground = self.arrays["current_ground"][start:end]
candidate_ground = self.arrays["candidate_ground"][start:end]
disagreement = (current_ground != candidate_ground).astype(np.uint8)
return {
"schema_version": LIDAR_FIELD_REVIEW_WINDOW_SCHEMA,
"review_id": self.review_id,
"display_name": self.report["display_name"],
"session_id": self.report["session_id"],
"source_pack_id": self.report["source_pack_id"],
"window_index": window_index,
"window_count": len(windows),
"window": window,
"point_count": end - start,
"coordinate_frame": "map",
"distance_unit": "m",
"intensity": {
"available": False,
"reason": "E10 derivative did not retain rgbi/intensity",
},
"points_xyz_m": self.arrays["points_xyz_map"][start:end].astype(np.float64).tolist(),
"masks": {
"current_ground": current_ground.astype(np.int64).tolist(),
"current_assigned": self.arrays["current_assigned"][start:end]
.astype(np.int64)
.tolist(),
"candidate_ground": candidate_ground.astype(np.int64).tolist(),
"candidate_assigned": self.arrays["candidate_assigned"][start:end]
.astype(np.int64)
.tolist(),
"disagreement": disagreement.astype(np.int64).tolist(),
},
"counts": {
"current_ground": int(np.count_nonzero(current_ground)),
"candidate_ground": int(np.count_nonzero(candidate_ground)),
"disagreement": int(np.count_nonzero(disagreement)),
},
"preview_url": (
f"/api/v1/lidar/field-reviews/{self.review_id}/windows/{window_index}/preview"
),
"access": "read-only",
"ground_truth": False,
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
},
}
def build_lidar_field_review(
source: E10LidarFieldSource,
output_root: Path,
*,
patchwork: GroundSegmenter,
profile: GroundBenchmarkProfile,
preview_paths: Mapping[str, Path],
windows: Sequence[FieldReviewWindowSpec] = RAVNOVES00_CENTRAL_WINDOWS,
display_name: str = "RAVNOVES00 · центральный городской интервал",
default_window_index: int = 2,
maximum_display_points: int = MAX_FIELD_REVIEW_WINDOW_POINTS,
) -> Path:
"""Build accumulated field windows without upgrading legacy input evidence."""
if (
not windows
or not 0 <= default_window_index < len(windows)
or not 1 <= maximum_display_points <= MAX_FIELD_REVIEW_WINDOW_POINTS
or set(preview_paths) != {window.key for window in windows}
):
raise LidarGroundError("LiDAR field-review build configuration is invalid")
times = source.arrays["session_seconds"]
available = source.arrays["sample_available"]
source_offsets = source.arrays["cloud_offsets"]
points_map = source.arrays["cloud_points_map"]
positions = source.arrays["pose_positions_map"]
quaternions = source.arrays["pose_quaternions_map_from_lidar"]
source_frame_indices = source.arrays["source_frame_indices"]
current = LocalPercentileGroundSegmenter(profile)
window_arrays: list[dict[str, npt.NDArray[Any]]] = []
window_reports: list[dict[str, object]] = []
current_latency: list[float] = []
candidate_latency: list[float] = []
current_fraction: list[float] = []
candidate_fraction: list[float] = []
disagreement_fraction: list[float] = []
algorithm_iou: list[float] = []
selected_source_point_count = 0
for window_index, spec in enumerate(windows):
source_rows = np.flatnonzero(
available & (times >= spec.start_seconds) & (times <= spec.end_seconds)
)
if source_rows.size == 0:
raise LidarGroundError("LiDAR field-review window has no source samples")
collected_points: list[npt.NDArray[np.float32]] = []
collected_current: list[npt.NDArray[np.uint8]] = []
collected_current_assigned: list[npt.NDArray[np.uint8]] = []
collected_candidate: list[npt.NDArray[np.uint8]] = []
collected_candidate_assigned: list[npt.NDArray[np.uint8]] = []
source_point_count = 0
for source_row in source_rows:
start = int(source_offsets[source_row])
end = int(source_offsets[source_row + 1])
cloud = np.asarray(points_map[start:end], dtype=np.float32)
source_point_count += cloud.shape[0]
current_input = np.zeros((cloud.shape[0], 4), dtype=np.float32)
current_input[:, :3] = cloud
current_result = current.segment(current_input)
try:
position = positions[source_row]
orientation = quaternions[source_row]
points_sensor = map_points_to_lidar(
cloud,
position_map_xyz=(
float(position[0]),
float(position[1]),
float(position[2]),
),
orientation_map_from_lidar_xyzw=(
float(orientation[0]),
float(orientation[1]),
float(orientation[2]),
float(orientation[3]),
),
)
except CalibratedProjectionError as exc:
raise LidarGroundError("LiDAR field-review pose conversion failed") from exc
candidate_input = np.zeros((cloud.shape[0], 4), dtype=np.float32)
candidate_input[:, :3] = points_sensor.astype(np.float32)
if profile.patchwork_map_vertical_origin_offset_m:
candidate_input[:, 2] -= profile.patchwork_map_vertical_origin_offset_m
candidate_result = patchwork.segment(candidate_input)
_segmentation(
current_result.ground_mask,
current_result.assigned_mask,
cloud.shape[0],
"current",
)
_segmentation(
candidate_result.ground_mask,
candidate_result.assigned_mask,
cloud.shape[0],
"candidate",
)
collected_points.append(cloud)
collected_current.append(current_result.ground_mask.astype(np.uint8))
collected_current_assigned.append(current_result.assigned_mask.astype(np.uint8))
collected_candidate.append(candidate_result.ground_mask.astype(np.uint8))
collected_candidate_assigned.append(candidate_result.assigned_mask.astype(np.uint8))
current_latency.append(current_result.latency_ms)
candidate_latency.append(candidate_result.latency_ms)
current_fraction.append(float(np.mean(current_result.ground_mask)))
candidate_fraction.append(float(np.mean(candidate_result.ground_mask)))
disagreement_fraction.append(
float(np.mean(current_result.ground_mask != candidate_result.ground_mask))
)
intersection = int(
np.count_nonzero(current_result.ground_mask & candidate_result.ground_mask)
)
union = int(np.count_nonzero(current_result.ground_mask | candidate_result.ground_mask))
algorithm_iou.append(float(intersection / union) if union else 1.0)
combined_points = np.concatenate(collected_points)
combined_current = np.concatenate(collected_current)
combined_current_assigned = np.concatenate(collected_current_assigned)
combined_candidate = np.concatenate(collected_candidate)
combined_candidate_assigned = np.concatenate(collected_candidate_assigned)
selected_source_point_count += source_point_count
selected = _uniform_indices(combined_points.shape[0], maximum_display_points)
displayed = {
"points_xyz_map": combined_points[selected].astype("<f4"),
"current_ground": combined_current[selected].astype("u1"),
"current_assigned": combined_current_assigned[selected].astype("u1"),
"candidate_ground": combined_candidate[selected].astype("u1"),
"candidate_assigned": combined_candidate_assigned[selected].astype("u1"),
}
window_arrays.append(displayed)
preview_row = int(
np.argmin(
np.abs(source_frame_indices.astype(np.int64) - spec.preview_source_frame_index)
)
)
window_reports.append(
{
"index": window_index,
"key": spec.key,
"label": spec.label,
"start_seconds": spec.start_seconds,
"end_seconds": spec.end_seconds,
"midpoint_seconds": (spec.start_seconds + spec.end_seconds) / 2,
"source_lidar_samples": int(source_rows.shape[0]),
"source_point_count": source_point_count,
"display_point_count": int(selected.shape[0]),
"source_frame_start": int(source_frame_indices[source_rows[0]]),
"source_frame_end": int(source_frame_indices[source_rows[-1]]),
"preview_source_frame_index": int(source_frame_indices[preview_row]),
"preview_session_seconds": float(times[preview_row]),
}
)
offsets = np.concatenate(
(
np.asarray([0], dtype="<i8"),
np.cumsum(
[item["points_xyz_map"].shape[0] for item in window_arrays],
dtype=np.int64,
),
)
).astype("<i8")
arrays: dict[str, npt.NDArray[Any]] = {
"window_offsets": offsets,
"points_xyz_map": np.concatenate([item["points_xyz_map"] for item in window_arrays]).astype(
"<f4"
),
"current_ground": np.concatenate([item["current_ground"] for item in window_arrays]).astype(
"u1"
),
"current_assigned": np.concatenate(
[item["current_assigned"] for item in window_arrays]
).astype("u1"),
"candidate_ground": np.concatenate(
[item["candidate_ground"] for item in window_arrays]
).astype("u1"),
"candidate_assigned": np.concatenate(
[item["candidate_assigned"] for item in window_arrays]
).astype("u1"),
}
logical_content_sha256 = _logical_sha256(arrays)
identity = {
"schema_version": LIDAR_FIELD_REVIEW_SCHEMA,
"source_pack_id": source.pack_id,
"source_pack_identity_sha256": source.manifest["identity_sha256"],
"source_artifact_sha256": source.manifest["artifact"]["sha256"],
"session_id": source.identity["session_id"],
"display_name": display_name,
"windows": [window.to_dict() for window in windows],
"window_count": len(windows),
"default_window_index": default_window_index,
"source_point_count": selected_source_point_count,
"display_point_count": int(arrays["points_xyz_map"].shape[0]),
"sampling": {
"method": "uniform-point-index-per-window",
"maximum_points_per_window": maximum_display_points,
},
"profile": profile.to_dict(),
"providers": {
"current": dict(current.identity),
"candidate": dict(patchwork.identity),
},
"logical_content_sha256": logical_content_sha256,
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
"ground_truth": False,
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
},
}
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
review_id = f"lidar-field-review-{identity_sha256}"
output_parent = output_root.expanduser().resolve()
output_parent.mkdir(mode=0o700, parents=True, exist_ok=True)
output = output_parent / review_id
if output.exists():
existing = LidarFieldReviewV1(output)
existing.close()
return output
report = {
"schema_version": LIDAR_FIELD_REVIEW_REPORT_SCHEMA,
"review_id": review_id,
"display_name": display_name,
"session_id": source.identity["session_id"],
"source_pack_id": source.pack_id,
"status": "diagnostic-only",
"source": {
"timeline_start_seconds": source.identity["timeline_start_seconds"],
"timeline_end_seconds": source.identity["timeline_end_seconds"],
"available_lidar_frames": source.identity["available_lidar_frames"],
"point_count": source.identity["point_count"],
"representation": "legacy-e10-vendor-map-with-pose",
"intensity_available": False,
"raw_scan_accepted": False,
},
"selection": {
"purpose": "operator-readable-central-urban-field-review",
"default_window_index": default_window_index,
"accumulation": "per-source-frame masks accumulated in map frame",
"sampling": identity["sampling"],
},
"windows": window_reports,
"metrics": {
"source_samples": len(current_latency),
"current": {
"provider": dict(current.identity),
"ground_fraction": _distribution(current_fraction),
"latency_ms": _distribution(current_latency),
},
"candidate": {
"provider": dict(patchwork.identity),
"ground_fraction": _distribution(candidate_fraction),
"latency_ms": _distribution(candidate_latency),
},
"comparison": {
"algorithm_to_algorithm_ground_iou": _distribution(algorithm_iou),
"ground_disagreement_fraction": _distribution(disagreement_fraction),
"is_accuracy_metric": False,
},
},
"decision": {
"status": "visual-review-only",
"production_promotion": False,
"reasons": [
"legacy E10 derivative does not retain intensity",
"source remains a vendor-mapped LIO product",
"independent ground labels are missing",
],
},
"ground_truth": False,
"authority": identity["authority"],
}
staging = output_parent / f".{review_id}.{os.getpid()}.incomplete"
staging.mkdir(mode=0o700, exist_ok=False)
try:
arrays_path = staging / FIELD_REVIEW_ARRAYS_NAME
np.savez_compressed(arrays_path, **arrays) # type: ignore[arg-type]
report_path = staging / FIELD_REVIEW_REPORT_NAME
_write_json(report_path, report)
artifacts = [
_artifact("field-review", arrays_path, "application/x-npz"),
_artifact("field-review-report", report_path, "application/json"),
]
for spec in windows:
preview_source = preview_paths[spec.key].expanduser().resolve(strict=True)
if not preview_source.is_file() or preview_source.is_symlink():
raise LidarGroundError("LiDAR field-review preview is invalid")
preview_target = staging / f"preview-{spec.key}.jpg"
shutil.copy2(preview_source, preview_target)
artifacts.append(
_artifact(
f"preview-{spec.key}",
preview_target,
"image/jpeg",
)
)
manifest = {
"schema_version": LIDAR_FIELD_REVIEW_SCHEMA,
"review_id": review_id,
"identity_sha256": identity_sha256,
"identity": identity,
"created_at_utc": _utc_now(),
"classification": "private-derived-lidar-diagnostic",
"ground_truth": False,
"artifacts": artifacts,
}
_write_json(staging / FIELD_REVIEW_MANIFEST_NAME, manifest)
os.replace(staging, output)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
validation = LidarFieldReviewV1(output)
validation.close()
return output
def lidar_field_review_catalog_item(review: LidarFieldReviewV1) -> dict[str, object]:
report = review.report
metrics = _object(report["metrics"], "field-review metrics")
return {
"review_id": review.review_id,
"display_name": report["display_name"],
"session_id": report["session_id"],
"source_pack_id": report["source_pack_id"],
"status": report["status"],
"source": report["source"],
"selection": report["selection"],
"windows": report["windows"],
"metrics": metrics,
"decision": report["decision"],
"created_at_utc": review.manifest.get("created_at_utc"),
"ground_truth": False,
"authority": report["authority"],
}
def _uniform_indices(count: int, maximum: int) -> npt.NDArray[np.int64]:
if count <= maximum:
return np.arange(count, dtype=np.int64)
return np.linspace(0, count - 1, maximum, dtype=np.int64)
def _segmentation(
ground_mask: npt.NDArray[np.bool_],
assigned_mask: npt.NDArray[np.bool_],
point_count: int,
label: str,
) -> None:
if (
ground_mask.shape != (point_count,)
or ground_mask.dtype != np.dtype("?")
or assigned_mask.shape != (point_count,)
or assigned_mask.dtype != np.dtype("?")
or np.any(ground_mask & ~assigned_mask)
):
raise LidarGroundError(f"LiDAR field-review {label} mask is invalid")
def _distribution(values: Sequence[float]) -> dict[str, float | int]:
array = np.asarray(values, dtype=np.float64)
if array.size == 0 or not np.isfinite(array).all():
raise LidarGroundError("LiDAR field-review distribution is invalid")
return {
"sample_count": int(array.shape[0]),
"minimum": float(np.min(array)),
"mean": float(np.mean(array)),
"p50": float(np.percentile(array, 50)),
"p95": float(np.percentile(array, 95)),
"maximum": float(np.max(array)),
}
def _logical_sha256(arrays: Mapping[str, npt.NDArray[Any]]) -> str:
digest = hashlib.sha256()
for name in sorted(arrays):
array = np.ascontiguousarray(arrays[name])
digest.update(name.encode())
digest.update(array.dtype.str.encode())
digest.update(_canonical_json(list(array.shape)))
digest.update(memoryview(array).cast("B"))
return digest.hexdigest()
def _validate_artifacts(root: Path, value: object) -> dict[str, Path]:
artifacts = _list(value, "field-review artifacts")
resolved: dict[str, Path] = {}
for value in artifacts:
item = _object(value, "field-review artifact")
role = item.get("role")
relative = item.get("path")
if (
not isinstance(role, str)
or role in resolved
or not isinstance(relative, str)
or Path(relative).name != relative
or not isinstance(item.get("byte_length"), int)
or isinstance(item.get("byte_length"), bool)
or item["byte_length"] < 1
or _SHA256.fullmatch(str(item.get("sha256"))) is None
):
raise LidarGroundError("LiDAR field-review artifact descriptor is invalid")
path = root / relative
if (
path.is_symlink()
or not path.is_file()
or path.stat().st_size != item["byte_length"]
or _sha256(path) != item["sha256"]
):
raise LidarGroundError("LiDAR field-review artifact is invalid")
resolved[role] = path
if "field-review" not in resolved or "field-review-report" not in resolved:
raise LidarGroundError("LiDAR field-review artifacts are incomplete")
return resolved
def _artifact(role: str, path: Path, media_type: str) -> dict[str, object]:
return {
"role": role,
"path": path.name,
"media_type": media_type,
"byte_length": path.stat().st_size,
"sha256": _sha256(path),
}
def _read_json(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file() or path.stat().st_size > 4_000_000:
raise LidarGroundError("LiDAR field-review JSON artifact is invalid")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise LidarGroundError("LiDAR field-review JSON artifact is unreadable") from exc
return _object(value, "LiDAR field-review JSON")
def _write_json(path: Path, value: Mapping[str, object]) -> None:
path.write_bytes(
json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
indent=2,
allow_nan=False,
).encode()
+ b"\n"
)
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise LidarGroundError(f"{label} must be an object")
return value
def _list(value: object, label: str) -> list[Any]:
if not isinstance(value, list):
raise LidarGroundError(f"{label} must be a list")
return value
def _nonnegative_int(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
raise LidarGroundError(f"{label} must be a non-negative integer")
return value
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _utc_now() -> str:
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")

View File

@ -6,13 +6,15 @@ from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import Any, Final from typing import Any, Final
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query, Response
from k1link.compute import ( from k1link.compute import (
LidarFieldReviewV1,
LidarGroundBenchmarkV1, LidarGroundBenchmarkV1,
LidarGroundError, LidarGroundError,
LidarReplayError, LidarReplayError,
LidarReplayPackV2, LidarReplayPackV2,
lidar_field_review_catalog_item,
lidar_ground_benchmark_catalog_item, lidar_ground_benchmark_catalog_item,
lidar_ground_frame_detail, lidar_ground_frame_detail,
lidar_pack_catalog_item, lidar_pack_catalog_item,
@ -21,8 +23,10 @@ from k1link.compute import (
LIDAR_CATALOG_SCHEMA: Final = "missioncore.lidar-replay-pack-catalog/v1" LIDAR_CATALOG_SCHEMA: Final = "missioncore.lidar-replay-pack-catalog/v1"
LIDAR_GROUND_CATALOG_SCHEMA: Final = "missioncore.lidar-ground-benchmark-catalog/v1" LIDAR_GROUND_CATALOG_SCHEMA: Final = "missioncore.lidar-ground-benchmark-catalog/v1"
LIDAR_FIELD_REVIEW_CATALOG_SCHEMA: Final = "missioncore.lidar-field-review-catalog/v1"
_PACK_ID = re.compile(r"^lidar-replay-pack-[a-f0-9]{64}$") _PACK_ID = re.compile(r"^lidar-replay-pack-[a-f0-9]{64}$")
_BENCHMARK_ID = re.compile(r"^ground-benchmark-[a-f0-9]{64}$") _BENCHMARK_ID = re.compile(r"^ground-benchmark-[a-f0-9]{64}$")
_FIELD_REVIEW_ID = re.compile(r"^lidar-field-review-[a-f0-9]{64}$")
RootProvider = Callable[[], Path | None] RootProvider = Callable[[], Path | None]
@ -36,10 +40,16 @@ def configured_lidar_ground_root() -> Path | None:
return Path(value).expanduser().absolute() if value else None return Path(value).expanduser().absolute() if value else None
def configured_lidar_field_review_root() -> Path | None:
value = os.environ.get("MISSIONCORE_LIDAR_FIELD_REVIEW_ROOT", "").strip()
return Path(value).expanduser().absolute() if value else None
def build_lidar_router( def build_lidar_router(
*, *,
root_provider: RootProvider = configured_lidar_replay_root, root_provider: RootProvider = configured_lidar_replay_root,
ground_root_provider: RootProvider = configured_lidar_ground_root, ground_root_provider: RootProvider = configured_lidar_ground_root,
field_review_root_provider: RootProvider = configured_lidar_field_review_root,
) -> APIRouter: ) -> APIRouter:
router = APIRouter(prefix="/api/v1/lidar", tags=["lidar"]) router = APIRouter(prefix="/api/v1/lidar", tags=["lidar"])
@ -269,4 +279,139 @@ def build_lidar_router(
detail="LiDAR ground frame не прошёл проверку целостности", detail="LiDAR ground frame не прошёл проверку целостности",
) from exc ) from exc
@router.get("/field-reviews")
def list_lidar_field_reviews(
limit: int = Query(default=10, ge=1, le=50),
) -> dict[str, Any]:
root = field_review_root_provider()
if root is None or not root.is_dir():
return {
"schema_version": LIDAR_FIELD_REVIEW_CATALOG_SCHEMA,
"configured": root is not None,
"items": [],
"valid_total": 0,
"invalid_total": 0,
"access": "read-only",
}
items: list[dict[str, object]] = []
invalid_total = 0
candidates = sorted(
(
candidate
for candidate in root.iterdir()
if candidate.is_dir() and _FIELD_REVIEW_ID.fullmatch(candidate.name) is not None
),
key=lambda candidate: candidate.stat().st_mtime_ns,
reverse=True,
)
for candidate in candidates:
try:
review = LidarFieldReviewV1(candidate)
try:
items.append(lidar_field_review_catalog_item(review))
finally:
review.close()
except (LidarGroundError, OSError):
invalid_total += 1
return {
"schema_version": LIDAR_FIELD_REVIEW_CATALOG_SCHEMA,
"configured": True,
"items": items[:limit],
"valid_total": len(items),
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get("/field-reviews/{review_id}/windows/{window_index}")
def get_lidar_field_review_window(
review_id: str,
window_index: int,
) -> dict[str, object]:
if _FIELD_REVIEW_ID.fullmatch(review_id) is None or window_index < 0:
raise HTTPException(
status_code=404,
detail="LiDAR field-review window не найден",
)
root = field_review_root_provider()
if root is None or not root.is_dir():
raise HTTPException(
status_code=503,
detail="LiDAR field-review storage не настроен",
)
candidate = root / review_id
if not candidate.is_dir():
raise HTTPException(
status_code=404,
detail="LiDAR field review не найден",
)
try:
review = LidarFieldReviewV1(candidate)
try:
return review.window_detail(window_index)
finally:
review.close()
except IndexError as exc:
raise HTTPException(
status_code=404,
detail="LiDAR field-review window не найден",
) from exc
except (LidarGroundError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="LiDAR field review не прошёл проверку целостности",
) from exc
@router.get("/field-reviews/{review_id}/windows/{window_index}/preview")
def get_lidar_field_review_preview(
review_id: str,
window_index: int,
) -> Response:
if _FIELD_REVIEW_ID.fullmatch(review_id) is None or window_index < 0:
raise HTTPException(
status_code=404,
detail="LiDAR field-review preview не найден",
)
root = field_review_root_provider()
if root is None or not root.is_dir():
raise HTTPException(
status_code=503,
detail="LiDAR field-review storage не настроен",
)
candidate = root / review_id
if not candidate.is_dir():
raise HTTPException(
status_code=404,
detail="LiDAR field review не найден",
)
try:
review = LidarFieldReviewV1(candidate)
try:
windows = review.report.get("windows")
if not isinstance(windows, list) or not 0 <= window_index < len(windows):
raise IndexError(window_index)
window = windows[window_index]
if not isinstance(window, dict) or not isinstance(window.get("key"), str):
raise LidarGroundError("LiDAR field-review preview key is invalid")
preview = review.preview_paths.get(window["key"])
if preview is None:
raise LidarGroundError("LiDAR field-review preview is missing")
content = preview.read_bytes()
finally:
review.close()
except IndexError as exc:
raise HTTPException(
status_code=404,
detail="LiDAR field-review preview не найден",
) from exc
except (LidarGroundError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="LiDAR field-review preview не прошёл проверку",
) from exc
return Response(
content=content,
media_type="image/jpeg",
headers={"Cache-Control": "private, max-age=31536000, immutable"},
)
return router return router

View File

@ -0,0 +1,238 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import numpy as np
import pytest
from fastapi import APIRouter
from fastapi.routing import APIRoute
from k1link.compute import (
E10_LIDAR_PACK_SCHEMA,
E10LidarFieldSource,
FieldReviewWindowSpec,
GroundBenchmarkProfile,
GroundSegmentation,
LidarFieldReviewV1,
LidarGroundError,
build_lidar_field_review,
)
from k1link.web.lidar_api import build_lidar_router
class _Candidate:
@property
def identity(self) -> dict[str, object]:
return {
"provider_id": "test-patchwork/v1",
"source_commit": "c" * 40,
}
def segment(self, xyzi: np.ndarray) -> GroundSegmentation:
ground = xyzi[:, 0] <= 0.25
assigned = np.ones(xyzi.shape[0], dtype=np.bool_)
return GroundSegmentation(ground, assigned, 0.25)
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _source_pack(root: Path) -> Path:
base_points = np.asarray(
[
[-1.0, -1.0, 0.00],
[-0.5, -1.0, 0.02],
[0.0, -1.0, 0.01],
[0.5, -1.0, 0.03],
[1.0, -1.0, 0.00],
[-1.0, 0.0, 0.01],
[-0.5, 0.0, 0.02],
[0.0, 0.0, 0.04],
[0.5, 0.0, 0.35],
[1.0, 0.0, 0.70],
],
dtype=np.float32,
)
points = np.concatenate(
[base_points + np.asarray([index * 0.1, index, 0], dtype=np.float32) for index in range(4)]
).astype("<f4")
arrays = {
"frame_indices": np.arange(4, dtype="<i8"),
"source_frame_indices": np.asarray([100, 110, 120, 130], dtype="<i8"),
"session_seconds": np.asarray([1.0, 2.0, 3.0, 4.0], dtype="<f8"),
"sample_available": np.ones(4, dtype="?"),
"cloud_offsets": np.asarray([0, 10, 20, 30, 40], dtype="<i8"),
"cloud_points_map": points,
"pose_positions_map": np.zeros((4, 3), dtype="<f8"),
"pose_quaternions_map_from_lidar": np.tile(
np.asarray([0.0, 0.0, 0.0, 1.0], dtype="<f8"),
(4, 1),
),
"lidar_camera_delta_ms": np.zeros(4, dtype="<f8"),
"pose_point_delta_ms": np.zeros(4, dtype="<f8"),
"intrinsic_fx_fy_cx_cy": np.asarray(
[100.0, 100.0, 50.0, 50.0],
dtype="<f8",
),
"distortion_kb4": np.zeros(4, dtype="<f8"),
"t_camera_from_lidar": np.eye(4, dtype="<f8"),
}
identity = {
"schema_version": E10_LIDAR_PACK_SCHEMA,
"session_id": "synthetic-ravnoves00",
"frame_count": 4,
"available_lidar_frames": 4,
"point_count": 40,
"timeline_start_seconds": 1.0,
"timeline_end_seconds": 4.0,
}
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
pack = root / f"e10-lidar-pack-{identity_sha256}"
pack.mkdir(parents=True)
arrays_path = pack / "lidar-pack.npz"
np.savez_compressed(arrays_path, **arrays) # type: ignore[arg-type]
manifest = {
"schema_version": E10_LIDAR_PACK_SCHEMA,
"pack_id": pack.name,
"identity_sha256": identity_sha256,
"identity": identity,
"artifact": {
"path": arrays_path.name,
"media_type": "application/x-npz",
"byte_length": arrays_path.stat().st_size,
"sha256": _sha256(arrays_path),
},
}
(pack / "manifest.json").write_bytes(json.dumps(manifest, sort_keys=True).encode())
return pack
def _endpoint(router: APIRouter, path: str) -> object:
for route in router.routes:
if (
isinstance(route, APIRoute)
and route.path == path
and route.methods is not None
and "GET" in route.methods
):
return route.endpoint
raise AssertionError(f"GET {path} route is missing")
def test_field_review_is_accumulated_path_free_visual_evidence(
tmp_path: Path,
) -> None:
source = E10LidarFieldSource(_source_pack(tmp_path / "source"))
windows = (
FieldReviewWindowSpec("street-a", "Улица A", 0.5, 2.1, 100),
FieldReviewWindowSpec("street-b", "Улица B", 2.5, 4.1, 120),
)
previews = {}
for window in windows:
preview = tmp_path / f"{window.key}.jpg"
preview.write_bytes(b"\xff\xd8synthetic-jpeg\xff\xd9")
previews[window.key] = preview
profile = GroundBenchmarkProfile(
profile_id="synthetic-ravnoves00-field-review/v1",
patchwork_sensor_height_proxy_m=1.27,
patchwork_map_vertical_origin_offset_m=1.27,
patchwork_height_evidence="operator-estimated",
)
try:
output = build_lidar_field_review(
source,
tmp_path / "reviews",
patchwork=_Candidate(),
profile=profile,
preview_paths=previews,
windows=windows,
default_window_index=1,
maximum_display_points=12,
)
finally:
source.close()
review = LidarFieldReviewV1(output)
try:
detail = review.window_detail(1)
assert review.report["status"] == "diagnostic-only"
assert review.report["source"]["intensity_available"] is False
assert review.report["selection"]["default_window_index"] == 1
assert len(review.report["windows"]) == 2
assert review.report["windows"][0]["source_lidar_samples"] == 2
assert detail["point_count"] == 12
intensity = detail["intensity"]
authority = detail["authority"]
assert isinstance(intensity, dict)
assert isinstance(authority, dict)
assert intensity["available"] is False
assert detail["ground_truth"] is False
assert authority["commands_enabled"] is False
assert str(tmp_path) not in repr(detail)
finally:
review.close()
router = build_lidar_router(
root_provider=lambda: None,
ground_root_provider=lambda: None,
field_review_root_provider=lambda: output.parent,
)
catalog_route = _endpoint(router, "/api/v1/lidar/field-reviews")
window_route = _endpoint(
router,
"/api/v1/lidar/field-reviews/{review_id}/windows/{window_index}",
)
preview_route = _endpoint(
router,
"/api/v1/lidar/field-reviews/{review_id}/windows/{window_index}/preview",
)
catalog = catalog_route(limit=10) # type: ignore[operator]
window = window_route(review_id=output.name, window_index=0) # type: ignore[operator]
preview = preview_route(review_id=output.name, window_index=0) # type: ignore[operator]
assert catalog["valid_total"] == 1
assert catalog["items"][0]["display_name"]
assert window["preview_url"].endswith("/windows/0/preview")
assert preview.media_type == "image/jpeg"
assert preview.body.startswith(b"\xff\xd8")
assert str(tmp_path) not in repr({"catalog": catalog, "window": window})
def test_field_review_reader_rejects_tampered_identity(tmp_path: Path) -> None:
source = E10LidarFieldSource(_source_pack(tmp_path / "source"))
window = FieldReviewWindowSpec("street-a", "Улица A", 0.5, 4.1, 100)
preview = tmp_path / "street-a.jpg"
preview.write_bytes(b"\xff\xd8synthetic-jpeg\xff\xd9")
try:
output = build_lidar_field_review(
source,
tmp_path / "reviews",
patchwork=_Candidate(),
profile=GroundBenchmarkProfile(),
preview_paths={window.key: preview},
windows=(window,),
default_window_index=0,
maximum_display_points=12,
)
finally:
source.close()
manifest_path = output / "manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["identity"]["display_name"] = "tampered"
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
with pytest.raises(LidarGroundError, match="identity"):
LidarFieldReviewV1(output)