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
@@ -147,6 +147,106 @@ export interface LidarGroundFrame {
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 LidarReplayApiError extends Error {
@@ -162,7 +262,10 @@ type LidarFetch = (
const SAFE_PACK_ID = /^lidar-replay-pack-[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_FIELD_KEY = /^[a-z0-9][a-z0-9-]{0,63}$/;
const SHA256 = /^[a-f0-9]{64}$/;
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(
response: Response,
fallback: string,
@@ -739,3 +1194,48 @@ export async function fetchLidarGroundFrame(
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.",
),
);
}