feat(lidar): add RAVNOVES field review
This commit is contained in:
@@ -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.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user