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
@@ -12,7 +12,8 @@ export type DatasetSourceAdmissionStatus =
| "verifying"
| "verified"
| "frame-ready"
| "smoke-ready";
| "smoke-ready"
| "dataset-ready";
export interface DatasetSource {
sourceId: string;
@@ -308,6 +309,7 @@ export function parseDatasetGatewayCatalog(
&& admissionStatus !== "verified"
&& admissionStatus !== "frame-ready"
&& admissionStatus !== "smoke-ready"
&& admissionStatus !== "dataset-ready"
) {
throw new DatasetGatewayContractError("source admission status неизвестен");
}
@@ -42,7 +42,10 @@ export interface GroundQualification {
checks: QualificationCheck[];
worstFrames: QualificationWorstFrame[];
decision: {
status: "shadow-candidate" | "qualification-rejected";
status:
| "shadow-candidate"
| "cross-dataset-qualified"
| "qualification-rejected";
passed: boolean;
promotedToNavigationOrSafety: false;
reason: string;
@@ -75,7 +78,7 @@ export interface GroundReviewFrameSummary {
frameId: string;
datasetSequenceId: string;
datasetFrameNumber: number;
sensorTimestampNs: string;
sensorTimestampNs: string | null;
sourcePointCount: number;
pointCount: number;
current: GroundReviewFrameMetrics;
@@ -264,7 +267,11 @@ export function decodeGroundQualification(payload: unknown): GroundQualification
);
const decision = record(source.decision, "decision");
const status = text(decision.status, "decision.status", 64);
if (status !== "shadow-candidate" && status !== "qualification-rejected") {
if (
status !== "shadow-candidate"
&& status !== "cross-dataset-qualified"
&& status !== "qualification-rejected"
) {
throw new GroundQualificationContractError("Решение квалификации неизвестно.");
}
if (decision.promoted_to_navigation_or_safety !== false) {
@@ -384,7 +391,11 @@ export function decodeGroundReview(payload: unknown): GroundReview {
throw new GroundQualificationContractError("Неизвестная схема покадрового просмотра.");
}
const frameCount = integer(source.frame_count, "frame_count", 1);
const frames = array(source.frames, "frames", 2_000).map(
const sourceId = text(source.source_id, "source_id");
if (sourceId !== "goose-3d/v2025-08-22" && sourceId !== "rellis-3d/v1.1") {
throw new GroundQualificationContractError("Источник review неизвестен.");
}
const frames = array(source.frames, "frames", 3_000).map(
(value, index): GroundReviewFrameSummary => {
const frame = record(value, `frames[${index}]`);
const sequence = integer(frame.sequence, `frames[${index}].sequence`);
@@ -392,7 +403,6 @@ export function decodeGroundReview(payload: unknown): GroundReview {
throw new GroundQualificationContractError("Кадры просмотра идут не по порядку.");
}
const frameId = id(frame.frame_id, `frames[${index}].frame_id`);
const frameIdentity = parseGooseFrameId(frameId);
const datasetSequenceId = id(
frame.dataset_sequence_id,
`frames[${index}].dataset_sequence_id`,
@@ -401,18 +411,30 @@ export function decodeGroundReview(payload: unknown): GroundReview {
frame.dataset_frame_number,
`frames[${index}].dataset_frame_number`,
);
const sensorTimestampNs = text(
frame.sensor_timestamp_ns,
`frames[${index}].sensor_timestamp_ns`,
20,
);
if (
datasetSequenceId !== frameIdentity.datasetSequenceId
|| datasetFrameNumber !== frameIdentity.datasetFrameNumber
|| sensorTimestampNs !== frameIdentity.sensorTimestampNs
let sensorTimestampNs: string | null = null;
if (sourceId === "goose-3d/v2025-08-22") {
const frameIdentity = parseGooseFrameId(frameId);
sensorTimestampNs = text(
frame.sensor_timestamp_ns,
`frames[${index}].sensor_timestamp_ns`,
20,
);
if (
datasetSequenceId !== frameIdentity.datasetSequenceId
|| datasetFrameNumber !== frameIdentity.datasetFrameNumber
|| sensorTimestampNs !== frameIdentity.sensorTimestampNs
) {
throw new GroundQualificationContractError(
`frames[${index}] противоречит идентичности GOOSE-кадра.`,
);
}
} else if (
frame.sensor_timestamp_ns !== null
|| !/^rellis-0000[0-4]-[0-9]{6}$/.test(frameId)
|| frameId !== `rellis-${datasetSequenceId}-${String(datasetFrameNumber).padStart(6, "0")}`
) {
throw new GroundQualificationContractError(
`frames[${index}] противоречит идентичности GOOSE-кадра.`,
`frames[${index}] противоречит идентичности RELLIS-кадра.`,
);
}
return {
@@ -457,7 +479,7 @@ export function decodeGroundReview(payload: unknown): GroundReview {
return {
runId: id(source.run_id, "run_id"),
identitySha256,
sourceId: text(source.source_id, "source_id"),
sourceId,
frameCount,
previewPoints: integer(source.preview_points, "preview_points", 1),
frames,
@@ -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>
);
}
@@ -184,6 +184,40 @@ test("decodes a complete ordered frame review", () => {
assert.equal(decoded.frames[0].patchwork.groundIou, 0.7);
});
test("decodes RELLIS sequences without inventing sensor timestamps", () => {
const metric = {
ground_iou: 0.6,
natural_ground_recall: 0.7,
obstacle_non_ground_recall: 0.93,
latency_ms: 18,
};
const decoded = decodeGroundReview({
schema_version: "missioncore.polygon-ground-review/v2",
access: "read-only",
run_id: "rellis-ground-abc",
identity_sha256: "c".repeat(64),
source_id: "rellis-3d/v1.1",
frame_count: 1,
preview_points: 12_000,
frames: [{
sequence: 0,
frame_id: "rellis-00000-000307",
dataset_sequence_id: "00000",
dataset_frame_number: 307,
sensor_timestamp_ns: null,
source_point_count: 131_072,
point_count: 12_000,
current: metric,
patchworkpp: metric,
ground_iou_delta: 0,
}],
});
assert.equal(decoded.sourceId, "rellis-3d/v1.1");
assert.equal(decoded.frames[0].datasetSequenceId, "00000");
assert.equal(decoded.frames[0].datasetFrameNumber, 307);
assert.equal(decoded.frames[0].sensorTimestampNs, null);
});
test("parses GOOSE sequence, source frame and nanosecond timestamp", () => {
assert.deepEqual(
parseGooseFrameId("2023-05-17_neubiberg_sunny__0459_1684330012686564560"),