feat(perception): add DDRNet full-video vegetation replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 01:13:18 +03:00
parent 67e6ae98e2
commit 30080c51aa
10 changed files with 729 additions and 64 deletions
@@ -97,7 +97,16 @@ function SpatialState({ message: text }: { message: string }) {
export interface M4ReplayThreatSemanticLayer {
resultId: string;
taxonomy: readonly E47SemanticClass[];
spatialResultId?: string | null;
maskUrl?: (sequence: number) => string;
label?: string;
maskAriaLabel?: string;
taxonomy: readonly {
classId: number;
label: string;
disposition: "labeled" | "ambiguous" | "prediction" | "undefined";
colorRgb: readonly [number, number, number];
}[];
}
export interface M4ReplayThreatReviewAnchor {
@@ -153,6 +162,8 @@ export function M4ReplayThreatVisual({
evidenceLabel = "M4.6",
initialSpatialMode = null,
classifiedSpatialLayer,
showReferenceMediaLayers = true,
showSpatialOverlaySummary = true,
onActiveSequenceChange,
}: {
resultId: string;
@@ -164,6 +175,8 @@ export function M4ReplayThreatVisual({
evidenceLabel?: string;
initialSpatialMode?: LaboratoryMetricSceneMode | null;
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
showReferenceMediaLayers?: boolean;
showSpatialOverlaySummary?: boolean;
onActiveSequenceChange?: (sequence: number | null) => void;
}) {
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
@@ -280,16 +293,30 @@ export function M4ReplayThreatVisual({
? lastSpatialFrameRef.current.frame
: null;
const cameraPointOverlay = useM4ThreatCameraPointOverlay({
enabled: showMediaPoints,
enabled: showReferenceMediaLayers && showMediaPoints,
resultId,
sequence: frame?.sequence ?? null,
endpointRoot: timelineEndpointRoot,
});
const semanticSpatialResultId = semantic
? semantic.spatialResultId === undefined ? semantic.resultId : semantic.spatialResultId
: null;
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
() => semanticSpatialResultId && semantic
? semantic.taxonomy.map((item) => ({
classId: item.classId,
label: item.label,
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
colorRgb: item.colorRgb,
}))
: [],
[semantic, semanticSpatialResultId],
);
const semanticTimeline = useE47SemanticTimelineFrame({
resultId: semantic?.resultId ?? null,
resultId: semanticSpatialResultId,
activeSequence: frame?.sequence ?? timelineFrame.activeSequence,
frameCount: metadata.timeline?.frameCount ?? 0,
taxonomy: semantic?.taxonomy ?? [],
taxonomy: spatialSemanticTaxonomy,
});
const displayingBufferedFrame = Boolean(
frame
@@ -339,7 +366,8 @@ export function M4ReplayThreatVisual({
const staticObstacleBoxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
const timeline = metadata.timeline;
if (
!frame
!showReferenceMediaLayers
|| !frame
|| !timeline?.cameraObstacleProjectionDelivery
|| !showStaticObstacles
) return [];
@@ -348,14 +376,14 @@ export function M4ReplayThreatVisual({
timeline.imageWidth,
timeline.imageHeight,
);
}, [frame, metadata.timeline, showStaticObstacles]);
}, [frame, metadata.timeline, showReferenceMediaLayers, showStaticObstacles]);
const activeBoxes = useMemo(
() => classifiedSpatialLayer ? [] : [
() => classifiedSpatialLayer || !showReferenceMediaLayers ? [] : [
...boxes(frame?.cameraProposals ?? []),
...staticObstacleBoxes,
...reviewAnchorBoxes,
],
[classifiedSpatialLayer, frame, reviewAnchorBoxes, staticObstacleBoxes],
[classifiedSpatialLayer, frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes],
);
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
() => semantic?.taxonomy.map((item) => ({
@@ -367,10 +395,14 @@ export function M4ReplayThreatVisual({
const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
() => semantic?.taxonomy.map((item) => ({
classId: item.classId,
color: item.disposition === "ambiguous"
color: item.disposition === "undefined"
? { kind: "transparent" as const }
: item.disposition === "ambiguous"
? { kind: "token" as const, token: "--nodedc-warning-rgb" as const }
: { kind: "diagnostic" as const, rgb: item.colorRgb },
opacity: item.disposition === "ambiguous" ? 0.52 : 0.92,
opacity: item.disposition === "undefined"
? 0
: item.disposition === "ambiguous" ? 0.52 : 0.92,
})) ?? [],
[semantic?.taxonomy],
);
@@ -580,15 +612,17 @@ export function M4ReplayThreatVisual({
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
semantic && showMediaSemantic && frame
? {
src: e47SemanticMaskUrl(semantic.resultId, frame.sequence),
src: semantic.maskUrl?.(frame.sequence)
?? e47SemanticMaskUrl(semantic.resultId, frame.sequence),
prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1)
.map((offset) => frame.sequence + offset)
.filter((sequence) => sequence < (metadata.timeline?.frameCount ?? 0))
.map((sequence) => e47SemanticMaskUrl(semantic.resultId, sequence)),
.map((sequence) => semantic.maskUrl?.(sequence)
?? e47SemanticMaskUrl(semantic.resultId, sequence)),
classes: semanticClasses,
palette: semanticPalette,
opacity: 0.9,
ariaLabel: `E47 semantic mask frame ${frame.sequence + 1}`,
ariaLabel: `${semantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`,
}
: undefined;
const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence
@@ -659,8 +693,8 @@ export function M4ReplayThreatVisual({
);
const mediaLayerControls = semantic
|| metadata.timeline?.cameraPointDelivery
|| metadata.timeline?.cameraObstacleProjectionDelivery ? (
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
|| (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
@@ -677,7 +711,7 @@ export function M4ReplayThreatVisual({
SEMANTICS
</Button>
) : null}
{metadata.timeline?.cameraPointDelivery ? (
{showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery ? (
<Button
size="compact"
shape="pill"
@@ -689,7 +723,7 @@ export function M4ReplayThreatVisual({
POINTS
</Button>
) : null}
{metadata.timeline?.cameraObstacleProjectionDelivery ? (
{showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery ? (
<Button
size="compact"
shape="pill"
@@ -738,7 +772,7 @@ export function M4ReplayThreatVisual({
>
{classifiedSpatialLayer.cellLayerLabel}
</Button>
{semantic ? (
{semanticSpatialResultId ? (
<Button
size="compact"
shape="pill"
@@ -796,7 +830,7 @@ export function M4ReplayThreatVisual({
LOW-STEP
</Button>
) : null}
{semantic ? (
{semanticSpatialResultId ? (
<Button
size="compact"
shape="pill"
@@ -901,9 +935,11 @@ export function M4ReplayThreatVisual({
: playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
</small>
</div>
<div>
<span>Spatial evidence</span>
<strong>{classifiedSpatialLayer
{showSpatialOverlaySummary ? (
<>
<div>
<span>Spatial evidence</span>
<strong>{classifiedSpatialLayer
? classifiedSpatialFrame
? replaceClassifiedPointCloud
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedCellCount.toLocaleString("ru-RU")} cells`
@@ -939,15 +975,15 @@ export function M4ReplayThreatVisual({
: showMediaPoints && cameraPointOverlay.error
? " · накопленное camera cloud недоступно"
: ""}
{semantic && spatialSemanticFrame
{semanticSpatialResultId && spatialSemanticFrame
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
: semantic ? " · semantic buffer" : ""}
: semanticSpatialResultId ? " · semantic buffer" : ""}
</>
)}</small>
</div>
<div>
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{classifiedSpatialLayer
</div>
<div>
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{classifiedSpatialLayer
? classifiedSpatialFrame
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
@@ -957,7 +993,9 @@ export function M4ReplayThreatVisual({
? `${classifiedCellCounts.ground} ground-support · visual review only · navigation authority OFF`
: "visual review only · navigation authority OFF"
: `${metadata.timeline.corridor.forwardLengthM} м · body ${metadata.timeline.rig.lengthM}×${metadata.timeline.rig.widthM} м · REPLAY-SIMULATED`}</small>
</div>
</div>
</>
) : null}
</div>
) : undefined;
@@ -1153,13 +1191,13 @@ export function M4ReplayThreatVisual({
<span>{timelineFrame.error}</span>
</div>
) : null}
{semantic && semanticTimeline.loading ? (
{semanticSpatialResultId && semanticTimeline.loading ? (
<div className="m4-replay-threat-visual__buffering" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Догружаем semantic-point evidence E47</span>
</div>
) : null}
{semanticTimeline.error ? (
{semanticSpatialResultId && semanticTimeline.error ? (
<div className="m4-replay-threat-visual__buffering" role="alert">
<Icon name="alert" size={16} />
<span>{semanticTimeline.error}</span>
@@ -1201,7 +1239,7 @@ export function M4ReplayThreatVisual({
<div className="l3-visual-audit m4-replay-threat-visual">
<LaboratoryEvidenceViewer
label={semantic
? "E47 semantic + SLAM diagnostic replay"
? semantic.label ?? "Semantic diagnostic replay"
: `${evidenceLabel} recorded-realtime replay`}
className="m4-replay-threat-evidence-viewer"
mode={mediaMode ?? "none"}
@@ -4,11 +4,15 @@ import {
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
import {
vegetationVideoMaskUrl,
type VegetationShadowResult,
} from "../../core/laboratory/vegetationShadow";
import {
M48MaskComparisonVisual,
type M48MaskComparisonCase,
} from "./M48FailureAtlasVisual";
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
@@ -63,20 +67,28 @@ export function VegetationShadowResultView({
summary={(
<LaboratorySummary
title="LAB V1 · готовые модели растительности"
description="Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."
status="Truth-backed model comparison · route transfer не принят"
description={result.routeVideo
? "M4.8 сохраняет truth-backed сравнение моделей, а штатный M4.7 viewer показывает фактический DDRNet prediction на всей записи RAVNOVES00. Все 4489 масок запечатаны локально и открываются без Worker 006."
: "Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."}
status={result.routeVideo
? "DDRNet full-video prediction ready · route truth отсутствует"
: "Truth-backed model comparison · route transfer не принят"}
statusTone="warning"
facts={[
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" },
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
...(result.routeVideo ? [{
label: "Видео",
value: "RAVNOVES00 · 4489/4489 DDRNet masks · exact recorded sequence",
}] : []),
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
]}
brief={{
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.",
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. Ошибки по каждому типу теперь проверяются в одном штатном инструменте.`,
limitation: "Это внешний GOOSE-домен, а не наш fisheye/off-road маршрут. Папоротник отдельным классом отсутствует; RAVNOVES00 не содержит truth-backed vegetation island и не используется как главное визуальное доказательство.",
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. ${result.routeVideo ? "Его фактическая temporal stability теперь видна на всех 4489 кадрах штатного recorded viewer." : "Ошибки по каждому типу проверяются в одном штатном инструменте."}`,
limitation: "GOOSE — внешний размеченный домен; RAVNOVES00 — наш fisheye, но без ручной truth-разметки. Full-video слой показывает prediction, а не доказывает правильность. Папоротник отдельным классом отсутствует.",
}}
method={{
completeness: "complete",
@@ -93,17 +105,42 @@ export function VegetationShadowResultView({
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
kind="diagnostic-model"
resizable
>
<M48MaskComparisonVisual
cases={comparisonCases(result)}
initialCandidate={result.selectedCandidate}
/>
</LaboratoryEvidence>
<>
<LaboratoryEvidence
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
kind="diagnostic-model"
resizable
>
<M48MaskComparisonVisual
cases={comparisonCases(result)}
initialCandidate={result.selectedCandidate}
/>
</LaboratoryEvidence>
{result.routeVideo ? (
<LaboratoryEvidence
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
title="DDRNet PREDICTION · 4489/4489 кадров · TRUTH для этой записи отсутствует"
kind="diagnostic-model"
resizable
>
<M4ReplayThreatVisual
resultId={result.routeVideo.baseM4ResultId}
evidenceLabel="LAB V1 · DDRNet"
showReferenceMediaLayers={false}
showSpatialOverlaySummary={false}
semantic={{
resultId: result.routeVideo.workerResultId,
spatialResultId: null,
taxonomy: result.routeVideo.taxonomy,
maskUrl: (sequence) => vegetationVideoMaskUrl(result.resultId, sequence),
label: "DDRNet vegetation prediction · recorded video",
maskAriaLabel: "DDRNet vegetation prediction",
}}
/>
</LaboratoryEvidence>
) : null}
</>
)}
result={(
<LaboratoryResultSummary
@@ -146,11 +183,16 @@ export function VegetationShadowResultView({
value: "12 truth-backed cases",
hint: "8 vegetation strata · Worker для открытия не требуется",
},
...(result.routeVideo ? [{
label: "Route video",
value: "4489/4489 masks",
hint: "DDRNet prediction · exact sequence · Worker-independent playback",
}] : []),
]}
conclusion={{
proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.",
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, temporal stability, collision safety и physical-live поведение ровера.",
decision: "Сохранить DDRNet как стартовый vegetation candidate. Mission-policy и автоматическое переключение пресетов подключать только после truth-backed island нашего офф-роуда; LiDAR/TGS fail-closed геометрию не ослаблять.",
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, collision safety и physical-live поведение ровера. Видео позволяет увидеть temporal stability, но без truth не превращает её в метрику качества.",
decision: "Смотреть полный prediction на видео и собирать конкретные temporal/domain failure cases. DDRNet остаётся diagnostic candidate; LiDAR/TGS fail-closed геометрию не ослаблять.",
}}
/>
)}