fix(polygon): separate dataset sources from runs

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 18:40:56 +03:00
parent 2e5c4ba1c5
commit e97573e221
13 changed files with 1036 additions and 658 deletions
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import {
Button,
GlassSurface,
@@ -7,16 +7,13 @@ import {
import {
fetchDatasetGatewayCatalog,
fetchDatasetGroundComparison,
fetchDatasetNativeScanPreview,
type DatasetGatewayCatalog,
type DatasetGroundComparison,
type DatasetNativeScanPreview,
} from "../core/lidar/datasetGateway";
import {
LidarGroundPointCloud,
type LidarGroundViewMode,
} from "./LidarGroundPointCloud";
fetchPolygonRunCatalog,
type PolygonRunCatalog,
} from "../core/polygon/runArchive";
import { GooseDatasetReview } from "./GooseDatasetReview";
const representationLabels: Record<string, string> = {
"native-scan": "Исходный скан",
@@ -51,18 +48,22 @@ function formatBytes(value: number): string {
}).format(value / 1_000_000_000);
}
function isGooseQualificationRun(
item: PolygonRunCatalog["items"][number],
): boolean {
return item.kind === "replay_shadow"
&& item.scenarioGeneration.startsWith("goose-3d");
}
export function DatasetGatewayWorkspace() {
const [catalog, setCatalog] = useState<DatasetGatewayCatalog | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [reloadGeneration, setReloadGeneration] = useState(0);
const [previewOpen, setPreviewOpen] = useState(false);
const [preview, setPreview] = useState<DatasetNativeScanPreview | null>(null);
const [comparison, setComparison] = useState<DatasetGroundComparison | null>(null);
const [previewError, setPreviewError] = useState<string | null>(null);
const [comparisonError, setComparisonError] = useState<string | null>(null);
const [previewMode, setPreviewMode] =
useState<LidarGroundViewMode>("semantic");
const [runCatalog, setRunCatalog] = useState<PolygonRunCatalog | null>(null);
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
const [runError, setRunError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
@@ -93,64 +94,29 @@ export function DatasetGatewayWorkspace() {
}, [catalog]);
useEffect(() => {
if (!previewOpen || preview) return;
const controller = new AbortController();
setPreviewError(null);
void fetchDatasetNativeScanPreview({ signal: controller.signal })
setRunError(null);
void fetchPolygonRunCatalog({ signal: controller.signal })
.then((value) => {
if (!controller.signal.aborted) setPreview(value);
if (controller.signal.aborted) return;
setRunCatalog(value);
const requested = new URLSearchParams(window.location.search).get("run");
const gooseRuns = value.items.filter(isGooseQualificationRun);
const selected = gooseRuns.find((item) => item.runId === requested)
?? gooseRuns[0]
?? null;
setSelectedRunId(selected?.runId ?? null);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setPreviewError(errorMessage(loadError));
if (!controller.signal.aborted) setRunError(errorMessage(loadError));
});
return () => controller.abort();
}, [previewOpen, preview]);
useEffect(() => {
if (!previewOpen || comparison) return;
const controller = new AbortController();
setComparisonError(null);
void fetchDatasetGroundComparison({ signal: controller.signal })
.then((value) => {
if (!controller.signal.aborted) setComparison(value);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) {
setComparisonError(errorMessage(loadError));
}
});
return () => controller.abort();
}, [comparison, previewOpen]);
const previewFrame = useMemo(() => {
if (!preview) return null;
const emptyMask = new Array<number>(preview.pointCount).fill(0);
const comparisonAligned = (
comparison
&& comparison.frameId === preview.frameId
&& comparison.pointCount === preview.pointCount
) ? comparison : null;
return {
pointCount: preview.pointCount,
pointsXyzM: preview.pointsXyzM,
intensity0To255: preview.remission0To255,
semanticRgb0To255: preview.semanticRgb0To255,
masks: {
currentGround: comparisonAligned?.currentGround ?? emptyMask,
currentAssigned: comparisonAligned?.evaluated ?? emptyMask,
candidateGround: comparisonAligned?.patchworkGround ?? emptyMask,
candidateAssigned: comparisonAligned?.patchworkAssigned ?? emptyMask,
disagreement: comparisonAligned?.currentDisagreement ?? emptyMask,
candidateDisagreement:
comparisonAligned?.patchworkDisagreement ?? emptyMask,
groundTruthGround: preview.groundTruthGround,
},
};
}, [comparison, preview]);
}, []);
const downloadProgress = catalog?.source.archive
? catalog.source.archive.bytesTransferred / catalog.source.archive.totalBytes
: null;
const gooseRuns = runCatalog?.items.filter(isGooseQualificationRun) ?? [];
return (
<div className="standard-workspace dataset-workspace">
@@ -213,9 +179,9 @@ export function DatasetGatewayWorkspace() {
<span>PUBLIC · OFF-ROAD · LABELED</span>
<h3>{catalog.source.displayName}</h3>
<p>
Один оборот VLS-128 с point-wise semantic и instance labels.
Первый baseline для ground segmentation и последующих
LiDAR-native моделей.
Validation split: восемь отдельных поездок и 961 разреженный
размеченный оборот VLS-128. Это коллекция native scans для
perception, а не непрерывное видео или траектория машины.
</p>
</div>
</div>
@@ -266,12 +232,12 @@ export function DatasetGatewayWorkspace() {
disabled={catalog.source.admissionStatus !== "frame-ready"}
title={
catalog.source.admissionStatus === "frame-ready"
? "Открыть первый размеченный native scan"
: "Первый реальный кадр ещё не импортирован"
? "Открыть все доступные последовательности и scans"
: "Размеченные scans ещё не импортированы"
}
onClick={() => setPreviewOpen((value) => !value)}
>
{previewOpen ? "Закрыть" : "Открыть"}
{previewOpen ? "Закрыть источник" : "Открыть источник"}
</Button>
<Button
size="compact"
@@ -286,145 +252,34 @@ export function DatasetGatewayWorkspace() {
</article>
</GlassSurface>
{previewOpen ? (
<section className="dataset-preview" aria-label="Первый GOOSE native scan">
<header>
<div>
<span className="section-eyebrow">NATIVE SCAN · GROUND TRUTH</span>
<h2>
{preview
? `${preview.frameId} · ${preview.sourcePointCount.toLocaleString("ru-RU")} точек`
: "Первый размеченный кадр"}
</h2>
{previewOpen && selectedRunId ? (
<>
{gooseRuns.length > 0 ? (
<div className="dataset-result-select">
<div>
<span className="section-eyebrow">НАЛОЖЕНИЕ РЕЗУЛЬТАТА</span>
<strong>Квалификационный прогон</strong>
</div>
<select
aria-label="Выбрать прогон для наложения"
value={selectedRunId}
onChange={(event) => setSelectedRunId(event.target.value)}
>
{gooseRuns.map((item) => (
<option key={item.runId} value={item.runId}>
{item.scenarioGeneration} · {item.runId}
</option>
))}
</select>
</div>
<div className="lidar-ground-modes" aria-label="Режим окраски">
{([
["semantic", "Классы"],
["ground-truth", "Ground"],
...(comparison
? ([
["current", "Current"],
["candidate", "Patchwork++"],
["disagreement", "Ошибки Current"],
["candidate-disagreement", "Ошибки PW++"],
] as const)
: []),
["intensity", "Remission"],
] as const).map(([mode, label]) => (
<button
key={mode}
type="button"
aria-pressed={previewMode === mode}
onClick={() => setPreviewMode(mode)}
>
{label}
</button>
))}
</div>
</header>
{comparison ? (
<dl className="dataset-preview__metrics">
<div>
<dt>Current · IoU</dt>
<dd>{(comparison.metrics.current.groundIou * 100).toFixed(1)}%</dd>
</div>
<div>
<dt>Current · Recall</dt>
<dd>{(comparison.metrics.current.recall * 100).toFixed(1)}%</dd>
</div>
<div>
<dt>Current · Natural</dt>
<dd>
{(comparison.metrics.current.naturalGroundRecall * 100).toFixed(1)}%
</dd>
</div>
<div>
<dt>Current · Latency</dt>
<dd>{comparison.latencyMs.current.toFixed(0)} ms</dd>
</div>
<div>
<dt>Patchwork++ · IoU</dt>
<dd>
{(comparison.metrics.patchworkpp.groundIou * 100).toFixed(1)}%
</dd>
</div>
<div>
<dt>Patchwork++ · Recall</dt>
<dd>{(comparison.metrics.patchworkpp.recall * 100).toFixed(1)}%</dd>
</div>
<div>
<dt>Patchwork++ · Natural</dt>
<dd>
{(comparison.metrics.patchworkpp.naturalGroundRecall * 100).toFixed(1)}%
</dd>
</div>
<div>
<dt>Patchwork++ · Latency</dt>
<dd>{comparison.latencyMs.patchworkpp.toFixed(1)} ms</dd>
</div>
</dl>
) : comparisonError ? (
<p className="dataset-preview__comparison-note">
Current baseline пока не рассчитан: {comparisonError}
</p>
) : null}
{previewFrame ? (
<>
<LidarGroundPointCloud frame={previewFrame} mode={previewMode} />
<div className="dataset-preview__legend">
{previewMode === "semantic" ? (
preview?.classes.map((item) => (
<span key={item.labelId}>
<i style={{ background: item.hex }} />
{item.className}
</span>
))
) : previewMode === "ground-truth" ? (
<>
<span><i className="dataset-legend-ground" />ground truth</span>
<span><i className="dataset-legend-other" />остальные точки</span>
</>
) : previewMode === "current" ? (
<>
<span><i className="dataset-legend-ground" />current ground</span>
<span><i className="dataset-legend-other" />current non-ground</span>
</>
) : previewMode === "candidate" ? (
<>
<span><i className="dataset-legend-ground" />Patchwork++ ground</span>
<span><i className="dataset-legend-other" />Patchwork++ non-ground</span>
</>
) : previewMode === "disagreement" ? (
<>
<span><i className="dataset-legend-error" />ошибка Current</span>
<span><i className="dataset-legend-other" />совпадение с labels</span>
</>
) : previewMode === "candidate-disagreement" ? (
<>
<span><i className="dataset-legend-error" />ошибка Patchwork++</span>
<span><i className="dataset-legend-other" />совпадение</span>
</>
) : (
<span>Remission нормализован только для визуального просмотра 0255</span>
)}
</div>
</>
) : (
<div className="lidar-ground-scene-placeholder">
<StatusBadge tone={previewError ? "danger" : "accent"}>
{previewError ? "Preview недоступен" : "Читаем frame"}
</StatusBadge>
<p>{previewError ?? "Проверяем point alignment и разметку."}</p>
</div>
)}
{comparison ? (
<p className="dataset-preview__comparison-note">
Patchwork++ использует опубликованную высоту VLS-128{" "}
{comparison.inputProfile.sensorHeightM.toFixed(2)} м; это
algorithm-specific admission, а не полный vehicle TF и не
normalized scan.
</p>
) : null}
<GooseDatasetReview runId={selectedRunId} />
</>
) : previewOpen ? (
<section className="polygon-review-unavailable">
<StatusBadge tone="warning">Review недоступен</StatusBadge>
<h3>Источник принят, но покадровый preview не опубликован</h3>
<p>{runError ?? "Нет квалификационного прогона с bounded frame-pack."}</p>
</section>
) : null}
@@ -0,0 +1,460 @@
import {
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from "react";
import { StatusBadge } from "@nodedc/ui-react";
import {
fetchGroundReview,
fetchGroundReviewFrame,
type GroundReview,
type GroundReviewFrame,
type GroundReviewFrameSummary,
} from "../core/polygon/groundQualification";
import {
LidarGroundPointCloud,
type LidarGroundViewMode,
} from "./LidarGroundPointCloud";
interface GooseDatasetReviewProps {
runId: string;
}
export interface GooseDatasetSequence {
datasetSequenceId: string;
frames: GroundReviewFrameSummary[];
durationSeconds: number;
minimumGapSeconds: number;
maximumGapSeconds: number;
}
const viewModes: ReadonlyArray<[LidarGroundViewMode, string]> = [
["intensity", "Исходный скан"],
["ground-truth", "Ground truth"],
["current", "Current"],
["candidate", "Patchwork++"],
["disagreement", "Ошибки Current"],
["candidate-disagreement", "Ошибки Patchwork++"],
];
function errorMessage(error: unknown): string {
if (error instanceof Error && error.message.trim()) return error.message;
return "Не удалось открыть последовательности GOOSE.";
}
function secondsBetween(
left: GroundReviewFrameSummary,
right: GroundReviewFrameSummary,
): number {
const delta = BigInt(right.sensorTimestampNs) - BigInt(left.sensorTimestampNs);
return Number(delta) / 1_000_000_000;
}
export function groupGooseReviewFrames(
review: GroundReview,
): GooseDatasetSequence[] {
const groups = new Map<string, GroundReviewFrameSummary[]>();
review.frames.forEach((frame) => {
const frames = groups.get(frame.datasetSequenceId) ?? [];
frames.push(frame);
groups.set(frame.datasetSequenceId, frames);
});
return [...groups.entries()].map(([datasetSequenceId, frames]) => {
const gaps = frames.slice(1).map((frame, index) => (
secondsBetween(frames[index], frame)
));
return {
datasetSequenceId,
frames,
durationSeconds: frames.length > 1
? secondsBetween(frames[0], frames[frames.length - 1])
: 0,
minimumGapSeconds: gaps.length ? Math.min(...gaps) : 0,
maximumGapSeconds: gaps.length ? Math.max(...gaps) : 0,
};
});
}
function sequenceLabel(sequenceId: string): string {
const date = sequenceId.slice(0, 10);
const title = sequenceId.slice(11).replaceAll("_", " ");
return `${date} · ${title}`;
}
function formatDuration(seconds: number): string {
const rounded = Math.max(0, Math.round(seconds));
const minutes = Math.floor(rounded / 60);
const remainder = rounded % 60;
return `${minutes}:${String(remainder).padStart(2, "0")}`;
}
function formatGap(seconds: number): string {
return `${seconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с`;
}
function formatPercent(value: number): string {
return new Intl.NumberFormat("ru-RU", {
style: "percent",
maximumFractionDigits: 1,
}).format(value);
}
function formatDelta(value: number): string {
const points = value * 100;
return `${points >= 0 ? "+" : ""}${points.toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} п.п.`;
}
function frameScene(frame: GroundReviewFrame | null) {
if (!frame) return null;
return {
pointCount: frame.pointCount,
pointsXyzM: frame.pointsXyzM,
intensity0To255: frame.intensity0To255,
masks: {
currentGround: frame.currentGround,
currentAssigned: frame.evaluated,
candidateGround: frame.patchworkGround,
candidateAssigned: frame.evaluated,
disagreement: frame.currentDisagreement,
candidateDisagreement: frame.patchworkDisagreement,
groundTruthGround: frame.groundTruthGround,
},
};
}
function modeExplanation(mode: LidarGroundViewMode): string {
if (mode === "intensity") return "Один sensor-frame оборот VLS-128; цвет — remission.";
if (mode === "ground-truth") return "Публичная point-aligned разметка GOOSE.";
if (mode === "current") return "Результат Current для этого исходного скана.";
if (mode === "candidate") return "Результат Patchwork++ для этого исходного скана.";
if (mode === "disagreement") return "Красным отмечены ошибки Current относительно labels.";
return "Красным отмечены ошибки Patchwork++ относительно labels.";
}
export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
const [review, setReview] = useState<GroundReview | null>(null);
const [error, setError] = useState<string | null>(null);
const [selectedSequenceId, setSelectedSequenceId] = useState<string | null>(null);
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(null);
const [reviewFrame, setReviewFrame] = useState<GroundReviewFrame | null>(null);
const [mode, setMode] = useState<LidarGroundViewMode>("intensity");
const [playing, setPlaying] = useState(false);
const [frameLoading, setFrameLoading] = useState(false);
const frameCache = useRef(new Map<string, GroundReviewFrame>());
useEffect(() => {
const controller = new AbortController();
setError(null);
setReview(null);
setReviewFrame(null);
setPlaying(false);
frameCache.current.clear();
void fetchGroundReview(runId, { signal: controller.signal })
.then((value) => {
if (controller.signal.aborted || !value) return;
setReview(value);
const first = value.frames[0] ?? null;
setSelectedSequenceId(first?.datasetSequenceId ?? null);
setSelectedFrameId(first?.frameId ?? null);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setError(errorMessage(loadError));
});
return () => controller.abort();
}, [runId]);
const sequences = useMemo(
() => review ? groupGooseReviewFrames(review) : [],
[review],
);
const selectedSequence = sequences.find(
(sequence) => sequence.datasetSequenceId === selectedSequenceId,
) ?? sequences[0] ?? null;
const selectedPosition = Math.max(
0,
selectedSequence?.frames.findIndex((frame) => frame.frameId === selectedFrameId) ?? 0,
);
const selectedSummary = selectedSequence?.frames[selectedPosition] ?? null;
const previousSummary = selectedSequence && selectedPosition > 0
? selectedSequence.frames[selectedPosition - 1]
: null;
const gapFromPrevious = selectedSummary && previousSummary
? secondsBetween(previousSummary, selectedSummary)
: null;
useEffect(() => {
if (!selectedFrameId) return;
const cached = frameCache.current.get(selectedFrameId);
if (cached) {
setReviewFrame(cached);
setFrameLoading(false);
return;
}
const controller = new AbortController();
setFrameLoading(true);
void fetchGroundReviewFrame(runId, selectedFrameId, {
signal: controller.signal,
})
.then((frame) => {
if (controller.signal.aborted) return;
frameCache.current.set(frame.frameId, frame);
while (frameCache.current.size > 32) {
const oldest = frameCache.current.keys().next().value as string | undefined;
if (oldest) frameCache.current.delete(oldest);
else break;
}
setReviewFrame(frame);
setFrameLoading(false);
})
.catch((loadError: unknown) => {
if (controller.signal.aborted) return;
setFrameLoading(false);
setError(errorMessage(loadError));
});
return () => controller.abort();
}, [runId, selectedFrameId]);
useEffect(() => {
if (
!playing
|| !selectedSequence
|| !selectedFrameId
|| reviewFrame?.frameId !== selectedFrameId
) return;
const timer = window.setTimeout(() => {
const next = selectedSequence.frames[selectedPosition + 1];
if (!next) {
setPlaying(false);
return;
}
setSelectedFrameId(next.frameId);
}, 700);
return () => window.clearTimeout(timer);
}, [
playing,
reviewFrame?.frameId,
selectedFrameId,
selectedPosition,
selectedSequence,
]);
const scene = useMemo(() => frameScene(reviewFrame), [reviewFrame]);
const selectPosition = (position: number) => {
if (!selectedSequence) return;
const bounded = Math.max(0, Math.min(position, selectedSequence.frames.length - 1));
setPlaying(false);
setSelectedFrameId(selectedSequence.frames[bounded]?.frameId ?? null);
};
const selectSequence = (sequenceId: string) => {
const sequence = sequences.find((candidate) => (
candidate.datasetSequenceId === sequenceId
));
setPlaying(false);
setSelectedSequenceId(sequenceId);
setSelectedFrameId(sequence?.frames[0]?.frameId ?? null);
setReviewFrame(null);
};
if (error) {
return (
<section className="polygon-review-unavailable">
<StatusBadge tone="danger">Источник недоступен</StatusBadge>
<h3>Не удалось открыть GOOSE</h3>
<p>{error}</p>
</section>
);
}
if (!review || !selectedSequence) {
return (
<section className="polygon-review-unavailable">
<StatusBadge tone="accent">Читаем индекс</StatusBadge>
<h3>Открываем последовательности GOOSE</h3>
<p>Загружаем только bounded preview, исходный архив остаётся на worker.</p>
</section>
);
}
return (
<section className="dataset-sequence-review" aria-label="Просмотр GOOSE validation">
<header className="dataset-sequence-review__truth">
<div>
<span className="section-eyebrow">СТРУКТУРА ИСХОДНИКА</span>
<h2>8 отдельных поездок, а не одно видео</h2>
<p>
Validation ZIP содержит {review.frameCount} разреженных размеченных
LiDAR-оборотов. Между ними нет непрерывной траектории машины. Play ниже
ускоренный просмотр выбранной поездки, а не воспроизведение реального времени.
</p>
</div>
<dl>
<div><dt>Последовательностей</dt><dd>{sequences.length}</dd></div>
<div><dt>Размеченных сканов</dt><dd>{review.frameCount}</dd></div>
<div><dt>Preview</dt><dd>{review.previewPoints.toLocaleString("ru-RU")} точек</dd></div>
</dl>
</header>
<div className="dataset-sequence-select">
<label htmlFor="goose-sequence">Последовательность</label>
<select
id="goose-sequence"
value={selectedSequence.datasetSequenceId}
onChange={(event) => selectSequence(event.target.value)}
>
{sequences.map((sequence) => (
<option
key={sequence.datasetSequenceId}
value={sequence.datasetSequenceId}
>
{sequenceLabel(sequence.datasetSequenceId)}
{" · "}{sequence.frames.length} scans
</option>
))}
</select>
<span>
{selectedSequence.frames.length} размеченных сканов · интервал{" "}
{formatGap(selectedSequence.minimumGapSeconds)}{
formatGap(selectedSequence.maximumGapSeconds)
} · охват {formatDuration(selectedSequence.durationSeconds)}
</span>
</div>
<section className="polygon-review-player">
<header className="polygon-review-player__header">
<div>
<span className="section-eyebrow">
SCAN {selectedPosition + 1} / {selectedSequence.frames.length}
{" · SOURCE FRAME "}{selectedSummary?.datasetFrameNumber ?? "—"}
</span>
<strong>{selectedSummary?.frameId ?? "Загружаем скан"}</strong>
</div>
<div className="polygon-review-mode" aria-label="Режим визуализации">
{viewModes.map(([viewMode, label]) => (
<button
type="button"
key={viewMode}
data-active={mode === viewMode ? "true" : undefined}
onClick={() => setMode(viewMode)}
>
{label}
</button>
))}
</div>
</header>
<div className="polygon-review-controls">
<button
type="button"
aria-label={playing ? "Пауза" : "Воспроизвести последовательность"}
onClick={() => setPlaying((value) => !value)}
>
{playing ? "Пауза" : "Play review"}
</button>
<button
type="button"
aria-label="Предыдущий скан"
disabled={selectedPosition <= 0}
onClick={() => selectPosition(selectedPosition - 1)}
>
</button>
<button
type="button"
aria-label="Следующий скан"
disabled={selectedPosition >= selectedSequence.frames.length - 1}
onClick={() => selectPosition(selectedPosition + 1)}
>
</button>
<input
type="range"
min={0}
max={Math.max(0, selectedSequence.frames.length - 1)}
value={selectedPosition}
aria-label="Позиция в последовательности"
onChange={(event) => selectPosition(Number(event.target.value))}
/>
<span>{selectedPosition + 1} / {selectedSequence.frames.length}</span>
</div>
<div className="polygon-review-stage">
{scene ? (
<LidarGroundPointCloud
key={selectedSequence.datasetSequenceId}
frame={scene}
mode={mode}
coordinateFrame="sensor-fixed"
sensorHeightM={2.24}
/>
) : (
<div className="polygon-review-stage__loading">Загружаем скан</div>
)}
<div className="polygon-review-stage__legend">
<strong>{viewModes.find(([viewMode]) => viewMode === mode)?.[1]}</strong>
<span>{modeExplanation(mode)}</span>
{frameLoading ? <span>Подгружаем выбранный scan</span> : null}
</div>
</div>
{selectedSummary ? (
<div className="polygon-review-frame-metrics">
<article>
<span>Интервал от предыдущего</span>
<strong>{gapFromPrevious === null ? "Начало" : formatGap(gapFromPrevious)}</strong>
<small>реальный timestamp, не cadence playback</small>
</article>
<article>
<span>Current · Ground IoU</span>
<strong>{formatPercent(selectedSummary.current.groundIou)}</strong>
<small>overlay выбранного прогона</small>
</article>
<article>
<span>Patchwork++ · Ground IoU</span>
<strong>{formatPercent(selectedSummary.patchwork.groundIou)}</strong>
<small>overlay выбранного прогона</small>
</article>
<article data-positive={selectedSummary.groundIouDelta >= 0 ? "true" : "false"}>
<span>Разница</span>
<strong>{formatDelta(selectedSummary.groundIouDelta)}</strong>
<small>Patchwork++ минус Current</small>
</article>
</div>
) : null}
<div
className="polygon-review-timeline dataset-sequence-timeline"
aria-label={`Все ${selectedSequence.frames.length} сканов последовательности`}
>
{selectedSequence.frames.map((frame, index) => (
<button
type="button"
key={frame.frameId}
aria-label={`Scan ${index + 1}, source frame ${frame.datasetFrameNumber}`}
title={`${frame.frameId}`}
data-active={frame.frameId === selectedFrameId ? "true" : undefined}
style={{ "--frame-color": "rgb(255 255 255 / 0.34)" } as CSSProperties}
onClick={() => selectPosition(index)}
/>
))}
</div>
<p className="polygon-review-timeline-note">
Риски показывают только доступные размеченные scans этой поездки.
Пропуски времени не интерполируются; ракурс сохраняется между соседними scans.
</p>
</section>
<p className="dataset-sequence-review__raw-note">
Для настоящего движения машины, odometry и синхронного sensor playback нужен
отдельный raw ROS bag GOOSE с localization. Он не входит в принятый 3.3 ГБ
annotated validation ZIP и сейчас не притворяется доступным.
</p>
</section>
);
}
@@ -30,6 +30,8 @@ export interface LidarGroundPointCloudFrame {
interface LidarGroundPointCloudProps {
frame: LidarGroundPointCloudFrame;
mode: LidarGroundViewMode;
coordinateFrame?: "auto-fit" | "sensor-fixed";
sensorHeightM?: number;
}
function percentile(values: number[], fraction: number): number {
@@ -140,6 +142,8 @@ function frameColors(
export function LidarGroundPointCloud({
frame,
mode,
coordinateFrame = "auto-fit",
sensorHeightM = 0,
}: LidarGroundPointCloudProps) {
const hostRef = useRef<HTMLDivElement | null>(null);
const geometryRef = useRef<THREE.BufferGeometry | null>(null);
@@ -149,6 +153,8 @@ export function LidarGroundPointCloud({
const fogRef = useRef<THREE.FogExp2 | null>(null);
const gridRef = useRef<THREE.GridHelper | null>(null);
const viewRadiusRef = useRef(1);
const viewTargetHeightRef = useRef(0.25);
const viewInitializedRef = useRef(false);
const [renderError, setRenderError] = useState<string | null>(null);
useEffect(() => {
@@ -265,6 +271,8 @@ export function LidarGroundPointCloud({
fogRef.current = null;
gridRef.current = null;
viewRadiusRef.current = 1;
viewTargetHeightRef.current = 0.25;
viewInitializedRef.current = false;
};
}, []);
@@ -294,12 +302,18 @@ export function LidarGroundPointCloud({
const maximumY = percentile(sampleY, 0.99);
const minimumZ = percentile(sampleZ, 0.01);
const maximumZ = percentile(sampleZ, 0.99);
const centerX = (minimumX + maximumX) / 2;
const centerY = (minimumY + maximumY) / 2;
const centerX = coordinateFrame === "sensor-fixed"
? 0
: (minimumX + maximumX) / 2;
const centerY = coordinateFrame === "sensor-fixed"
? 0
: (minimumY + maximumY) / 2;
frame.pointsXyzM.forEach(([x, y, z], index) => {
const offset = index * 3;
positions[offset] = x - centerX;
positions[offset + 1] = z - minimumZ;
positions[offset + 1] = coordinateFrame === "sensor-fixed"
? z + sensorHeightM
: z - minimumZ;
positions[offset + 2] = -(y - centerY);
});
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
@@ -317,15 +331,21 @@ export function LidarGroundPointCloud({
fog.density = THREE.MathUtils.clamp(0.18 / radius, 0.0008, 0.035);
grid.scale.setScalar(Math.max(radius / 5, 0.25));
const targetHeight = Math.max((maximumZ - minimumZ) * 0.42, 0.15);
const targetHeight = coordinateFrame === "sensor-fixed"
? Math.max(sensorHeightM * 0.35, 0.35)
: Math.max((maximumZ - minimumZ) * 0.42, 0.15);
const distance = Math.max(radius * 1.15, 0.9);
controls.target.set(0, targetHeight, 0);
camera.position.set(distance, distance * 0.72, distance);
viewTargetHeightRef.current = targetHeight;
camera.near = Math.max(distance / 1_000, 0.005);
camera.far = Math.max(distance * 100, 100);
camera.updateProjectionMatrix();
if (coordinateFrame === "auto-fit" || !viewInitializedRef.current) {
controls.target.set(0, targetHeight, 0);
camera.position.set(distance, distance * 0.72, distance);
viewInitializedRef.current = true;
}
controls.update();
}, [frame]);
}, [coordinateFrame, frame, sensorHeightM]);
useEffect(() => {
const geometry = geometryRef.current;
@@ -345,7 +365,7 @@ export function LidarGroundPointCloud({
const radius = Math.max(viewRadiusRef.current, 0.2);
const distance = Math.max(radius * 1.15, 0.9);
camera.position.set(distance, distance * 0.72, distance);
controls.target.set(0, radius * 0.25, 0);
controls.target.set(0, viewTargetHeightRef.current, 0);
controls.update();
};
@@ -1,20 +1,9 @@
import {
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from "react";
import { useEffect, useState } from "react";
import { Button, StatusBadge } from "@nodedc/ui-react";
import {
fetchGroundQualification,
fetchGroundReview,
fetchGroundReviewFrame,
type GroundQualification,
type GroundReview,
type GroundReviewFrame,
type GroundReviewFrameSummary,
} from "../core/polygon/groundQualification";
import {
fetchPolygonRunCatalog,
@@ -24,17 +13,11 @@ import {
type PolygonRunRoute,
type PolygonRunState,
} from "../core/polygon/runArchive";
import {
LidarGroundPointCloud,
type LidarGroundViewMode,
} from "./LidarGroundPointCloud";
interface PolygonRunWorkspaceProps {
route: PolygonRunRoute;
}
type ReviewOrder = "sequence" | "best" | "worst";
const stateLabels: Record<PolygonRunState, string> = {
admitted: "Допущен",
starting: "Запускается",
@@ -46,21 +29,6 @@ const stateLabels: Record<PolygonRunState, string> = {
aborted: "Прерван",
};
const viewModes: ReadonlyArray<[LidarGroundViewMode, string]> = [
["intensity", "Облако"],
["ground-truth", "Эталон"],
["current", "Current"],
["candidate", "Patchwork++"],
["disagreement", "Ошибки Current"],
["candidate-disagreement", "Ошибки Patchwork++"],
];
const orderLabels: ReadonlyArray<[ReviewOrder, string]> = [
["sequence", "По записи"],
["best", "Лучшие"],
["worst", "Худшие"],
];
function stateTone(
state: PolygonRunState,
): "success" | "accent" | "warning" | "danger" | "neutral" {
@@ -71,6 +39,17 @@ function stateTone(
return "neutral";
}
function runKindLabel(kind: string): string {
if (kind === "simulation-closed-loop" || kind === "simulation_closed_loop") {
return "Closed-loop simulation";
}
if (kind === "dataset-replay" || kind === "replay_shadow") {
return "Dataset replay";
}
if (kind === "shadow") return "Live shadow";
return kind;
}
function formatTimestamp(value: string | null): string {
if (!value) return "—";
return new Intl.DateTimeFormat("ru-RU", {
@@ -88,11 +67,6 @@ function formatBytes(value: number): string {
})} ${units[exponent]}`;
}
function errorMessage(error: unknown): string {
if (error instanceof Error && error.message.trim()) return error.message;
return "Не удалось прочитать прогон.";
}
function formatPercent(value: number): string {
return new Intl.NumberFormat("ru-RU", {
style: "percent",
@@ -107,74 +81,19 @@ function formatDelta(value: number): string {
})} п.п.`;
}
function formatMilliseconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} мс`;
}
function frameScene(frame: GroundReviewFrame | null) {
if (!frame) return null;
return {
pointCount: frame.pointCount,
pointsXyzM: frame.pointsXyzM,
intensity0To255: frame.intensity0To255,
masks: {
currentGround: frame.currentGround,
currentAssigned: frame.evaluated,
candidateGround: frame.patchworkGround,
candidateAssigned: frame.evaluated,
disagreement: frame.currentDisagreement,
candidateDisagreement: frame.patchworkDisagreement,
groundTruthGround: frame.groundTruthGround,
},
};
}
function modeExplanation(mode: LidarGroundViewMode): string {
if (mode === "intensity") return "Нейтральная геометрия нативного LiDAR-кадра.";
if (mode === "ground-truth") return "Публичная GOOSE-разметка: земля и всё остальное.";
if (mode === "current") return "Что текущий алгоритм Mission Core считает землёй.";
if (mode === "candidate") return "Что Patchwork++ считает землёй.";
if (mode === "disagreement") return "Красным — точки, где Current расходится с эталоном.";
return "Красным — точки, где Patchwork++ расходится с эталоном.";
}
function frameColor(delta: number): string {
if (delta < 0) {
const opacity = Math.min(0.92, 0.42 + Math.abs(delta) * 3);
return `rgb(255 104 104 / ${opacity})`;
}
const opacity = Math.min(0.9, 0.25 + delta * 2.8);
return `rgb(210 242 188 / ${opacity})`;
}
function orderFrames(
review: GroundReview,
order: ReviewOrder,
): GroundReviewFrameSummary[] {
if (order === "sequence") return review.frames;
return [...review.frames].sort((left, right) => (
order === "best"
? right.groundIouDelta - left.groundIouDelta
: left.groundIouDelta - right.groundIouDelta
));
function errorMessage(error: unknown): string {
if (error instanceof Error && error.message.trim()) return error.message;
return "Не удалось прочитать прогон.";
}
export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const [catalog, setCatalog] = useState<PolygonRunCatalog | null>(null);
const [detail, setDetail] = useState<PolygonRunDetail | null>(null);
const [qualification, setQualification] = useState<GroundQualification | null>(null);
const [selectedRunId, setSelectedRunId] = useState<string | null>(route.runId);
const [loading, setLoading] = useState(route.error === null);
const [error, setError] = useState<string | null>(route.error);
const [reloadGeneration, setReloadGeneration] = useState(0);
const [qualification, setQualification] = useState<GroundQualification | null>(null);
const [review, setReview] = useState<GroundReview | null>(null);
const [reviewError, setReviewError] = useState<string | null>(null);
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(null);
const [reviewFrame, setReviewFrame] = useState<GroundReviewFrame | null>(null);
const [reviewMode, setReviewMode] = useState<LidarGroundViewMode>("ground-truth");
const [reviewOrder, setReviewOrder] = useState<ReviewOrder>("sequence");
const [playing, setPlaying] = useState(false);
const frameCache = useRef(new Map<string, GroundReviewFrame>());
useEffect(() => {
if (route.error) {
@@ -190,20 +109,30 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const nextCatalog = await fetchPolygonRunCatalog({ signal: controller.signal });
if (controller.signal.aborted) return;
setCatalog(nextCatalog);
const targetRunId = selectedRunId ?? route.runId ?? nextCatalog.items[0]?.runId ?? null;
const targetRunId = selectedRunId
?? route.runId
?? nextCatalog.items[0]?.runId
?? null;
if (!targetRunId) {
setDetail(null);
setQualification(null);
return;
}
const nextDetail = await fetchPolygonRunDetail(targetRunId, {
signal: controller.signal,
});
if (controller.signal.aborted) return;
const nextQualification = await fetchGroundQualification(targetRunId, {
signal: controller.signal,
});
if (controller.signal.aborted) return;
setSelectedRunId(targetRunId);
setDetail(nextDetail);
setQualification(nextQualification);
} catch (loadError) {
if (controller.signal.aborted) return;
setDetail(null);
setQualification(null);
setError(errorMessage(loadError));
} finally {
if (!controller.signal.aborted) setLoading(false);
@@ -212,135 +141,13 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
return () => controller.abort();
}, [reloadGeneration, route.error, route.runId, selectedRunId]);
useEffect(() => {
const runId = detail?.run.runId;
setQualification(null);
setReview(null);
setReviewFrame(null);
setSelectedFrameId(null);
setReviewError(null);
setPlaying(false);
frameCache.current.clear();
if (!runId) return;
const controller = new AbortController();
void (async () => {
try {
const nextQualification = await fetchGroundQualification(runId, {
signal: controller.signal,
});
if (controller.signal.aborted) return;
setQualification(nextQualification);
if (!nextQualification) return;
const nextReview = await fetchGroundReview(runId, {
signal: controller.signal,
});
if (controller.signal.aborted) return;
setReview(nextReview);
setSelectedFrameId(nextReview?.frames[0]?.frameId ?? null);
} catch (loadError) {
if (!controller.signal.aborted) setReviewError(errorMessage(loadError));
}
})();
return () => controller.abort();
}, [detail?.run.runId]);
useEffect(() => {
const runId = review?.runId;
if (!runId || !selectedFrameId) {
setReviewFrame(null);
return;
}
const cached = frameCache.current.get(selectedFrameId);
if (cached) {
setReviewFrame(cached);
return;
}
const controller = new AbortController();
setReviewFrame(null);
void fetchGroundReviewFrame(runId, selectedFrameId, {
signal: controller.signal,
})
.then((frame) => {
if (controller.signal.aborted) return;
frameCache.current.set(frame.frameId, frame);
while (frameCache.current.size > 24) {
const oldest = frameCache.current.keys().next().value as string | undefined;
if (oldest) frameCache.current.delete(oldest);
else break;
}
setReviewFrame(frame);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setReviewError(errorMessage(loadError));
});
return () => controller.abort();
}, [review?.runId, selectedFrameId]);
const orderedFrames = useMemo(
() => review ? orderFrames(review, reviewOrder) : [],
[review, reviewOrder],
);
const selectedPosition = Math.max(
0,
orderedFrames.findIndex((frame) => frame.frameId === selectedFrameId),
);
const selectedSummary = orderedFrames[selectedPosition] ?? null;
const bestFrame = useMemo(
() => review
? [...review.frames].sort(
(left, right) => right.groundIouDelta - left.groundIouDelta,
)[0]
: null,
[review],
);
const worstFrame = useMemo(
() => review
? [...review.frames].sort(
(left, right) => left.groundIouDelta - right.groundIouDelta,
)[0]
: null,
[review],
);
useEffect(() => {
if (!playing || !reviewFrame || !selectedFrameId || !orderedFrames.length) return;
if (reviewFrame.frameId !== selectedFrameId) return;
const timer = window.setTimeout(() => {
const nextPosition = selectedPosition + 1;
if (nextPosition >= orderedFrames.length) {
setPlaying(false);
return;
}
setSelectedFrameId(orderedFrames[nextPosition].frameId);
}, 520);
return () => window.clearTimeout(timer);
}, [
orderedFrames,
playing,
reviewFrame,
selectedFrameId,
selectedPosition,
]);
const scene = useMemo(() => frameScene(reviewFrame), [reviewFrame]);
const selectPosition = (position: number) => {
const frame = orderedFrames[Math.max(0, Math.min(position, orderedFrames.length - 1))];
if (frame) setSelectedFrameId(frame.frameId);
};
const changeOrder = (order: ReviewOrder) => {
setPlaying(false);
setReviewOrder(order);
};
if (loading && !detail) {
return (
<div className="standard-workspace polygon-run-workspace">
<section className="polygon-run-message">
<span className="section-eyebrow">ПОЛИГОН / ПРОГОНЫ</span>
<h2>Открываем запись</h2>
<p>Читаем индекс кадров и готовим покадровый просмотр.</p>
<h2>Открываем историю прогонов</h2>
<p>Читаем каталог и выбранный воспроизводимый запуск.</p>
</section>
</div>
);
@@ -370,8 +177,8 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
<div className="standard-workspace polygon-run-workspace">
<section className="polygon-run-message">
<span className="section-eyebrow">ПОЛИГОН / ПРОГОНЫ</span>
<h2>Записей пока нет</h2>
<p>Здесь появятся replay- и simulation-прогоны после публикации результатов.</p>
<h2>Прогонов пока нет</h2>
<p>Здесь появятся simulation, dataset replay и shadow-запуски.</p>
</section>
</div>
);
@@ -380,195 +187,75 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const { run } = detail;
return (
<div className="standard-workspace polygon-run-workspace">
<section className="polygon-run-picker">
<div>
<span className="section-eyebrow">ИСТОРИЯ ПОЛИГОНА</span>
<strong>Выбранный прогон</strong>
</div>
<select
aria-label="Выбрать прогон"
value={run.runId}
disabled={!catalog?.items.length}
onChange={(event) => setSelectedRunId(event.target.value)}
>
{(catalog?.items ?? []).map((item) => (
<option key={item.runId} value={item.runId}>
{runKindLabel(item.kind)} · {item.scenarioGeneration} · {item.runId}
</option>
))}
</select>
<small>{catalog?.total ?? 0} в архиве</small>
</section>
<section className="polygon-review-lead">
<div>
<span className="section-eyebrow">ПОЛИГОН / ЗАПИСЬ ДАТАСЕТА</span>
<span className="section-eyebrow">{runKindLabel(run.kind).toUpperCase()}</span>
<h2>{qualification ? "GOOSE · Ground segmentation" : run.scenarioGeneration}</h2>
<p>
{qualification
? `${qualification.frameCount} кадров · Current и Patchwork++ против публичной разметки`
: "Архивный прогон без покадрового perception review."}
? "Результат алгоритмического replay. Сам исходный датасет и его sequences открываются в разделе «Датасеты»."
: "Воспроизводимая запись запуска Полигона."}
</p>
</div>
<div>
<StatusBadge tone={stateTone(run.state)}>{stateLabels[run.state]}</StatusBadge>
{catalog && catalog.items.length > 1 ? (
<select
aria-label="Выбрать прогон"
value={run.runId}
onChange={(event) => setSelectedRunId(event.target.value)}
>
{catalog.items.map((item) => (
<option key={item.runId} value={item.runId}>{item.runId}</option>
))}
</select>
) : null}
</div>
<StatusBadge tone={stateTone(run.state)}>{stateLabels[run.state]}</StatusBadge>
</section>
{qualification && review ? (
{qualification ? (
<>
<section className="polygon-review-player" aria-label="Покадровый просмотр GOOSE">
<header className="polygon-review-player__header">
<div>
<span className="section-eyebrow">КАДР {selectedSummary
? selectedSummary.sequence + 1
: "—"} / {review.frameCount}</span>
<strong>{selectedSummary?.frameId ?? "Загружаем запись"}</strong>
</div>
<div className="polygon-review-mode" aria-label="Режим визуализации">
{viewModes.map(([mode, label]) => (
<button
type="button"
key={mode}
data-active={reviewMode === mode ? "true" : undefined}
onClick={() => setReviewMode(mode)}
>
{label}
</button>
))}
</div>
</header>
<div className="polygon-review-controls">
<button
type="button"
aria-label={playing ? "Пауза" : "Воспроизвести"}
onClick={() => setPlaying((value) => !value)}
>
{playing ? "Пауза" : "Play"}
</button>
<button
type="button"
aria-label="Предыдущий кадр"
disabled={selectedPosition <= 0}
onClick={() => selectPosition(selectedPosition - 1)}
>
</button>
<button
type="button"
aria-label="Следующий кадр"
disabled={selectedPosition >= orderedFrames.length - 1}
onClick={() => selectPosition(selectedPosition + 1)}
>
</button>
<input
type="range"
min={0}
max={Math.max(0, orderedFrames.length - 1)}
value={selectedPosition}
aria-label="Позиция в записи"
onChange={(event) => selectPosition(Number(event.target.value))}
/>
<span>{selectedPosition + 1} / {orderedFrames.length}</span>
<section className="polygon-run-input">
<div>
<span className="section-eyebrow">ВХОД ПРОГОНА</span>
<h3>GOOSE 3D · validation</h3>
<p>
Восемь отдельных поездок, 961 разреженный размеченный LiDAR scan.
Это не closed-loop simulation и не непрерывная запись движения машины.
</p>
</div>
<div className="polygon-review-stage">
{scene ? (
<LidarGroundPointCloud frame={scene} mode={reviewMode} />
) : (
<div className="polygon-review-stage__loading">Загружаем кадр</div>
<Button
size="compact"
variant="secondary"
onClick={() => window.location.assign(
`/?workspace=polygon-datasets&run=${encodeURIComponent(run.runId)}`,
)}
<div className="polygon-review-stage__legend">
<strong>{viewModes.find(([mode]) => mode === reviewMode)?.[1]}</strong>
<span>{modeExplanation(reviewMode)}</span>
</div>
</div>
{selectedSummary ? (
<div className="polygon-review-frame-metrics">
<article>
<span>Current · Ground IoU</span>
<strong>{formatPercent(selectedSummary.current.groundIou)}</strong>
<small>прогноз против эталона</small>
</article>
<article>
<span>Patchwork++ · Ground IoU</span>
<strong>{formatPercent(selectedSummary.patchwork.groundIou)}</strong>
<small>прогноз против эталона</small>
</article>
<article data-positive={selectedSummary.groundIouDelta >= 0 ? "true" : "false"}>
<span>Разница на этом кадре</span>
<strong>{formatDelta(selectedSummary.groundIouDelta)}</strong>
<small>Patchwork++ минус Current</small>
</article>
<article>
<span>Natural ground · PW++</span>
<strong>{formatPercent(
selectedSummary.patchwork.naturalGroundRecall,
)}</strong>
<small>recall на естественном грунте</small>
</article>
</div>
) : null}
<div className="polygon-review-order">
<div>
{orderLabels.map(([order, label]) => (
<button
type="button"
key={order}
data-active={reviewOrder === order ? "true" : undefined}
onClick={() => changeOrder(order)}
>
{label}
</button>
))}
</div>
<div>
<button
type="button"
onClick={() => bestFrame && setSelectedFrameId(bestFrame.frameId)}
>
Лучший прирост
</button>
<button
type="button"
onClick={() => worstFrame && setSelectedFrameId(worstFrame.frameId)}
>
Худшее ухудшение
</button>
</div>
</div>
<div
className="polygon-review-timeline"
aria-label={`Все ${orderedFrames.length} кадров`}
>
{orderedFrames.map((frame, index) => (
<button
type="button"
key={frame.frameId}
aria-label={`Кадр ${frame.sequence + 1}: ${formatDelta(frame.groundIouDelta)}`}
title={`${frame.frameId} · ${formatDelta(frame.groundIouDelta)}`}
data-active={frame.frameId === selectedFrameId ? "true" : undefined}
style={{ "--frame-color": frameColor(frame.groundIouDelta) } as CSSProperties}
onClick={() => selectPosition(index)}
/>
))}
</div>
<p className="polygon-review-timeline-note">
Каждая риска доступный кадр. Светлая означает улучшение IoU, красная
ухудшение. Можно нажать на любую и проверить результат вручную.
</p>
Открыть исходник
</Button>
</section>
<section className="polygon-review-summary">
<header>
<div>
<span className="section-eyebrow">ВЕСЬ VALIDATION SPLIT</span>
<h3>Что именно сравнивалось</h3>
<span className="section-eyebrow">РЕЗУЛЬТАТ ПРОГОНА</span>
<h3>Current против Patchwork++</h3>
</div>
<StatusBadge tone={qualification.decision.passed ? "success" : "danger"}>
{qualification.decision.passed ? "27 / 27 проверок" : "Есть проваленные проверки"}
{qualification.decision.passed ? "27 / 27 проверок" : "Есть провалы"}
</StatusBadge>
</header>
<p>
На каждом из {qualification.frameCount} нативных LiDAR-кадров оба алгоритма
независимо решали одну задачу: какие точки являются землёй. Их ответы
сравнивались с публичной point-aligned разметкой GOOSE.
Оба алгоритма независимо обработали одни и те же 961 scans.
Покадровая визуальная проверка теперь находится рядом с самим входом
в «Датасетах», а не притворяется симуляцией машины.
</p>
<div>
<article>
@@ -597,16 +284,20 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
</article>
<article>
<span>Latency p95</span>
<strong>{formatMilliseconds(qualification.current.latencyMs.p95)}</strong>
<strong>{qualification.current.latencyMs.p95.toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} мс</strong>
<i></i>
<strong>{formatMilliseconds(qualification.patchwork.latencyMs.p95)}</strong>
<small>на кадр</small>
<strong>{qualification.patchwork.latencyMs.p95.toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} мс</strong>
<small>на scan</small>
</article>
</div>
</section>
<details className="polygon-review-analysis">
<summary>Полные метрики и деградации</summary>
<summary>Acceptance и деградации</summary>
<div className="polygon-review-analysis__content">
<section>
<span className="section-eyebrow">ACCEPTANCE GATES</span>
@@ -629,28 +320,20 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
<strong>{profileId}</strong>
<span>IoU {formatPercent(aggregate.micro.groundIou)}</span>
<span>Natural {formatPercent(aggregate.micro.naturalGroundRecall)}</span>
<span>p95 {formatMilliseconds(aggregate.latencyMs.p95)}</span>
<span>p95 {aggregate.latencyMs.p95.toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} мс</span>
</p>
))}
</section>
</div>
</details>
</>
) : qualification ? (
<section className="polygon-review-unavailable">
<span className="section-eyebrow">ПОКАДРОВЫЙ ПРОСМОТР</span>
<h3>Review-pack ещё не опубликован</h3>
<p>
Числовой отчёт есть, но мы не показываем его как достаточный результат:
оператор должен иметь возможность проверить все {qualification.frameCount} кадров.
</p>
{reviewError ? <small>{reviewError}</small> : null}
</section>
) : (
<section className="polygon-review-unavailable">
<span className="section-eyebrow">АРХИВНЫЙ ПРОГОН</span>
<h3>В этой записи нет perception review</h3>
<p>Технические доказательства доступны в раскрытии ниже.</p>
<h3>Для этого запуска нет perception qualification</h3>
<p>Техническая идентичность доступна ниже.</p>
</section>
)}
@@ -659,6 +342,7 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
<div className="polygon-run-technical__content">
<dl>
<div><dt>Run ID</dt><dd>{run.runId}</dd></div>
<div><dt>Тип</dt><dd>{runKindLabel(run.kind)}</dd></div>
<div><dt>Сценарий</dt><dd>{run.scenarioGeneration}</dd></div>
<div><dt>Профиль</dt><dd>{run.profileGeneration}</dd></div>
<div><dt>Mission Core</dt><dd>{run.missionCoreCommit.slice(0, 12)}</dd></div>