feat(perception): add PointPillars visual audit

This commit is contained in:
DCCONSTRUCTIONS
2026-07-31 11:47:04 +03:00
parent 8fe6184c52
commit a2cb50d1ad
16 changed files with 2351 additions and 4 deletions
@@ -13,8 +13,10 @@ import {
import { fetchE34TemporalLayerResult } from "./e34TemporalLayer";
import { fetchE35DegradationRecoveryResult } from "./e35DegradationRecovery";
import { fetchE40ProductGateResult } from "./e40ProductGate";
import { fetchL3PointPillarsVisualAudit } from "./l3PointPillarsVisualAudit";
export type AdvancedLaboratoryWorkId =
| "l3-pointpillars-visual-audit"
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
@@ -32,6 +34,7 @@ export interface AdvancedLaboratoryIndexItem {
}
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"l3-pointpillars-visual-audit",
"e31-source-binding",
"e32-track-geometry",
"e33-worker-shadow",
@@ -44,6 +47,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
];
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
"e31-source-binding": "e31-source-qualification",
"e32-track-geometry": "e32-track-geometry",
"e33-worker-shadow": "e33-worker-shadow",
@@ -63,6 +67,7 @@ export function isAdvancedLaboratoryWorkId(
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
return {
l3: null,
e31: null,
e32: null,
e33: null,
@@ -163,7 +168,8 @@ export function advancedLaboratoryResultAvailable(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
): boolean {
return workId === "e31-source-binding" ? results.e31 !== null
return workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
: workId === "e31-source-binding" ? results.e31 !== null
: workId === "e32-track-geometry" ? results.e32 !== null
: workId === "e33-worker-shadow" ? results.e33 !== null
: workId === "e34-temporal-layer" ? results.e34 !== null
@@ -185,7 +191,9 @@ export async function fetchAdvancedLaboratoryResult(
} = {},
): Promise<AdvancedLaboratoryResults> {
const results = emptyAdvancedLaboratoryResults();
if (workId === "e31-source-binding") {
if (workId === "l3-pointpillars-visual-audit") {
results.l3 = await fetchL3PointPillarsVisualAudit({ fetcher, signal });
} else if (workId === "e31-source-binding") {
results.e31 = await fetchOne(
"/api/v1/laboratory/e31/results?limit=1",
parseE31,
@@ -11,6 +11,7 @@ import {
type E40PerceptionProductGateResult,
} from "./e40ProductGate";
import { settledCatalogValue } from "./catalogTransport";
import type { L3PointPillarsVisualAuditResult } from "./l3PointPillarsVisualAudit";
export interface E31LaboratoryResult {
resultId: string;
@@ -238,6 +239,7 @@ export interface E39PerceptionRefinementResult {
}
export interface AdvancedLaboratoryResults {
l3: L3PointPillarsVisualAuditResult | null;
e31: E31LaboratoryResult | null;
e32: E32LaboratoryResult | null;
e33: E33LaboratoryResult | null;
@@ -988,5 +990,5 @@ export async function fetchAdvancedLaboratoryResults({
const e38 = settledCatalogValue(settled[6]);
const e39 = settledCatalogValue(settled[7]);
const e40 = settledCatalogValue(settled[8]);
return { e31, e32, e33, e34, e35, e37, e38, e39, e40 };
return { l3: null, e31, e32, e33, e34, e35, e37, e38, e39, e40 };
}
@@ -0,0 +1,384 @@
import {
AdvancedLaboratoryContractError,
type LaboratoryFetch,
} from "./advancedResults";
export interface L3VisualFrameSummary {
frameId: string;
inferenceMs: number;
predictionCount: number;
evaluatedPredictionCount: number;
outsideSharedRangeCount: number;
truthCount: number;
truePositiveCount: number;
falsePositiveCount: number;
falseNegativeCount: number;
truthClasses: readonly string[];
}
export interface L3VisualBox {
benchmarkClass: string;
centerXyzM: readonly [number, number, number];
sizeLwhM: readonly [number, number, number];
yawRad: number;
status: "matched" | "false-negative" | "true-positive" | "false-positive";
score: number | null;
}
export interface L3VisualFrame {
frameId: string;
summary: L3VisualFrameSummary;
sourcePointCount: number;
sharedRangePointCount: number;
sampledPointCount: number;
pointsXyzi: readonly number[];
truthBoxes: readonly L3VisualBox[];
predictionBoxes: readonly L3VisualBox[];
}
export interface L3PointPillarsVisualAuditResult {
resultId: string;
createdAtUtc: string;
status: "operator-visual-review-required";
sourceRunId: string;
sourceFrameResultsIdentitySha256: string;
datasetSourceId: string;
datasetReleaseIdentitySha256: string;
metrics: {
frameCount: number;
bevMap40: number;
threeDMap40: number;
falseOccupiedRate: number;
inferenceP95Ms: number;
modelOutputBoxCount: number;
evaluatedBoxCount: number;
outsideSharedRangeCount: number;
};
frames: readonly L3VisualFrameSummary[];
}
const RESULT_ID = /^l3-pointpillars-visual-audit-[a-f0-9]{64}$/;
const RUN_ID = /^l3-pointpillars-kitti-[a-f0-9]{64}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const FRAME_ID = /^[0-9]{6}$/;
function objectValue(
value: unknown,
label: string,
): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new AdvancedLaboratoryContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function arrayValue(value: unknown, label: string): readonly unknown[] {
if (!Array.isArray(value)) {
throw new AdvancedLaboratoryContractError(`${label}: ожидался массив.`);
}
return value;
}
function exact(value: unknown, expected: string, label: string): string {
if (value !== expected) {
throw new AdvancedLaboratoryContractError(`${label}: нарушен контракт.`);
}
return expected;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new AdvancedLaboratoryContractError(`${label}: ожидалась строка.`);
}
return value;
}
function numberValue(value: unknown, label: string, minimum = 0): number {
if (
typeof value !== "number"
|| !Number.isFinite(value)
|| value < minimum
) {
throw new AdvancedLaboratoryContractError(`${label}: неверное число.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isInteger(parsed)) {
throw new AdvancedLaboratoryContractError(`${label}: ожидалось целое.`);
}
return parsed;
}
function tuple3(
value: unknown,
label: string,
positive = false,
): readonly [number, number, number] {
const values = arrayValue(value, label);
if (values.length !== 3) {
throw new AdvancedLaboratoryContractError(`${label}: ожидалось 3 числа.`);
}
return [
numberValue(values[0], `${label}[0]`, positive ? Number.MIN_VALUE : -Infinity),
numberValue(values[1], `${label}[1]`, positive ? Number.MIN_VALUE : -Infinity),
numberValue(values[2], `${label}[2]`, positive ? Number.MIN_VALUE : -Infinity),
];
}
function parseSummary(value: unknown): L3VisualFrameSummary {
const item = objectValue(value, "L3 visual frame");
const frameId = stringValue(item.frame_id, "L3 frame_id");
if (!FRAME_ID.test(frameId)) {
throw new AdvancedLaboratoryContractError("L3 frame_id: неверный формат.");
}
const truthClasses = arrayValue(item.truth_classes, "L3 truth_classes")
.map((entry) => stringValue(entry, "L3 truth class"));
if (
truthClasses.some(
(entry) => !["Car", "Pedestrian", "Cyclist"].includes(entry),
)
) {
throw new AdvancedLaboratoryContractError("L3 truth class: неизвестен.");
}
return {
frameId,
inferenceMs: numberValue(item.inference_ms, "L3 inference_ms", Number.MIN_VALUE),
predictionCount: integerValue(item.prediction_count, "L3 prediction_count"),
evaluatedPredictionCount: integerValue(
item.evaluated_prediction_count,
"L3 evaluated_prediction_count",
),
outsideSharedRangeCount: integerValue(
item.outside_shared_range_count,
"L3 outside_shared_range_count",
),
truthCount: integerValue(item.truth_count, "L3 truth_count"),
truePositiveCount: integerValue(
item.true_positive_count,
"L3 true_positive_count",
),
falsePositiveCount: integerValue(
item.false_positive_count,
"L3 false_positive_count",
),
falseNegativeCount: integerValue(
item.false_negative_count,
"L3 false_negative_count",
),
truthClasses,
};
}
function parseBox(value: unknown, truth: boolean): L3VisualBox {
const box = objectValue(value, "L3 visual box");
const benchmarkClass = stringValue(
box.benchmark_class,
"L3 benchmark_class",
);
if (!["Car", "Pedestrian", "Cyclist"].includes(benchmarkClass)) {
throw new AdvancedLaboratoryContractError("L3 box class: неизвестен.");
}
const status = stringValue(box.status, "L3 box status");
const allowed = truth
? ["matched", "false-negative"]
: ["true-positive", "false-positive"];
if (!allowed.includes(status)) {
throw new AdvancedLaboratoryContractError("L3 box status: неизвестен.");
}
return {
benchmarkClass,
centerXyzM: tuple3(box.center_xyz_m, "L3 center_xyz_m"),
sizeLwhM: tuple3(box.size_lwh_m, "L3 size_lwh_m", true),
yawRad: numberValue(box.yaw_rad, "L3 yaw_rad", -Infinity),
status: status as L3VisualBox["status"],
score: truth ? null : numberValue(box.score, "L3 score"),
};
}
function parseResult(value: unknown): L3PointPillarsVisualAuditResult {
const result = objectValue(value, "L3 visual result");
exact(
result.schema_version,
"missioncore.l3-pointpillars-visual-audit-result/v1",
"L3 result.schema_version",
);
exact(result.access, "read-only", "L3 result.access");
const resultId = stringValue(result.result_id, "L3 result_id");
const sourceRunId = stringValue(result.source_run_id, "L3 source_run_id");
const frameIdentity = stringValue(
result.source_frame_results_identity_sha256,
"L3 frame identity",
);
const datasetIdentity = stringValue(
result.dataset_release_identity_sha256,
"L3 dataset identity",
);
if (
!RESULT_ID.test(resultId)
|| !RUN_ID.test(sourceRunId)
|| !SHA256.test(frameIdentity)
|| !SHA256.test(datasetIdentity)
) {
throw new AdvancedLaboratoryContractError(
"L3 result: нарушена идентичность.",
);
}
const metrics = objectValue(result.metrics, "L3 metrics");
const aggregates = objectValue(metrics.aggregates, "L3 aggregates");
const volume = objectValue(
aggregates.prediction_volume,
"L3 prediction_volume",
);
const latency = objectValue(
aggregates.inference_latency_ms,
"L3 latency",
);
const frames = arrayValue(result.frames, "L3 frames").map(parseSummary);
if (
!frames.length
|| frames.length > 24
|| new Set(frames.map(({ frameId }) => frameId)).size !== frames.length
) {
throw new AdvancedLaboratoryContractError(
"L3 frames: нарушен ограниченный каталог.",
);
}
return {
resultId,
createdAtUtc: stringValue(result.created_at_utc, "L3 created_at_utc"),
status: exact(
result.status,
"operator-visual-review-required",
"L3 status",
) as "operator-visual-review-required",
sourceRunId,
sourceFrameResultsIdentitySha256: frameIdentity,
datasetSourceId: stringValue(result.dataset_source_id, "L3 dataset_source_id"),
datasetReleaseIdentitySha256: datasetIdentity,
metrics: {
frameCount: integerValue(metrics.frame_count, "L3 frame_count"),
bevMap40: numberValue(aggregates.bev_map40, "L3 bev_map40"),
threeDMap40: numberValue(aggregates["3d_map40"], "L3 3d_map40"),
falseOccupiedRate: numberValue(
aggregates.false_occupied_rate,
"L3 false_occupied_rate",
),
inferenceP95Ms: numberValue(latency.p95, "L3 latency.p95"),
modelOutputBoxCount: integerValue(
volume.model_output_box_count,
"L3 model_output_box_count",
),
evaluatedBoxCount: integerValue(
volume.evaluated_box_count,
"L3 evaluated_box_count",
),
outsideSharedRangeCount: integerValue(
volume.outside_shared_range_count,
"L3 outside_shared_range_count",
),
},
frames,
};
}
export async function fetchL3PointPillarsVisualAudit({
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise<L3PointPillarsVisualAuditResult | null> {
const response = await fetcher(
"/api/v1/laboratory/l3/pointpillars-visual-audits/results?limit=1",
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new AdvancedLaboratoryContractError(
`L3 visual audit недоступен: HTTP ${response.status}.`,
);
}
const catalog = objectValue(await response.json(), "L3 result catalog");
exact(
catalog.schema_version,
"missioncore.l3-pointpillars-visual-audit-catalog-results/v1",
"L3 catalog.schema_version",
);
exact(catalog.access, "read-only", "L3 catalog.access");
const items = arrayValue(catalog.items, "L3 catalog.items");
if (items.length > 1) {
throw new AdvancedLaboratoryContractError("L3 catalog: лишние результаты.");
}
return items.length ? parseResult(items[0]) : null;
}
export async function fetchL3PointPillarsVisualFrame(
resultId: string,
frameId: string,
{
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {},
): Promise<L3VisualFrame> {
if (!RESULT_ID.test(resultId) || !FRAME_ID.test(frameId)) {
throw new AdvancedLaboratoryContractError(
"L3 visual frame: неверная идентичность.",
);
}
const response = await fetcher(
`/api/v1/laboratory/l3/pointpillars-visual-audits/${resultId}/frames/${frameId}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new AdvancedLaboratoryContractError(
`L3 visual frame недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "L3 visual frame");
exact(
payload.schema_version,
"missioncore.l3-pointpillars-visual-frame/v1",
"L3 frame.schema_version",
);
exact(payload.access, "read-only", "L3 frame.access");
exact(payload.frame_id, frameId, "L3 frame.frame_id");
const points = objectValue(payload.points, "L3 points");
exact(points.layout, "flat-xyzi", "L3 points.layout");
const pointValues = arrayValue(points.values, "L3 points.values").map(
(value, index) => numberValue(value, `L3 points[${index}]`, -Infinity),
);
const sampledPointCount = integerValue(
points.sampled_point_count,
"L3 sampled_point_count",
);
if (sampledPointCount > 12_000 || pointValues.length !== sampledPointCount * 4) {
throw new AdvancedLaboratoryContractError(
"L3 points: нарушен ограниченный массив.",
);
}
return {
frameId,
summary: parseSummary(payload.summary),
sourcePointCount: integerValue(
points.source_point_count,
"L3 source_point_count",
),
sharedRangePointCount: integerValue(
points.shared_range_point_count,
"L3 shared_range_point_count",
),
sampledPointCount,
pointsXyzi: pointValues,
truthBoxes: arrayValue(payload.truth_boxes, "L3 truth_boxes")
.map((box) => parseBox(box, true)),
predictionBoxes: arrayValue(
payload.prediction_boxes,
"L3 prediction_boxes",
).map((box) => parseBox(box, false)),
};
}