fix(perception): reuse M4.8 for vegetation evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 00:13:58 +03:00
parent 57204b3b0b
commit 67e6ae98e2
12 changed files with 482 additions and 226 deletions
@@ -4,11 +4,25 @@ const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
const SHA256 = /^[a-f0-9]{64}$/; const SHA256 = /^[a-f0-9]{64}$/;
const CANDIDATES = ["ddrnet", "ppliteseg"] as const; const CANDIDATES = ["ddrnet", "ppliteseg"] as const;
const ROUTE_MODES = ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"] as const; const ROUTE_MODES = ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"] as const;
const VALIDATION_MODES = ["source", "truth", "ddrnet", "ppliteseg"] as const; const VALIDATION_ASSETS = [
"source",
"truth",
"ddrnet",
"ppliteseg",
"ddrnet_error",
"ppliteseg_error",
] as const;
export type VegetationCandidateKey = typeof CANDIDATES[number]; export type VegetationCandidateKey = typeof CANDIDATES[number];
export type VegetationRouteMode = typeof ROUTE_MODES[number]; export type VegetationRouteMode = typeof ROUTE_MODES[number];
export type VegetationValidationMode = typeof VALIDATION_MODES[number];
export interface VegetationVisualFocus {
className: string;
labelId: number;
truthPixels: number;
truthFraction: number;
stratumRank: number;
}
export interface VegetationCandidateMetrics { export interface VegetationCandidateMetrics {
candidate: VegetationCandidateKey; candidate: VegetationCandidateKey;
@@ -33,6 +47,7 @@ export interface VegetationVisualCase {
height: number; height: number;
centerCropXyxy: readonly [number, number, number, number]; centerCropXyxy: readonly [number, number, number, number];
outsideCropState: "undefined" | "not-applicable"; outsideCropState: "undefined" | "not-applicable";
focus: VegetationVisualFocus | null;
assets: Readonly<Record<string, string>>; assets: Readonly<Record<string, string>>;
} }
@@ -174,7 +189,7 @@ function visualCaseValue(
.join("/")}`; .join("/")}`;
} }
const expectedAssets = expectedKind === "goose" const expectedAssets = expectedKind === "goose"
? VALIDATION_MODES ? VALIDATION_ASSETS
: ROUTE_MODES; : ROUTE_MODES;
if (expectedAssets.some((key) => !projected[key])) { if (expectedAssets.some((key) => !projected[key])) {
throw new VegetationShadowContractError(`vegetation.case.assets: ${expectedKind} набор неполон.`); throw new VegetationShadowContractError(`vegetation.case.assets: ${expectedKind} набор неполон.`);
@@ -183,6 +198,23 @@ function visualCaseValue(
if (outsideCropState !== "undefined" && outsideCropState !== "not-applicable") { if (outsideCropState !== "undefined" && outsideCropState !== "not-applicable") {
throw new VegetationShadowContractError("vegetation.case.outside_crop_state: контракт изменён."); throw new VegetationShadowContractError("vegetation.case.outside_crop_state: контракт изменён.");
} }
let focus: VegetationVisualFocus | null = null;
if (expectedKind === "goose") {
const rawFocus = objectValue(row.focus, "vegetation.case.focus");
const truthFraction = numberValue(rawFocus.truth_fraction, "vegetation.case.focus.truth_fraction");
if (truthFraction <= 0 || truthFraction > 1) {
throw new VegetationShadowContractError("vegetation.case.focus.truth_fraction: диапазон изменён.");
}
focus = {
className: textValue(rawFocus.class_name, "vegetation.case.focus.class_name"),
labelId: integerValue(rawFocus.label_id, "vegetation.case.focus.label_id"),
truthPixels: integerValue(rawFocus.truth_pixels, "vegetation.case.focus.truth_pixels"),
truthFraction,
stratumRank: integerValue(rawFocus.stratum_rank, "vegetation.case.focus.stratum_rank"),
};
} else if (row.focus !== null) {
throw new VegetationShadowContractError("vegetation.case.focus: RAVNOVES focus отсутствует.");
}
return { return {
caseId, caseId,
sourceKind: expectedKind, sourceKind: expectedKind,
@@ -190,6 +222,7 @@ function visualCaseValue(
height: integerValue(row.height, "vegetation.case.height"), height: integerValue(row.height, "vegetation.case.height"),
centerCropXyxy: crop as unknown as readonly [number, number, number, number], centerCropXyxy: crop as unknown as readonly [number, number, number, number],
outsideCropState, outsideCropState,
focus,
assets: projected, assets: projected,
}; };
} }
@@ -233,8 +266,8 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
.map((item) => visualCaseValue(item, resultId, "ravnoves")); .map((item) => visualCaseValue(item, resultId, "ravnoves"));
const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose") const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
.map((item) => visualCaseValue(item, resultId, "goose")); .map((item) => visualCaseValue(item, resultId, "goose"));
if (routeCases.length !== 12 || validationCases.length !== 12) { if (routeCases.length !== 0 || validationCases.length !== 12) {
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 + 12 случаев."); throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
} }
return { return {
resultId, resultId,
@@ -28,6 +28,119 @@ const ATLAS_MODES = [
] as const; ] as const;
type AtlasMode = typeof ATLAS_MODES[number]["value"]; type AtlasMode = typeof ATLAS_MODES[number]["value"];
const MASK_COMPARISON_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "truth", label: "TRUTH" },
{ value: "prediction", label: "PREDICTION" },
{ value: "error", label: "ERROR" },
] as const;
const MASK_COMPARISON_CANDIDATES = [
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
] as const;
type MaskComparisonMode = typeof MASK_COMPARISON_MODES[number]["value"];
type MaskComparisonCandidate = typeof MASK_COMPARISON_CANDIDATES[number]["value"];
export interface M48MaskComparisonCase {
caseId: string;
title: string;
context?: string;
sourceUrl: string;
truthUrl: string;
predictions: Readonly<Record<MaskComparisonCandidate, string>>;
errors: Readonly<Record<MaskComparisonCandidate, string>>;
}
function M48MaskComparisonScene({
item,
mode,
candidate,
}: {
item: M48MaskComparisonCase;
mode: MaskComparisonMode;
candidate: MaskComparisonCandidate;
}) {
const overlay = mode === "truth"
? item.truthUrl
: mode === "prediction"
? item.predictions[candidate]
: mode === "error"
? item.errors[candidate]
: null;
return (
<div className="recorded-evidence-image-scene">
<img src={item.sourceUrl} alt="" draggable={false} />
{overlay ? <img src={overlay} alt="" draggable={false} /> : null}
</div>
);
}
export function M48MaskComparisonVisual({
cases,
initialCandidate,
}: {
cases: readonly M48MaskComparisonCase[];
initialCandidate: MaskComparisonCandidate;
}) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<MaskComparisonMode>("error");
const [candidate, setCandidate] = useState<MaskComparisonCandidate>(initialCandidate);
const [expanded, setExpanded] = useState(false);
const item = cases[index] ?? null;
return (
<LaboratoryEvidenceViewer
label="M4.8 vegetation truth comparison"
className="m48-atlas-visual"
mode={mode}
modes={MASK_COMPARISON_MODES}
secondaryMode={{
value: candidate,
modes: MASK_COMPARISON_CANDIDATES,
label: "Сравниваемая модель",
onChange: setCandidate,
}}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
chromeLayout="stacked"
actions={(
<>
<IconButton
label="Предыдущий vegetation hard case"
disabled={!cases.length}
onClick={() => setIndex((current) => (current - 1 + cases.length) % cases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий vegetation hard case"
disabled={!cases.length}
onClick={() => setIndex((current) => (current + 1) % cases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="accent">GOOSE TRUTH · {index + 1}/{cases.length}</StatusBadge>
<strong>{item.title}</strong>
{item.context ? <small>{item.context}</small> : null}
</div>
) : null}
>
{item ? (
<M48MaskComparisonScene item={item} mode={mode} candidate={candidate} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
Vegetation hard-case каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
function message(error: unknown): string { function message(error: unknown): string {
return error instanceof Error && error.message.trim() ? error.message : "M4.8 evidence недоступно."; return error instanceof Error && error.message.trim() ? error.message : "M4.8 evidence недоступно.";
} }
@@ -6,14 +6,45 @@ import {
} from "../../components/laboratory/LaboratoryPresentation"; } from "../../components/laboratory/LaboratoryPresentation";
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow"; import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
import { import {
VegetationRouteVisual, M48MaskComparisonVisual,
VegetationValidationVisual, type M48MaskComparisonCase,
} from "./VegetationShadowVisual"; } from "./M48FailureAtlasVisual";
function decimal(value: number, digits = 1): string { function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits }); return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
} }
const VEGETATION_LABELS: Readonly<Record<string, string>> = {
high_grass: "Высокая трава",
low_grass: "Низкая трава",
bush: "Куст",
tree_trunk: "Ствол дерева",
tree_crown: "Крона дерева",
hedge: "Живая изгородь",
forest: "Лесная растительность",
crops: "Посевы",
};
function comparisonCases(result: VegetationShadowResult): readonly M48MaskComparisonCase[] {
return result.validationCases.map((item) => {
const focus = item.focus!;
return {
caseId: item.caseId,
title: `${VEGETATION_LABELS[focus.className] ?? focus.className} · truth ${decimal(focus.truthFraction * 100, 1)}% кадра`,
sourceUrl: item.assets.source,
truthUrl: item.assets.truth,
predictions: {
ddrnet: item.assets.ddrnet,
ppliteseg: item.assets.ppliteseg,
},
errors: {
ddrnet: item.assets.ddrnet_error,
ppliteseg: item.assets.ppliteseg_error,
},
};
});
}
export function VegetationShadowResultView({ export function VegetationShadowResultView({
rigLabel, rigLabel,
result, result,
@@ -31,21 +62,21 @@ export function VegetationShadowResultView({
<LaboratoryWorkTemplate <LaboratoryWorkTemplate
summary={( summary={(
<LaboratorySummary <LaboratorySummary
title="LAB V1 · растительность и mission-policy пресеты" title="LAB V1 · готовые модели растительности"
description="Две готовые fine-64 модели GOOSE проверены на полном validation split и перенесены в автономный визуальный shadow по RAVNOVES00. Интерфейс читает sealed-кадры локально и не зависит от доступности Worker 006." description="Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."
status="Визуальный shadow готов · navigation authority OFF" status="Truth-backed model comparison · route transfer не принят"
statusTone="warning" statusTone="warning"
facts={[ facts={[
{ label: "Источник", value: `${rigLabel} RIGHT · 12 raw KB4 кадров + GOOSE validation 962` }, { label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" },
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" }, { label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
{ label: "Policy", value: "Urban / rural / off-road · mission-configurable" }, { label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
{ label: "Authority", value: "SHADOW ONLY · commands OFF · geometry stays authoritative" }, { label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
]} ]}
brief={{ brief={{
question: "Можно ли взять готовую сегментацию растительности, увидеть её на нашем маршруте и сразу проверить разные правила миссии?", question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime: сначала 962 размеченных GOOSE-кадра, затем 12 детерминированных кадров RAVNOVES00. Для каждого кадра запечатаны source, обе семантики и три policy-проекции.", approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.",
principalResult: `${selected.loadedModelName} выбран по vegetation IoU ${decimal(selected.vegetationMeanIouPercent, 2)}% при shadow p95 ${decimal(selected.shadowLatencyP95Ms, 2)} ms. Визуальный результат доступен локально без Worker.`, principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. Ошибки по каждому типу теперь проверяются в одном штатном инструменте.`,
limitation: "RAVNOVES00 не размечен по fine-64, поэтому это перенос и визуальная проверка, а не доказательство точности или безопасности. Камерная семантика не может очищать жёсткую LiDAR/TGS occupancy.", limitation: "Это внешний GOOSE-домен, а не наш fisheye/off-road маршрут. Папоротник отдельным классом отсутствует; RAVNOVES00 не содержит truth-backed vegetation island и не используется как главное визуальное доказательство.",
}} }}
method={{ method={{
completeness: "complete", completeness: "complete",
@@ -62,29 +93,22 @@ export function VegetationShadowResultView({
/> />
)} )}
evidence={( evidence={(
<> <LaboratoryEvidence
<LaboratoryEvidence eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
eyebrow="RAVNOVES00 · AUTONOMOUS VISUAL SHADOW" title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
title="Источник, обе модели и три правила миссии на одинаковых кадрах" kind="diagnostic-model"
kind="diagnostic-model" resizable
resizable >
> <M48MaskComparisonVisual
<VegetationRouteVisual result={result} /> cases={comparisonCases(result)}
</LaboratoryEvidence> initialCandidate={result.selectedCandidate}
<LaboratoryEvidence />
eyebrow="GOOSE · EXTERNAL VALIDATION EVIDENCE" </LaboratoryEvidence>
title="Независимая разметка: truth против DDRNet и PPLiteSeg"
kind="diagnostic-model"
resizable
>
<VegetationValidationVisual result={result} />
</LaboratoryEvidence>
</>
)} )}
result={( result={(
<LaboratoryResultSummary <LaboratoryResultSummary
title="Готовые веса дают рабочую точку старта, но ещё не право ехать" title="DDRNet — стартовые веса; перенос на ровер ещё не доказан"
status={`${selected.loadedModelName} выбран для shadow`} status={`${selected.loadedModelName} выбран только как vegetation candidate`}
statusTone="warning" statusTone="warning"
metrics={[ metrics={[
{ {
@@ -98,7 +122,7 @@ export function VegetationShadowResultView({
hint: "агрегация классов grass/vegetation/bush/tree и родственных fine-64 labels", hint: "агрегация классов grass/vegetation/bush/tree и родственных fine-64 labels",
}, },
{ {
label: "RAVNOVES p95", label: "Worker shadow p95",
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`, value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`,
hint: "чистый inference · одна тяжёлая модель за раз", hint: "чистый inference · одна тяжёлая модель за раз",
}, },
@@ -108,7 +132,7 @@ export function VegetationShadowResultView({
hint: "один явный inference до допуска кадров; исключён из steady-state p95", hint: "один явный inference до допуска кадров; исключён из steady-state p95",
}, },
{ {
label: "RAVNOVES throughput", label: "Worker throughput",
value: `${decimal(selected.shadowThroughputFps, 1)} / ${decimal(alternative.shadowThroughputFps, 1)} FPS`, value: `${decimal(selected.shadowThroughputFps, 1)} / ${decimal(alternative.shadowThroughputFps, 1)} FPS`,
hint: "изолированный Worker 006 · не realtime graph целиком", hint: "изолированный Worker 006 · не realtime graph целиком",
}, },
@@ -118,15 +142,15 @@ export function VegetationShadowResultView({
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`, hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
}, },
{ {
label: "Autonomous evidence", label: "Hard-case evidence",
value: "12 route + 12 validation", value: "12 truth-backed cases",
hint: "только текущий кадр загружается в viewer; Worker не требуется", hint: "8 vegetation strata · Worker для открытия не требуется",
}, },
]} ]}
conclusion={{ conclusion={{
proved: "Обе официальные fine-64 модели запускаются на Worker 006, проходят полный GOOSE validation и дают воспроизводимые растительные маски на 12 фиксированных RAVNOVES00 кадрах. Urban/rural/off-road policy-проекции формируются без повторного inference.", proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.",
notProved: "Не доказаны accuracy на нашем fisheye-домене, различение тонкой травы от толстого ствола во всех условиях, temporal stability, collision safety и physical-live поведение ровера.", notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, temporal stability, collision safety и physical-live поведение ровера.",
decision: "Сохранить выбранную модель как shadow provider. Следующий критический блок — разметить небольшой hard-case island нашего офф-роуда: трава, папоротник, куст с толстыми стволами и дерево; затем калибровать policy без ослабления LiDAR/TGS fail-closed геометрии.", decision: "Сохранить DDRNet как стартовый vegetation candidate. Mission-policy и автоматическое переключение пресетов подключать только после truth-backed island нашего офф-роуда; LiDAR/TGS fail-closed геометрию не ослаблять.",
}} }}
/> />
)} )}
@@ -1,157 +0,0 @@
import { useState } from "react";
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type {
VegetationRouteMode,
VegetationShadowResult,
VegetationValidationMode,
VegetationVisualCase,
} from "../../core/laboratory/vegetationShadow";
const ROUTE_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
{ value: "urban", label: "URBAN" },
{ value: "rural", label: "RURAL" },
{ value: "offroad", label: "OFF-ROAD" },
] as const;
const VALIDATION_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "truth", label: "TRUTH" },
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
] as const;
function VegetationScene({
item,
mode,
}: {
item: VegetationVisualCase;
mode: string;
}) {
const overlay = mode === "source" ? null : item.assets[mode];
return (
<div className="recorded-evidence-image-scene">
<img src={item.assets.source} alt="" draggable={false} />
{overlay ? <img src={overlay} alt="" draggable={false} /> : null}
</div>
);
}
export function VegetationRouteVisual({ result }: { result: VegetationShadowResult }) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<VegetationRouteMode>("offroad");
const [expanded, setExpanded] = useState(false);
const item = result.routeCases[index] ?? null;
const selected = result.candidates.find(
(candidate) => candidate.candidate === result.selectedCandidate,
);
return (
<LaboratoryEvidenceViewer
label="RAVNOVES00 vegetation policy shadow"
className="m48-atlas-visual"
mode={mode}
modes={ROUTE_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<>
<IconButton
label="Предыдущий vegetation shadow кадр"
disabled={!result.routeCases.length}
onClick={() => setIndex((current) => (
current - 1 + result.routeCases.length
) % result.routeCases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий vegetation shadow кадр"
disabled={!result.routeCases.length}
onClick={() => setIndex((current) => (current + 1) % result.routeCases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="warning">SHADOW ONLY</StatusBadge>
<strong>RAVNOVES00 · {item.caseId} · {mode.toUpperCase()}</strong>
<small>
{selected?.loadedModelName ?? result.selectedCandidate} policy provider
{" · "}center crop 600×600
{" · "}outside crop UNKNOWN
</small>
</div>
) : null}
>
{item ? (
<VegetationScene item={item} mode={mode} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
RAVNOVES vegetation shadow каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
export function VegetationValidationVisual({ result }: { result: VegetationShadowResult }) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<VegetationValidationMode>("truth");
const [expanded, setExpanded] = useState(false);
const item = result.validationCases[index] ?? null;
return (
<LaboratoryEvidenceViewer
label="GOOSE validation vegetation comparison"
className="m48-atlas-visual"
mode={mode}
modes={VALIDATION_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<>
<IconButton
label="Предыдущий GOOSE validation кадр"
disabled={!result.validationCases.length}
onClick={() => setIndex((current) => (
current - 1 + result.validationCases.length
) % result.validationCases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий GOOSE validation кадр"
disabled={!result.validationCases.length}
onClick={() => setIndex((current) => (current + 1) % result.validationCases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="accent">GOOSE VALIDATION</StatusBadge>
<strong>{item.caseId} · {mode.toUpperCase()}</strong>
<small>official fine-64 labels · fixed 512×512 preprocessing · visual sample</small>
</div>
) : null}
>
{item ? (
<VegetationScene item={item} mode={mode} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
GOOSE validation каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
@@ -65,10 +65,10 @@ const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный
const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = { const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = {
"lab-v1-vegetation-shadow": { "lab-v1-vegetation-shadow": {
profileId: "rig-ravnoves-perception-gate-v1", profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 vegetation shadow`, profileName: (rigLabel) => `${rig(rigLabel)} · GOOSE vegetation qualification`,
experimentId: "lab-v1-vegetation-mission-policy", experimentId: "lab-v1-vegetation-mission-policy",
experimentName: "GOOSE ready weights → RAVNOVES vegetation policy", experimentName: "DDRNet vs PPLiteSeg · truth-backed vegetation hard cases",
variantName: "LAB V1 · DDRNet vs PPLiteSeg · urban/rural/off-road presets", variantName: "LAB V1 · готовые vegetation weights · GOOSE truth",
}, },
"m48-object-centric-quality": { "m48-object-centric-quality": {
profileId: "rig-dual-evidence-virtual-corridor-v1", profileId: "rig-dual-evidence-virtual-corridor-v1",
@@ -1,4 +1,5 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { access, readFile } from "node:fs/promises";
import { after, before, test } from "node:test"; import { after, before, test } from "node:test";
import { createServer } from "vite"; import { createServer } from "vite";
@@ -51,7 +52,7 @@ function candidate(candidateKey, vegetationIou) {
function visualCase(sourceKind, index) { function visualCase(sourceKind, index) {
const caseId = `case-${index}`; const caseId = `case-${index}`;
const keys = sourceKind === "goose" const keys = sourceKind === "goose"
? ["source", "truth", "ddrnet", "ppliteseg"] ? ["source", "truth", "ddrnet", "ppliteseg", "ddrnet_error", "ppliteseg_error"]
: ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"]; : ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"];
return { return {
case_id: caseId, case_id: caseId,
@@ -60,6 +61,13 @@ function visualCase(sourceKind, index) {
height: sourceKind === "goose" ? 512 : 600, height: sourceKind === "goose" ? 512 : 600,
center_crop_xyxy: sourceKind === "goose" ? [0, 0, 512, 512] : [100, 0, 700, 600], center_crop_xyxy: sourceKind === "goose" ? [0, 0, 512, 512] : [100, 0, 700, 600],
outside_crop_state: sourceKind === "goose" ? "not-applicable" : "undefined", outside_crop_state: sourceKind === "goose" ? "not-applicable" : "undefined",
focus: sourceKind === "goose" ? {
class_name: "high_grass",
label_id: 51,
truth_pixels: 16384,
truth_fraction: 0.0625,
stratum_rank: index + 1,
} : null,
assets: Object.fromEntries(keys.map((key) => [key, { assets: Object.fromEntries(keys.map((key) => [key, {
path: `visual/${sourceKind}/${caseId}/${key}.png`, path: `visual/${sourceKind}/${caseId}/${key}.png`,
sha256: "d".repeat(64), sha256: "d".repeat(64),
@@ -101,7 +109,7 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
}, },
catalogs: { catalogs: {
goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)), goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)),
ravnoves: Array.from({ length: 12 }, (_, index) => visualCase("ravnoves", index)), ravnoves: [],
}, },
access: "read-only", access: "read-only",
}), { status: 200, headers: { "Content-Type": "application/json" } }); }), { status: 200, headers: { "Content-Type": "application/json" } });
@@ -113,9 +121,10 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
); );
assert.equal(result.selectedCandidate, "ddrnet"); assert.equal(result.selectedCandidate, "ddrnet");
assert.equal(result.candidates[0].vegetationMeanIouPercent, 64); assert.equal(result.candidates[0].vegetationMeanIouPercent, 64);
assert.equal(result.routeCases.length, 12); assert.equal(result.routeCases.length, 0);
assert.equal(result.validationCases.length, 12); assert.equal(result.validationCases.length, 12);
assert.match(result.routeCases[0].assets.offroad, /\/assets\/visual\/ravnoves\//); assert.equal(result.validationCases[0].focus.className, "high_grass");
assert.match(result.validationCases[0].assets.ddrnet_error, /\/assets\/visual\/goose\//);
assert.deepEqual(result.authority, { assert.deepEqual(result.authority, {
commandsEnabled: false, commandsEnabled: false,
navigationOrSafetyAccepted: false, navigationOrSafetyAccepted: false,
@@ -123,3 +132,17 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
cameraSemanticsCanClearRigidGeometry: false, cameraSemanticsCanClearRigidGeometry: false,
}); });
}); });
test("vegetation LAB reuses the admitted M4.8 instrument", async () => {
const resultSource = await readFile(
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
"utf8",
);
assert.match(resultSource, /M48MaskComparisonVisual/);
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 1);
assert.doesNotMatch(resultSource, /VegetationRouteVisual|urban\/rural\/off-road presets/);
await assert.rejects(
access(new URL("../src/workspaces/laboratory/VegetationShadowVisual.tsx", import.meta.url)),
{ code: "ENOENT" },
);
});
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"schema_version": "missioncore.laboratory-value-review-registry/v1", "schema_version": "missioncore.laboratory-value-review-registry/v1",
"reviewed_at_utc": "2026-08-27T20:20:14Z", "reviewed_at_utc": "2026-08-27T20:59:10Z",
"entries": [ "entries": [
{ {
"catalog_id": "e28-local-surface", "catalog_id": "e28-local-surface",
@@ -285,7 +285,7 @@
{ {
"catalog_id": "lab-v1-vegetation-shadow", "catalog_id": "lab-v1-vegetation-shadow",
"evidence_id": "lab-v1-vegetation-shadow-ad4d9fbbb21ff8a270b77f559b4e78dcdaf0455afd61afb5033009623984e554", "evidence_id": "lab-v1-vegetation-shadow-ad4d9fbbb21ff8a270b77f559b4e78dcdaf0455afd61afb5033009623984e554",
"signal": "progress", "signal": "failed",
"lifecycle": "current", "lifecycle": "current",
"visual_evidence": "available" "visual_evidence": "available"
} }
@@ -79,6 +79,27 @@
"hedge", "hedge",
"tree_root" "tree_root"
], ],
"visual_case_contract": {
"selection_basis": "ground-truth-class-support-only",
"case_count": 12,
"minimum_focus_pixels": 2048,
"strata": [
{ "class_name": "high_grass", "count": 2 },
{ "class_name": "low_grass", "count": 2 },
{ "class_name": "bush", "count": 2 },
{ "class_name": "tree_trunk", "count": 2 },
{ "class_name": "tree_crown", "count": 1 },
{ "class_name": "hedge", "count": 1 },
{ "class_name": "forest", "count": 1 },
{ "class_name": "crops", "count": 1 }
],
"error_overlay": {
"correct_material_rgba": [34, 197, 94, 72],
"missed_vegetation_rgba": [239, 68, 68, 220],
"false_vegetation_rgba": [245, 158, 11, 220],
"wrong_vegetation_material_rgba": [168, 85, 247, 220]
}
},
"policy_action_colors": { "policy_action_colors": {
"ALLOW": "#22c55e", "ALLOW": "#22c55e",
"HIGH_COST": "#f59e0b", "HIGH_COST": "#f59e0b",
@@ -161,18 +161,81 @@ def find_goose_pairs(root: Path) -> list[tuple[Path, Path]]:
return pairs return pairs
def visual_indices(count: int, visual_count: int) -> set[int]: def visual_indices(count: int, visual_count: int) -> dict[int, dict[str, Any]]:
if count <= 0 or visual_count <= 0: if count <= 0 or visual_count <= 0:
return set() return {}
selected_count = min(count, visual_count) selected_count = min(count, visual_count)
if selected_count == 1: if selected_count == 1:
return {0} return {0: {}}
return { return {
round(index * (count - 1) / (selected_count - 1)) round(index * (count - 1) / (selected_count - 1)): {}
for index in range(selected_count) for index in range(selected_count)
} }
def truth_focused_visuals(
items: list[tuple[str, Path, Path | None]],
names: dict[int, str],
contract: dict[str, Any],
) -> dict[int, dict[str, Any]]:
if contract.get("selection_basis") != "ground-truth-class-support-only":
raise RunnerError("visual selection basis changed")
case_count = contract.get("case_count")
minimum_pixels = contract.get("minimum_focus_pixels")
strata = contract.get("strata")
if (
not isinstance(case_count, int)
or case_count <= 0
or not isinstance(minimum_pixels, int)
or minimum_pixels <= 0
or not isinstance(strata, list)
or sum(row.get("count", 0) for row in strata if isinstance(row, dict)) != case_count
):
raise RunnerError("visual case contract is invalid")
ids_by_name = {class_name: label_id for label_id, class_name in names.items()}
supports: list[dict[int, int]] = []
for _, _, label_path in items:
if label_path is None:
raise RunnerError("truth-focused selection requires labels")
truth = preprocess_label(Image.open(label_path))
values, counts = np.unique(truth, return_counts=True)
supports.append({int(value): int(count) for value, count in zip(values, counts)})
selected: dict[int, dict[str, Any]] = {}
for raw in strata:
if not isinstance(raw, dict):
raise RunnerError("visual stratum is invalid")
class_name = raw.get("class_name")
count = raw.get("count")
if class_name not in ids_by_name or not isinstance(count, int) or count <= 0:
raise RunnerError("visual stratum identity changed")
label_id = ids_by_name[class_name]
ranked = sorted(
(
(support.get(label_id, 0), items[index][0], index)
for index, support in enumerate(supports)
if index not in selected and support.get(label_id, 0) > 0
),
key=lambda row: (-row[0], row[1]),
)
admitted = [row for row in ranked if row[0] >= minimum_pixels]
if len(admitted) < count:
admitted = ranked
if len(admitted) < count:
raise RunnerError(f"visual stratum {class_name} has fewer than {count} cases")
for rank, (truth_pixels, _, index) in enumerate(admitted[:count], start=1):
selected[index] = {
"class_name": class_name,
"label_id": label_id,
"truth_pixels": truth_pixels,
"truth_fraction": round(truth_pixels / float(512 * 512), 8),
"stratum_rank": rank,
}
if len(selected) != case_count:
raise RunnerError("truth-focused visual selection did not produce the frozen case count")
return selected
def load_model(candidate: str, checkpoint: Path) -> tuple[torch.nn.Module, str, list[str]]: def load_model(candidate: str, checkpoint: Path) -> tuple[torch.nn.Module, str, list[str]]:
failures: list[str] = [] failures: list[str] = []
for model_name in MODEL_NAMES[candidate]: for model_name in MODEL_NAMES[candidate]:
@@ -297,6 +360,9 @@ def write_visual_case(
policy_palettes: dict[str, np.ndarray], policy_palettes: dict[str, np.ndarray],
crop_box: tuple[int, int, int, int], crop_box: tuple[int, int, int, int],
truth: np.ndarray | None = None, truth: np.ndarray | None = None,
focus: dict[str, Any] | None = None,
material_codes: np.ndarray | None = None,
error_colors: dict[str, list[int]] | None = None,
preserve_source_size: bool = False, preserve_source_size: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
case_root = output / "cases" / case_id case_root = output / "cases" / case_id
@@ -343,6 +409,26 @@ def write_visual_case(
"relative_path": truth_semantic_path.relative_to(output).as_posix(), "relative_path": truth_semantic_path.relative_to(output).as_posix(),
"sha256": save_image(truth_semantic_path, semantic_palette[truth_image], "RGBA"), "sha256": save_image(truth_semantic_path, semantic_palette[truth_image], "RGBA"),
} }
if material_codes is None or error_colors is None:
raise RunnerError("truth visual case requires the material-error contract")
truth_material = material_codes[truth_image]
predicted_material = material_codes[prediction_image]
truth_vegetation = truth_material > 0
predicted_vegetation = predicted_material > 0
error_overlay = np.zeros((*truth_image.shape, 4), dtype=np.uint8)
correct = truth_vegetation & (truth_material == predicted_material)
missed = truth_vegetation & ~predicted_vegetation
false_positive = ~truth_vegetation & predicted_vegetation
wrong_material = truth_vegetation & predicted_vegetation & (truth_material != predicted_material)
error_overlay[correct] = error_colors["correct_material_rgba"]
error_overlay[missed] = error_colors["missed_vegetation_rgba"]
error_overlay[false_positive] = error_colors["false_vegetation_rgba"]
error_overlay[wrong_material] = error_colors["wrong_vegetation_material_rgba"]
error_path = case_root / "vegetation-material-error.png"
files["vegetation_material_error"] = {
"relative_path": error_path.relative_to(output).as_posix(),
"sha256": save_image(error_path, error_overlay, "RGBA"),
}
return { return {
"schema_version": VISUAL_SCHEMA, "schema_version": VISUAL_SCHEMA,
"case_id": case_id, "case_id": case_id,
@@ -350,6 +436,7 @@ def write_visual_case(
"source_height": source_image.height, "source_height": source_image.height,
"center_crop_xyxy": list(crop_box), "center_crop_xyxy": list(crop_box),
"outside_crop_state": "undefined" if preserve_source_size else "not-applicable", "outside_crop_state": "undefined" if preserve_source_size else "not-applicable",
"focus": focus,
"files": files, "files": files,
} }
@@ -400,6 +487,27 @@ def run() -> None:
) )
for preset in ("urban", "rural", "offroad") for preset in ("urban", "rural", "offroad")
} }
provider_labels = provider_map["providers"]["goose-fine-64"]["labels"]
vegetation_materials = sorted(
{
material
for material in provider_labels.values()
if material in {
"grass",
"herbaceous_vegetation",
"cultivated_vegetation",
"woody_shrub",
"tree_or_trunk",
"vegetation_unknown",
}
}
)
material_code_by_name = {
material: index for index, material in enumerate(vegetation_materials, start=1)
}
material_codes = np.zeros(CLASS_COUNT, dtype=np.uint8)
for label_id, class_name in names.items():
material_codes[label_id] = material_code_by_name.get(provider_labels.get(class_name), 0)
if args.mode == "goose": if args.mode == "goose":
pairs = find_goose_pairs(mapping_root) pairs = find_goose_pairs(mapping_root)
@@ -423,6 +531,17 @@ def run() -> None:
if not items: if not items:
raise RunnerError("no inputs were selected") raise RunnerError("no inputs were selected")
visual_contract = config.get("visual_case_contract")
if not isinstance(visual_contract, dict):
raise RunnerError("visual case contract is unavailable")
configured_visual_count = visual_contract.get("case_count")
if args.mode == "goose":
if args.visual_count != configured_visual_count:
raise RunnerError("GOOSE visual count differs from the truth-focused contract")
selected_visuals = truth_focused_visuals(items, names, visual_contract)
else:
selected_visuals = visual_indices(len(items), args.visual_count)
args.output.mkdir(parents=True, exist_ok=False) args.output.mkdir(parents=True, exist_ok=False)
torch.cuda.empty_cache() torch.cuda.empty_cache()
model, model_name, architecture_failures = load_model(args.candidate, args.checkpoint) model, model_name, architecture_failures = load_model(args.candidate, args.checkpoint)
@@ -430,7 +549,6 @@ def run() -> None:
warmup_tensor, _ = preprocess(warmup_source) warmup_tensor, _ = preprocess(warmup_source)
warmup_latencies_ms = [infer(model, warmup_tensor)[1] for _ in range(3)] warmup_latencies_ms = [infer(model, warmup_tensor)[1] for _ in range(3)]
torch.cuda.reset_peak_memory_stats() torch.cuda.reset_peak_memory_stats()
selected_visuals = visual_indices(len(items), args.visual_count)
confusion = np.zeros((CLASS_COUNT, CLASS_COUNT), dtype=np.int64) confusion = np.zeros((CLASS_COUNT, CLASS_COUNT), dtype=np.int64)
latencies_ms: list[float] = [] latencies_ms: list[float] = []
visuals: list[dict[str, Any]] = [] visuals: list[dict[str, Any]] = []
@@ -454,6 +572,9 @@ def run() -> None:
policy_palettes, policy_palettes,
crop_box, crop_box,
truth=truth, truth=truth,
focus=selected_visuals[index] or None,
material_codes=material_codes,
error_colors=visual_contract["error_overlay"],
preserve_source_size=args.mode == "ravnoves", preserve_source_size=args.mode == "ravnoves",
) )
) )
+57 -4
View File
@@ -16,6 +16,16 @@ WORKER_SCHEMA: Final = "missioncore.lab-v1-goose-vegetation-run/v1"
RESULT_PREFIX: Final = "lab-v1-vegetation-shadow-" RESULT_PREFIX: Final = "lab-v1-vegetation-shadow-"
_CANDIDATES: Final = ("ddrnet", "ppliteseg") _CANDIDATES: Final = ("ddrnet", "ppliteseg")
_MODES: Final = ("goose", "ravnoves") _MODES: Final = ("goose", "ravnoves")
_FOCUS_ORDER: Final = (
"high_grass",
"low_grass",
"bush",
"tree_trunk",
"tree_crown",
"hedge",
"forest",
"crops",
)
_IMAGE_KEYS: Final = ( _IMAGE_KEYS: Final = (
"source", "source",
"prediction_semantic", "prediction_semantic",
@@ -93,6 +103,22 @@ def _case_map(result: dict[str, Any], label: str) -> dict[str, dict[str, Any]]:
return rows return rows
def _visual_case_order(row: dict[str, Any]) -> tuple[int, int, str]:
focus = _object(row.get("focus"), "GOOSE visual focus")
class_name = focus.get("class_name")
stratum_rank = focus.get("stratum_rank")
case_id = row.get("case_id")
if (
not isinstance(class_name, str)
or class_name not in _FOCUS_ORDER
or not isinstance(stratum_rank, int)
or stratum_rank <= 0
or not isinstance(case_id, str)
):
raise VegetationShadowLabError("GOOSE visual focus ordering is invalid")
return _FOCUS_ORDER.index(class_name), stratum_rank, case_id
def _file_from_case( def _file_from_case(
root: Path, root: Path,
case: dict[str, Any], case: dict[str, Any],
@@ -198,8 +224,14 @@ def seal_vegetation_shadow_lab(
artifacts: list[dict[str, object]] = [] artifacts: list[dict[str, object]] = []
catalogs: dict[str, list[dict[str, object]]] = {"goose": [], "ravnoves": []} catalogs: dict[str, list[dict[str, object]]] = {"goose": [], "ravnoves": []}
try: try:
for mode in _MODES: # RAVNOVES is retained in the immutable Worker proof and timing summary,
for case_id in sorted(cases[("ddrnet", mode)]): # but it has no vegetation truth island. Publishing those urban frames
# as primary visual cases would misrepresent the operator question.
for mode in ("goose",):
for case_id in sorted(
cases[("ddrnet", mode)],
key=lambda value: _visual_case_order(cases[("ddrnet", mode)][value]),
):
ddr_case = cases[("ddrnet", mode)][case_id] ddr_case = cases[("ddrnet", mode)][case_id]
pplite_case = cases[("ppliteseg", mode)][case_id] pplite_case = cases[("ppliteseg", mode)][case_id]
row: dict[str, object] = { row: dict[str, object] = {
@@ -209,8 +241,13 @@ def seal_vegetation_shadow_lab(
"height": ddr_case.get("source_height"), "height": ddr_case.get("source_height"),
"center_crop_xyxy": ddr_case.get("center_crop_xyxy"), "center_crop_xyxy": ddr_case.get("center_crop_xyxy"),
"outside_crop_state": ddr_case.get("outside_crop_state"), "outside_crop_state": ddr_case.get("outside_crop_state"),
"focus": ddr_case.get("focus"),
"assets": {}, "assets": {},
} }
if ddr_case.get("focus") != pplite_case.get("focus"):
raise VegetationShadowLabError(
f"{mode} case {case_id} focus contract differs between candidates"
)
asset_map = _object(row["assets"], "sealed assets") asset_map = _object(row["assets"], "sealed assets")
sources: list[tuple[str, str, dict[str, Any], str]] = [ sources: list[tuple[str, str, dict[str, Any], str]] = [
("source", "ddrnet", ddr_case, "source"), ("source", "ddrnet", ddr_case, "source"),
@@ -218,7 +255,23 @@ def seal_vegetation_shadow_lab(
("ppliteseg", "ppliteseg", pplite_case, "prediction_semantic"), ("ppliteseg", "ppliteseg", pplite_case, "prediction_semantic"),
] ]
if mode == "goose": if mode == "goose":
sources.append(("truth", "ddrnet", ddr_case, "truth_semantic")) sources.extend(
(
("truth", "ddrnet", ddr_case, "truth_semantic"),
(
"ddrnet_error",
"ddrnet",
ddr_case,
"vegetation_material_error",
),
(
"ppliteseg_error",
"ppliteseg",
pplite_case,
"vegetation_material_error",
),
)
)
else: else:
selected_case = cases[(selected, mode)][case_id] selected_case = cases[(selected, mode)][case_id]
sources.extend( sources.extend(
@@ -328,7 +381,7 @@ def seal_vegetation_shadow_lab(
}, },
"limitations": [ "limitations": [
"GOOSE validation is external-domain qualification, not RAVNOVES ground truth.", "GOOSE validation is external-domain qualification, not RAVNOVES ground truth.",
"The RAVNOVES island is visual shadow evidence without independent labels.", "The RAVNOVES shadow remains in Worker proofs and is not catalogued as vegetation evidence because it has no independent labels.",
"Vegetation semantics never clears rigid LiDAR/TGS occupancy.", "Vegetation semantics never clears rigid LiDAR/TGS occupancy.",
"Undefined pixels outside the 600x600 center crop remain fail-closed.", "Undefined pixels outside the 600x600 center crop remain fail-closed.",
], ],
@@ -44,6 +44,19 @@ def test_benchmark_contract_is_bounded_and_fail_closed() -> None:
) )
assert config["ravnoves"]["expected_frame_count"] == 4489 assert config["ravnoves"]["expected_frame_count"] == 4489
assert len(config["ravnoves"]["frame_indices"]) == 12 assert len(config["ravnoves"]["frame_indices"]) == 12
assert config["visual_case_contract"]["selection_basis"] == (
"ground-truth-class-support-only"
)
assert config["visual_case_contract"]["case_count"] == 12
assert sum(
row["count"] for row in config["visual_case_contract"]["strata"]
) == 12
assert {row["class_name"] for row in config["visual_case_contract"]["strata"]} >= {
"high_grass",
"low_grass",
"bush",
"tree_trunk",
}
assert config["invariants"] == { assert config["invariants"] == {
"one_heavy_candidate_at_a_time": True, "one_heavy_candidate_at_a_time": True,
"raw_fisheye_is_immutable": True, "raw_fisheye_is_immutable": True,
@@ -68,6 +81,8 @@ def test_runner_uses_exact_visible_pairs_and_never_grants_authority() -> None:
assert '"prewarm_inference_count": len(warmup_latencies_ms)' in source assert '"prewarm_inference_count": len(warmup_latencies_ms)' in source
assert '"prewarm_latency_ms": round(warmup_latencies_ms[0], 4)' in source assert '"prewarm_latency_ms": round(warmup_latencies_ms[0], 4)' in source
assert '"prewarm_latency_ms_last": round(warmup_latencies_ms[-1], 4)' in source assert '"prewarm_latency_ms_last": round(warmup_latencies_ms[-1], 4)' in source
assert "truth_focused_visuals(items, names, visual_contract)" in source
assert 'files["vegetation_material_error"]' in source
def test_worker_wrapper_is_isolated_from_canonical_triton() -> None: def test_worker_wrapper_is_isolated_from_canonical_triton() -> None:
+15 -5
View File
@@ -28,7 +28,7 @@ def _worker_result(root: Path, *, candidate: str, mode: str, vegetation_iou: flo
case_root.mkdir(parents=True) case_root.mkdir(parents=True)
keys = ["source", "prediction_semantic", "policy_urban", "policy_rural", "policy_offroad"] keys = ["source", "prediction_semantic", "policy_urban", "policy_rural", "policy_offroad"]
if mode == "goose": if mode == "goose":
keys.append("truth_semantic") keys.extend(("truth_semantic", "vegetation_material_error"))
files = {} files = {}
for key in keys: for key in keys:
path = case_root / f"{key}.png" path = case_root / f"{key}.png"
@@ -44,6 +44,13 @@ def _worker_result(root: Path, *, candidate: str, mode: str, vegetation_iou: flo
"source_height": 600 if mode == "ravnoves" else 512, "source_height": 600 if mode == "ravnoves" else 512,
"center_crop_xyxy": [100, 0, 700, 600] if mode == "ravnoves" else [0, 0, 512, 512], "center_crop_xyxy": [100, 0, 700, 600] if mode == "ravnoves" else [0, 0, 512, 512],
"outside_crop_state": "undefined" if mode == "ravnoves" else "not-applicable", "outside_crop_state": "undefined" if mode == "ravnoves" else "not-applicable",
"focus": {
"class_name": "high_grass",
"label_id": 51,
"truth_pixels": 16384,
"truth_fraction": 0.0625,
"stratum_rank": index + 1,
} if mode == "goose" else None,
"files": files, "files": files,
} }
) )
@@ -99,9 +106,12 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
assert manifest["ground_truth"] is False assert manifest["ground_truth"] is False
assert manifest["authority"]["commands_enabled"] is False assert manifest["authority"]["commands_enabled"] is False
assert manifest["authority"]["navigation_or_safety_accepted"] is False assert manifest["authority"]["navigation_or_safety_accepted"] is False
assert len(manifest["catalogs"]["ravnoves"]) == 12 assert len(manifest["catalogs"]["ravnoves"]) == 0
assert len(manifest["catalogs"]["goose"]) == 12 assert len(manifest["catalogs"]["goose"]) == 12
assert len(manifest["artifacts"]) == 124 assert len(manifest["artifacts"]) == 76
assert manifest["catalogs"]["goose"][0]["focus"]["class_name"] == "high_grass"
assert "ddrnet_error" in manifest["catalogs"]["goose"][0]["assets"]
assert "ppliteseg_error" in manifest["catalogs"]["goose"][0]["assets"]
assert "all_classes" not in manifest["metrics"]["candidates"]["ddrnet"]["validation_metrics"] assert "all_classes" not in manifest["metrics"]["candidates"]["ddrnet"]["validation_metrics"]
assert (result_root / "result.json").stat().st_size <= 64 * 1024 assert (result_root / "result.json").stat().st_size <= 64 * 1024
@@ -111,7 +121,7 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
) )
proof = verify_laboratory_evidence_result(definition, result_root) proof = verify_laboratory_evidence_result(definition, result_root)
assert proof["result_id"] == result_root.name assert proof["result_id"] == result_root.name
assert proof["artifact_count"] == 124 assert proof["artifact_count"] == 76
app = FastAPI() app = FastAPI()
app.include_router(build_vegetation_shadow_lab_router(root_provider=lambda: result_root.parent)) app.include_router(build_vegetation_shadow_lab_router(root_provider=lambda: result_root.parent))
@@ -119,7 +129,7 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
response = client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}") response = client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["access"] == "read-only" assert response.json()["access"] == "read-only"
asset_path = manifest["catalogs"]["ravnoves"][0]["assets"]["offroad"]["path"] asset_path = manifest["catalogs"]["goose"][0]["assets"]["ddrnet_error"]["path"]
asset = client.get( asset = client.get(
f"/api/v1/laboratory/vegetation-shadow/{result_root.name}/assets/{asset_path}" f"/api/v1/laboratory/vegetation-shadow/{result_root.name}/assets/{asset_path}"
) )