feat: qualify complete RELLIS ground dataset

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 21:12:37 +03:00
parent 054feec0d2
commit f2f044080f
17 changed files with 2349 additions and 35 deletions
@@ -41,6 +41,7 @@ function admissionLabel(status: DatasetSourceAdmissionStatus): string {
verified: "Проверен",
"frame-ready": "Dataset готов",
"smoke-ready": "Smoke готов",
"dataset-ready": "Dataset готов",
};
return labels[status];
}
@@ -60,9 +61,17 @@ function isGooseQualificationRun(
&& item.scenarioGeneration.startsWith("goose-3d");
}
function isRellisQualificationRun(
item: PolygonRunCatalog["items"][number],
): boolean {
return item.kind === "replay_shadow"
&& item.scenarioGeneration.startsWith("rellis-3d");
}
function sourceReady(source: DatasetSource): boolean {
return source.admissionStatus === "frame-ready"
|| source.admissionStatus === "smoke-ready";
|| source.admissionStatus === "smoke-ready"
|| source.admissionStatus === "dataset-ready";
}
function sourceDescription(source: DatasetSource): string {
@@ -76,6 +85,9 @@ function sourceFooter(source: DatasetSource, storageReady: boolean): string {
if (source.admissionStatus === "frame-ready") {
return "Native scans и разметка приняты. Можно открыть фрагменты и результаты алгоритмов.";
}
if (source.admissionStatus === "dataset-ready") {
return "Все официальные scans, labels, poses и split-индексы выровнены на worker D.";
}
if (source.admissionStatus === "smoke-ready") {
return "Официальный пример совместим. Можно проверить scan, классы и ground mapping.";
}
@@ -98,6 +110,7 @@ export function DatasetGatewayWorkspace() {
const [openSourceId, setOpenSourceId] = useState<string | null>(null);
const [runCatalog, setRunCatalog] = useState<PolygonRunCatalog | null>(null);
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
const [selectedRellisRunId, setSelectedRellisRunId] = useState<string | null>(null);
const [runError, setRunError] = useState<string | null>(null);
useEffect(() => {
@@ -143,6 +156,11 @@ export function DatasetGatewayWorkspace() {
?? gooseRuns[0]
?? null;
setSelectedRunId(selected?.runId ?? null);
const rellisRuns = value.items.filter(isRellisQualificationRun);
const selectedRellis = rellisRuns.find((item) => item.runId === requested)
?? rellisRuns[0]
?? null;
setSelectedRellisRunId(selectedRellis?.runId ?? null);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setRunError(errorMessage(loadError));
@@ -151,6 +169,7 @@ export function DatasetGatewayWorkspace() {
}, []);
const gooseRuns = runCatalog?.items.filter(isGooseQualificationRun) ?? [];
const rellisRuns = runCatalog?.items.filter(isRellisQualificationRun) ?? [];
const openSource = catalog?.sources.find(
(source) => source.sourceId === openSourceId,
) ?? null;
@@ -322,7 +341,28 @@ export function DatasetGatewayWorkspace() {
<p>{runError ?? "Нет совместимой версии покадрового анализа."}</p>
</section>
) : openSource?.sourceKind === "rellis" ? (
<RellisDatasetReview />
<>
{rellisRuns.length > 1 && selectedRellisRunId ? (
<div className="dataset-result-select">
<div>
<span className="section-eyebrow">РЕЗУЛЬТАТЫ ОБРАБОТКИ</span>
<strong>Версия анализа</strong>
</div>
<select
aria-label="Выбрать версию анализа RELLIS"
value={selectedRellisRunId}
onChange={(event) => setSelectedRellisRunId(event.target.value)}
>
{rellisRuns.map((item) => (
<option key={item.runId} value={item.runId}>
{item.profileGeneration}
</option>
))}
</select>
</div>
) : null}
<RellisDatasetReview runId={selectedRellisRunId} />
</>
) : null}
<details className="dataset-contract">
@@ -68,6 +68,9 @@ function secondsBetween(
left: GroundReviewFrameSummary,
right: GroundReviewFrameSummary,
): number {
if (left.sensorTimestampNs === null || right.sensorTimestampNs === null) {
throw new Error("GOOSE frame timestamp отсутствует.");
}
const delta = BigInt(right.sensorTimestampNs) - BigInt(left.sensorTimestampNs);
return Number(delta) / 1_000_000_000;
}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { StatusBadge } from "@nodedc/ui-react";
import {
@@ -9,6 +9,13 @@ import {
LidarGroundPointCloud,
type LidarGroundViewMode,
} from "./LidarGroundPointCloud";
import {
fetchGroundReview,
fetchGroundReviewFrame,
type GroundReview,
type GroundReviewFrame,
type GroundReviewFrameSummary,
} from "../core/polygon/groundQualification";
const modes: ReadonlyArray<[LidarGroundViewMode, string]> = [
["intensity", "Исходный скан"],
@@ -39,7 +46,25 @@ function modeExplanation(mode: LidarGroundViewMode): string {
return "Один исходный оборот Ouster OS1 64; цвет — нормализованная remission.";
}
export function RellisDatasetReview() {
interface RellisDatasetReviewProps {
runId?: string | null;
}
const qualifiedModes: ReadonlyArray<[LidarGroundViewMode, string]> = [
["intensity", "Исходный скан"],
["ground-truth", "Ground truth"],
["current", "Текущий алгоритм"],
["candidate", "Patchwork++"],
["disagreement", "Ошибки текущего"],
["candidate-disagreement", "Ошибки Patchwork++"],
];
export function RellisDatasetReview({ runId = null }: RellisDatasetReviewProps) {
if (runId) return <RellisQualifiedReview runId={runId} />;
return <RellisSmokeReview />;
}
function RellisSmokeReview() {
const [preview, setPreview] = useState<DatasetNativeScanPreview | null>(null);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<LidarGroundViewMode>("semantic");
@@ -237,3 +262,313 @@ export function RellisDatasetReview() {
</section>
);
}
function reviewScene(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 groupRellisFrames(review: GroundReview) {
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(([sequenceId, frames]) => ({
sequenceId,
frames,
}));
}
function averageMetric(
frames: GroundReviewFrameSummary[],
value: (frame: GroundReviewFrameSummary) => number,
) {
return frames.reduce((total, frame) => total + value(frame), 0) / frames.length;
}
function qualifiedModeExplanation(mode: LidarGroundViewMode) {
if (mode === "intensity") return "Один исходный Ouster OS1 scan; цвет — remission.";
if (mode === "ground-truth") return "Point-aligned ground target из RELLIS labels.";
if (mode === "current") return "Результат текущего Mission Core baseline.";
if (mode === "candidate") return "Результат закреплённого Patchwork++ v1.4.1.";
if (mode === "disagreement") return "Красным — ошибки текущего baseline.";
return "Красным — ошибки Patchwork++ относительно labels.";
}
function RellisQualifiedReview({ runId }: { runId: string }) {
const [review, setReview] = useState<GroundReview | null>(null);
const [frame, setFrame] = useState<GroundReviewFrame | null>(null);
const [selectedSequence, setSelectedSequence] = useState<string | null>(null);
const [selectedFrame, setSelectedFrame] = useState<string | null>(null);
const [mode, setMode] = useState<LidarGroundViewMode>("intensity");
const [playing, setPlaying] = useState(false);
const [error, setError] = useState<string | null>(null);
const cache = useRef(new Map<string, GroundReviewFrame>());
useEffect(() => {
const controller = new AbortController();
setError(null);
setReview(null);
setFrame(null);
cache.current.clear();
void fetchGroundReview(runId, { signal: controller.signal })
.then((value) => {
if (controller.signal.aborted || !value) return;
setReview(value);
setSelectedSequence(value.frames[0]?.datasetSequenceId ?? null);
setSelectedFrame(value.frames[0]?.frameId ?? null);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setError(errorMessage(loadError));
});
return () => controller.abort();
}, [runId]);
const sequences = useMemo(
() => review ? groupRellisFrames(review) : [],
[review],
);
const sequence = sequences.find((item) => item.sequenceId === selectedSequence)
?? sequences[0]
?? null;
const position = Math.max(
0,
sequence?.frames.findIndex((item) => item.frameId === selectedFrame) ?? 0,
);
const summary = sequence?.frames[position] ?? null;
useEffect(() => {
if (!selectedFrame) return;
const cached = cache.current.get(selectedFrame);
if (cached) {
setFrame(cached);
return;
}
const controller = new AbortController();
void fetchGroundReviewFrame(runId, selectedFrame, { signal: controller.signal })
.then((value) => {
if (controller.signal.aborted) return;
cache.current.set(value.frameId, value);
while (cache.current.size > 32) {
const first = cache.current.keys().next().value as string | undefined;
if (first) cache.current.delete(first);
else break;
}
setFrame(value);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setError(errorMessage(loadError));
});
return () => controller.abort();
}, [runId, selectedFrame]);
useEffect(() => {
if (!playing || !sequence || frame?.frameId !== selectedFrame) return;
const timer = window.setTimeout(() => {
const next = sequence.frames[position + 1];
if (!next) {
setPlaying(false);
return;
}
setSelectedFrame(next.frameId);
}, 250);
return () => window.clearTimeout(timer);
}, [frame?.frameId, playing, position, selectedFrame, sequence]);
if (error) {
return (
<section className="polygon-review-unavailable">
<StatusBadge tone="danger">Review недоступен</StatusBadge>
<h3>RELLIS-3D не открыт</h3>
<p>{error}</p>
</section>
);
}
if (!review || !sequence) {
return (
<section className="polygon-review-unavailable">
<StatusBadge tone="accent">Читаем индекс</StatusBadge>
<h3>Открываем RELLIS validation</h3>
<p>Исходные архивы остаются на worker D; в браузер идёт bounded preview.</p>
</section>
);
}
const selectPosition = (value: number) => {
const bounded = Math.max(0, Math.min(value, sequence.frames.length - 1));
setPlaying(false);
setSelectedFrame(sequence.frames[bounded]?.frameId ?? null);
};
const patchworkBetter = sequence.frames.filter(
(item) => item.groundIouDelta > 0,
).length;
const scene = reviewScene(frame);
return (
<section
className="dataset-sequence-review rellis-review"
aria-label="Просмотр RELLIS validation"
>
<header className="dataset-sequence-review__truth">
<div>
<span className="section-eyebrow">RELLIS · FULL VALIDATION</span>
<h2>Пять последовательностей Ouster OS1 с покадровым сравнением</h2>
<p>
Все {review.frameCount.toLocaleString("ru-RU")} validation-сканов
выровнены с labels и pose index. Высота Patchwork++ определена только
на train split; validation labels использованы исключительно для оценки.
</p>
</div>
<dl>
<div><dt>Последовательностей</dt><dd>{sequences.length}</dd></div>
<div><dt>Validation scans</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="rellis-sequence">Последовательность датасета</label>
<select
id="rellis-sequence"
value={sequence.sequenceId}
onChange={(event) => {
const next = sequences.find((item) => item.sequenceId === event.target.value);
setPlaying(false);
setSelectedSequence(event.target.value);
setSelectedFrame(next?.frames[0]?.frameId ?? null);
setFrame(null);
}}
>
{sequences.map((item) => (
<option key={item.sequenceId} value={item.sequenceId}>
Sequence {item.sequenceId} · {item.frames.length} scans
</option>
))}
</select>
<span>
Индексы {sequence.frames[0]?.datasetFrameNumber ?? 0}{
sequence.frames[sequence.frames.length - 1]?.datasetFrameNumber ?? 0
}
</span>
</div>
<section className="polygon-review-player">
<header className="polygon-review-player__header">
<div>
<span className="section-eyebrow">
SCAN {position + 1} / {sequence.frames.length}
{" · SOURCE FRAME "}{summary?.datasetFrameNumber ?? "—"}
</span>
<strong>{summary?.frameId ?? "Загружаем scan"}</strong>
</div>
<div className="polygon-review-mode" aria-label="Режим RELLIS review">
{qualifiedModes.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" onClick={() => setPlaying((value) => !value)}>
{playing ? "Пауза" : "Play sequence"}
</button>
<button
type="button"
disabled={position === 0}
onClick={() => selectPosition(position - 1)}
>
</button>
<button
type="button"
disabled={position === sequence.frames.length - 1}
onClick={() => selectPosition(position + 1)}
>
</button>
<input
type="range"
min={0}
max={sequence.frames.length - 1}
value={position}
aria-label="Позиция в RELLIS sequence"
onChange={(event) => selectPosition(Number(event.target.value))}
/>
<span>{position + 1} / {sequence.frames.length}</span>
</div>
<div className="polygon-review-stage">
{scene ? (
<LidarGroundPointCloud
key={sequence.sequenceId}
frame={scene}
mode={mode}
coordinateFrame="sensor-fixed"
/>
) : (
<div className="polygon-review-stage__loading">Загружаем scan</div>
)}
<div className="polygon-review-stage__legend">
<strong>
{qualifiedModes.find(([viewMode]) => viewMode === mode)?.[1]}
</strong>
<span>{qualifiedModeExplanation(mode)}</span>
</div>
</div>
</section>
<div className="rellis-review__metrics">
<article>
<span>Ground IoU · Current</span>
<strong>{formatPercent(averageMetric(sequence.frames, (item) => item.current.groundIou))}</strong>
<small>среднее по выбранной sequence</small>
</article>
<article>
<span>Ground IoU · Patchwork++</span>
<strong>
{formatPercent(averageMetric(sequence.frames, (item) => item.patchwork.groundIou))}
</strong>
<small>лучше Current на {patchworkBetter} scans</small>
</article>
<article>
<span>Obstacle recall · Patchwork++</span>
<strong>
{formatPercent(
averageMetric(
sequence.frames,
(item) => item.patchwork.obstacleNonGroundRecall,
),
)}
</strong>
<small>non-ground сохранён как препятствие</small>
</article>
</div>
<p className="dataset-sequence-review__raw-note">
Это независимая исследовательская проверка переносимости. Она не
включает управление машиной и не принимает алгоритм для navigation/safety.
Лицензия исходника CC-BY-NC-SA-3.0.
</p>
</section>
);
}