feat(perception): add PointPillars visual audit
This commit is contained in:
@@ -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)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user