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
@@ -18,6 +18,7 @@ import { E37Result } from "./E37Result";
import { E38Result } from "./E38Result";
import { E39Result } from "./E39Result";
import { E40Result } from "./E40Result";
import { L3PointPillarsResult } from "./L3PointPillarsResult";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
export { isAdvancedLaboratoryWorkId };
@@ -32,6 +33,10 @@ export function advancedLaboratoryWorkOptions(
): readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] {
const available = new Set(index.map(({ workId }) => workId));
const options: readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] = [
{
id: "l3-pointpillars-visual-audit",
label: "L3 · визуальный аудит PointPillars",
},
{ id: "e31-source-binding", label: "LAB E31 · source binding" },
{ id: "e32-track-geometry", label: "LAB E32 · TrackGeometry v1" },
{ id: "e33-worker-shadow", label: "LAB E33 · worker shadow 1×" },
@@ -82,6 +87,9 @@ export function AdvancedLaboratoryResult({
failedSessionId: string | null;
replayError: string | null;
}) {
if (workId === "l3-pointpillars-visual-audit" && results.l3) {
return <L3PointPillarsResult result={results.l3} />;
}
if (workId === "e40-perception-product-gate" && results.e40) {
return <E40Result rigLabel={rigLabel} result={results.e40} />;
}
@@ -0,0 +1,141 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type {
L3PointPillarsVisualAuditResult,
} from "../../core/laboratory/l3PointPillarsVisualAudit";
import { L3PointPillarsVisualAudit } from "./L3PointPillarsVisualAudit";
function percent(value: number, digits = 3): string {
return `${(value * 100).toLocaleString("ru-RU", {
maximumFractionDigits: digits,
})}%`;
}
export function L3PointPillarsResult({
result,
}: {
result: L3PointPillarsVisualAuditResult;
}) {
const metrics = result.metrics;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="L3 · визуальный аудит PointPillars"
description="Визуальная производная полного KITTI transfer-прогона: исходные LiDAR-точки, независимые truth-боксы и предсказания модели сопоставлены тем же глобальным 3D IoU-контрактом. Производная не меняет метрики и не выдает результат за K1 accuracy."
status="Требуется визуальная проверка"
statusTone="warning"
facts={[
{
label: "Источник",
value: `${result.datasetSourceId} · ${metrics.frameCount.toLocaleString("ru-RU")} кадров`,
},
{
label: "Визуальная выборка",
value: `${result.frames.length} доказательных кадров · lazy-load`,
},
{
label: "Исполнение",
value: "Worker 006 · последовательная производная",
},
{
label: "Полномочия",
value: "Read-only · без navigation/safety acceptance",
},
]}
brief={{
question: "Соответствуют ли измеренные провал переноса и почти сплошная ложная занятость фактической геометрии исходных LiDAR-кадров?",
approach: "Полный sealed run проверен по hash identity. Global score-order matching повторён с исходными порогами IoU, после чего детерминированно выбраны TP-, FP-, FN- и class-coverage кадры. В браузер поступает только выбранный кадр.",
principalResult: `Полный прогон: BEV mAP40 ${percent(metrics.bevMap40)}, 3D mAP40 ${percent(metrics.threeDMap40, 6)}, false occupied ${percent(metrics.falseOccupiedRate)}. Визуальный аудит теперь доступен в 3D и BEV.`,
limitation: "Это cross-domain KITTI probe модели, обученной на proprietary solid-state LiDAR. Он проверяет перенос и корректность измерителя, но не доказывает точность K1, camera-first детектор, навигацию или safety.",
}}
method={{
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "l3-pointpillars-kitti-transfer/visual-audit-v1",
components: [
{
kind: "source",
name: result.sourceRunId,
version: "sealed 3769-frame transfer run",
role: "неизменяемые предсказания и latency",
identitySha256: result.sourceFrameResultsIdentitySha256,
},
{
kind: "source",
name: result.datasetSourceId,
version: "admitted public release",
role: "LiDAR и независимые ориентированные 3D truth-боксы",
identitySha256: result.datasetReleaseIdentitySha256,
},
{
kind: "algorithm",
name: "Global score-order oriented 3D IoU matching",
version: "Car 0.7 · Pedestrian/Cyclist 0.5",
role: "единая классификация TP, FP и FN",
identitySha256: null,
},
{
kind: "runtime",
name: "Worker 006 → Mission Core lazy evidence",
version: result.resultId,
role: "append-only visual derivative без повторного inference",
identitySha256: result.resultId.split("-").at(-1) ?? null,
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="SEALED RUN → ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
title="LiDAR, truth и предсказания PointPillars"
kind="diagnostic-model"
resizable
>
<L3PointPillarsVisualAudit result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Численный transfer gate не пройден; визуальная ревизия открыта"
status="Не допущено"
statusTone="danger"
metrics={[
{
label: "BEV mAP40",
value: percent(metrics.bevMap40),
hint: "полный denominator · public cross-domain",
},
{
label: "3D mAP40",
value: percent(metrics.threeDMap40, 6),
hint: `${metrics.evaluatedBoxCount.toLocaleString("ru-RU")} оценённых боксов`,
},
{
label: "False occupied",
value: percent(metrics.falseOccupiedRate),
hint: `${metrics.modelOutputBoxCount.toLocaleString("ru-RU")} post-NMS · ${metrics.outsideSharedRangeCount.toLocaleString("ru-RU")} вне range`,
},
{
label: "Inference p95",
value: `${metrics.inferenceP95Ms.toLocaleString("ru-RU", {
maximumFractionDigits: 2,
})} мс`,
hint: "Worker 006 · последовательное исполнение",
},
]}
conclusion={{
proved: "Полный cross-domain прогон воспроизводим, его численные артефакты связаны с исходными LiDAR-кадрами, а TP/FP/FN можно проверить в 3D и BEV без повторного inference.",
notProved: "Не доказаны пригодность этой модели для K1, точность camera-first семантики, метрическая геометрия K1 в других условиях, навигация, команды или safety.",
decision: "Не переносить этот публичный PointPillars-кандидат в operational pipeline. Использовать визуальный аудит для проверки природы провала и сохранить архитектуру camera-first semantics + LiDAR metric geometry как основной продуктовый путь.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,256 @@
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import type {
L3VisualBox,
L3VisualFrame,
} from "../../core/laboratory/l3PointPillarsVisualAudit";
export type L3VisualMode = "3d" | "bev";
function tokenColor(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
): THREE.Color {
const value = getComputedStyle(host).getPropertyValue(token).trim();
if (value.startsWith("#")) return new THREE.Color(value);
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return new THREE.Color(red / 255, green / 255, blue / 255);
}
function pointPositions(values: readonly number[]): Float32Array {
const positions = new Float32Array((values.length / 4) * 3);
for (
let source = 0, target = 0;
source < values.length;
source += 4, target += 3
) {
positions[target] = values[source];
positions[target + 1] = values[source + 2];
positions[target + 2] = -values[source + 1];
}
return positions;
}
function boxSegments(box: L3VisualBox): Float32Array {
const [centerX, centerY, centerZ] = box.centerXyzM;
const [length, width, height] = box.sizeLwhM;
const cosine = Math.cos(box.yawRad);
const sine = Math.sin(box.yawRad);
const corners: THREE.Vector3[] = [];
for (const zOffset of [-height / 2, height / 2]) {
for (const [xOffset, yOffset] of [
[-length / 2, -width / 2],
[length / 2, -width / 2],
[length / 2, width / 2],
[-length / 2, width / 2],
]) {
const x = centerX + xOffset * cosine - yOffset * sine;
const y = centerY + xOffset * sine + yOffset * cosine;
corners.push(new THREE.Vector3(x, centerZ + zOffset, -y));
}
}
const edges = [
[0, 1], [1, 2], [2, 3], [3, 0],
[4, 5], [5, 6], [6, 7], [7, 4],
[0, 4], [1, 5], [2, 6], [3, 7],
];
const positions = new Float32Array(edges.length * 6);
edges.forEach(([from, to], index) => {
corners[from].toArray(positions, index * 6);
corners[to].toArray(positions, index * 6 + 3);
});
return positions;
}
function addBoxes(
scene: THREE.Scene,
boxes: readonly L3VisualBox[],
colors: Readonly<Record<L3VisualBox["status"], THREE.Color>>,
opacity: number,
): THREE.LineSegments[] {
return boxes.map((box) => {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
"position",
new THREE.BufferAttribute(boxSegments(box), 3),
);
const material = new THREE.LineBasicMaterial({
color: colors[box.status],
transparent: true,
opacity,
depthTest: true,
depthWrite: false,
});
const lines = new THREE.LineSegments(geometry, material);
scene.add(lines);
return lines;
});
}
export function L3PointPillarsScene({
frame,
mode,
}: {
frame: L3VisualFrame;
mode: L3VisualMode;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const [renderError, setRenderError] = useState<string | null>(null);
useEffect(() => {
const host = hostRef.current;
if (!host) return;
setRenderError(null);
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: "high-performance",
});
} catch {
setRenderError("Браузер не смог открыть WebGL-сцену L3.");
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(
tokenColor(host, "--nodedc-canvas", [5, 5, 6]),
1,
);
renderer.domElement.setAttribute("role", "img");
renderer.domElement.setAttribute(
"aria-label",
`L3 PointPillars: кадр ${frame.frameId}, режим ${mode}`,
);
host.append(renderer.domElement);
const scene = new THREE.Scene();
const positions = pointPositions(frame.pointsXyzi);
const pointsGeometry = new THREE.BufferGeometry();
pointsGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3),
);
const pointsMaterial = new THREE.PointsMaterial({
color: tokenColor(host, "--nodedc-text-secondary", [187, 190, 196]),
size: mode === "bev" ? 1.4 : 1.8,
sizeAttenuation: false,
transparent: true,
opacity: 0.52,
depthWrite: false,
});
scene.add(new THREE.Points(pointsGeometry, pointsMaterial));
const colors: Readonly<Record<L3VisualBox["status"], THREE.Color>> = {
matched: tokenColor(host, "--nodedc-text-primary", [247, 248, 244]),
"false-negative": tokenColor(
host,
"--nodedc-warning-rgb",
[255, 209, 102],
),
"true-positive": tokenColor(
host,
"--nodedc-success-rgb",
[143, 255, 93],
),
"false-positive": tokenColor(
host,
"--nodedc-danger-rgb",
[255, 98, 112],
),
};
const truthLines = addBoxes(scene, frame.truthBoxes, colors, 0.9);
const predictionLines = addBoxes(
scene,
frame.predictionBoxes,
colors,
0.72,
);
const grid = new THREE.GridHelper(
80,
40,
tokenColor(host, "--nodedc-text-muted", [96, 99, 106]),
tokenColor(host, "--nodedc-glass-outline", [48, 50, 56]),
);
const gridMaterials = Array.isArray(grid.material)
? grid.material
: [grid.material];
gridMaterials.forEach((material) => {
material.transparent = true;
material.opacity = 0.22;
material.depthWrite = false;
});
scene.add(grid);
const perspective = new THREE.PerspectiveCamera(52, 1, 0.1, 500);
perspective.position.set(-12, 18, 36);
const orthographic = new THREE.OrthographicCamera(-40, 40, 40, -40, 0.1, 500);
orthographic.position.set(35, 100, 0);
orthographic.up.set(1, 0, 0);
const camera = mode === "bev" ? orthographic : perspective;
camera.lookAt(30, 0, 0);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = false;
controls.enableRotate = mode === "3d";
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.target.set(30, 0, 0);
controls.update();
const render = () => renderer.render(scene, camera);
controls.addEventListener("change", render);
const resize = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
renderer.setSize(width, height, false);
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = width / height;
camera.updateProjectionMatrix();
} else {
const horizontal = 42;
camera.left = -horizontal;
camera.right = horizontal;
camera.top = horizontal / (width / height);
camera.bottom = -horizontal / (width / height);
camera.updateProjectionMatrix();
}
render();
};
const observer = new ResizeObserver(resize);
observer.observe(host);
resize();
return () => {
observer.disconnect();
controls.removeEventListener("change", render);
controls.dispose();
pointsGeometry.dispose();
pointsMaterial.dispose();
[...truthLines, ...predictionLines].forEach((lines) => {
lines.geometry.dispose();
(lines.material as THREE.Material).dispose();
});
grid.geometry.dispose();
gridMaterials.forEach((material) => material.dispose());
renderer.dispose();
renderer.domElement.remove();
};
}, [frame, mode]);
return (
<div className="l3-visual-audit__scene" ref={hostRef}>
{renderError ? (
<div className="l3-visual-audit__state" role="status">
{renderError}
</div>
) : null}
</div>
);
}
@@ -0,0 +1,171 @@
import { useEffect, useState } from "react";
import {
Icon,
IconButton,
Select,
} from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchL3PointPillarsVisualFrame,
type L3PointPillarsVisualAuditResult,
type L3VisualFrame,
} from "../../core/laboratory/l3PointPillarsVisualAudit";
import {
L3PointPillarsScene,
type L3VisualMode,
} from "./L3PointPillarsScene";
function frameLabel(
frame: L3PointPillarsVisualAuditResult["frames"][number],
): string {
return (
`Кадр ${frame.frameId} · TP ${frame.truePositiveCount}`
+ ` · FP ${frame.falsePositiveCount} · FN ${frame.falseNegativeCount}`
);
}
export function L3PointPillarsVisualAudit({
result,
}: {
result: L3PointPillarsVisualAuditResult;
}) {
const [selectedFrameId, setSelectedFrameId] = useState(
result.frames[0]?.frameId ?? "",
);
const [frame, setFrame] = useState<L3VisualFrame | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<L3VisualMode>("3d");
const [expanded, setExpanded] = useState(false);
useEffect(() => {
if (!selectedFrameId) return;
const controller = new AbortController();
setFrame(null);
setLoading(true);
setError(null);
void fetchL3PointPillarsVisualFrame(
result.resultId,
selectedFrameId,
{ signal: controller.signal },
).then((next) => {
if (!controller.signal.aborted) setFrame(next);
}).catch((caught: unknown) => {
if (controller.signal.aborted) return;
setError(
caught instanceof Error
? caught.message
: "Визуальный кадр L3 недоступен.",
);
}).finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedFrameId]);
const selectedIndex = result.frames.findIndex(
({ frameId }) => frameId === selectedFrameId,
);
const navigate = (offset: -1 | 1) => {
if (!result.frames.length || selectedIndex < 0) return;
const index = (
selectedIndex + offset + result.frames.length
) % result.frames.length;
setSelectedFrameId(result.frames[index].frameId);
};
const controls = (
<div className="l3-visual-audit__actions">
<div className="l3-visual-audit__pagination">
<IconButton
label="Предыдущий кадр L3"
onClick={() => navigate(-1)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий кадр L3"
onClick={() => navigate(1)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
label="Выбрать кадр визуального аудита L3"
value={selectedFrameId}
options={result.frames.map((item) => ({
value: item.frameId,
label: frameLabel(item),
}))}
variant="split"
menuWidth="anchor"
onChange={setSelectedFrameId}
/>
</div>
);
const overlay = frame ? (
<div className="l3-visual-audit__overlay">
<div>
<span>Кадр</span>
<strong>{frame.frameId}</strong>
<small>
{frame.sampledPointCount.toLocaleString("ru-RU")} из{" "}
{frame.sharedRangePointCount.toLocaleString("ru-RU")} точек
</small>
</div>
<div>
<span>Сопоставление 3D</span>
<strong>
TP {frame.summary.truePositiveCount}
{" · "}FP {frame.summary.falsePositiveCount}
{" · "}FN {frame.summary.falseNegativeCount}
</strong>
<small>
{frame.summary.inferenceMs.toLocaleString("ru-RU", {
maximumFractionDigits: 2,
})} мс · {frame.summary.outsideSharedRangeCount} вне общего range
</small>
</div>
<div className="l3-visual-audit__legend" aria-label="Легенда L3">
<span data-tone="truth">Truth · совпало</span>
<span data-tone="tp">TP · предсказание</span>
<span data-tone="fp">FP · ложный бокс</span>
<span data-tone="fn">FN · пропущенный truth</span>
</div>
</div>
) : undefined;
return (
<div className="l3-visual-audit">
<LaboratoryEvidenceViewer
label="визуальный аудит PointPillars"
mode={mode}
modes={[
{ value: "3d", label: "3D" },
{ value: "bev", label: "BEV" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={controls}
overlay={overlay}
>
{loading ? (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Проверяем и открываем выбранный кадр L3</span>
</div>
) : error || !frame ? (
<div className="l3-visual-audit__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Визуальный кадр L3 недоступен."}</span>
</div>
) : (
<L3PointPillarsScene frame={frame} mode={mode} />
)}
</LaboratoryEvidenceViewer>
</div>
);
}
@@ -18,6 +18,7 @@ function mergeResults(
next: AdvancedLaboratoryResults,
): AdvancedLaboratoryResults {
return {
l3: next.l3 ?? current.l3,
e31: next.e31 ?? current.e31,
e32: next.e32 ?? current.e32,
e33: next.e33 ?? current.e33,