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