fix(perception): reuse M4.8 for vegetation evidence
This commit is contained in:
@@ -4,11 +4,25 @@ const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const CANDIDATES = ["ddrnet", "ppliteseg"] 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 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 {
|
||||
candidate: VegetationCandidateKey;
|
||||
@@ -33,6 +47,7 @@ export interface VegetationVisualCase {
|
||||
height: number;
|
||||
centerCropXyxy: readonly [number, number, number, number];
|
||||
outsideCropState: "undefined" | "not-applicable";
|
||||
focus: VegetationVisualFocus | null;
|
||||
assets: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
@@ -174,7 +189,7 @@ function visualCaseValue(
|
||||
.join("/")}`;
|
||||
}
|
||||
const expectedAssets = expectedKind === "goose"
|
||||
? VALIDATION_MODES
|
||||
? VALIDATION_ASSETS
|
||||
: ROUTE_MODES;
|
||||
if (expectedAssets.some((key) => !projected[key])) {
|
||||
throw new VegetationShadowContractError(`vegetation.case.assets: ${expectedKind} набор неполон.`);
|
||||
@@ -183,6 +198,23 @@ function visualCaseValue(
|
||||
if (outsideCropState !== "undefined" && outsideCropState !== "not-applicable") {
|
||||
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 {
|
||||
caseId,
|
||||
sourceKind: expectedKind,
|
||||
@@ -190,6 +222,7 @@ function visualCaseValue(
|
||||
height: integerValue(row.height, "vegetation.case.height"),
|
||||
centerCropXyxy: crop as unknown as readonly [number, number, number, number],
|
||||
outsideCropState,
|
||||
focus,
|
||||
assets: projected,
|
||||
};
|
||||
}
|
||||
@@ -233,8 +266,8 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
||||
.map((item) => visualCaseValue(item, resultId, "ravnoves"));
|
||||
const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
|
||||
.map((item) => visualCaseValue(item, resultId, "goose"));
|
||||
if (routeCases.length !== 12 || validationCases.length !== 12) {
|
||||
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 + 12 случаев.");
|
||||
if (routeCases.length !== 0 || validationCases.length !== 12) {
|
||||
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
|
||||
}
|
||||
return {
|
||||
resultId,
|
||||
|
||||
@@ -28,6 +28,119 @@ const ATLAS_MODES = [
|
||||
] as const;
|
||||
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 {
|
||||
return error instanceof Error && error.message.trim() ? error.message : "M4.8 evidence недоступно.";
|
||||
}
|
||||
|
||||
@@ -6,14 +6,45 @@ import {
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
|
||||
import {
|
||||
VegetationRouteVisual,
|
||||
VegetationValidationVisual,
|
||||
} from "./VegetationShadowVisual";
|
||||
M48MaskComparisonVisual,
|
||||
type M48MaskComparisonCase,
|
||||
} from "./M48FailureAtlasVisual";
|
||||
|
||||
function decimal(value: number, digits = 1): string {
|
||||
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({
|
||||
rigLabel,
|
||||
result,
|
||||
@@ -31,21 +62,21 @@ export function VegetationShadowResultView({
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB V1 · растительность и mission-policy пресеты"
|
||||
description="Две готовые fine-64 модели GOOSE проверены на полном validation split и перенесены в автономный визуальный shadow по RAVNOVES00. Интерфейс читает sealed-кадры локально и не зависит от доступности Worker 006."
|
||||
status="Визуальный shadow готов · navigation authority OFF"
|
||||
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 не принят"
|
||||
statusTone="warning"
|
||||
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: "Policy", value: "Urban / rural / off-road · mission-configurable" },
|
||||
{ label: "Authority", value: "SHADOW ONLY · commands OFF · geometry stays authoritative" },
|
||||
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
|
||||
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
|
||||
]}
|
||||
brief={{
|
||||
question: "Можно ли взять готовую сегментацию растительности, увидеть её на нашем маршруте и сразу проверить разные правила миссии?",
|
||||
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime: сначала 962 размеченных GOOSE-кадра, затем 12 детерминированных кадров RAVNOVES00. Для каждого кадра запечатаны source, обе семантики и три policy-проекции.",
|
||||
principalResult: `${selected.loadedModelName} выбран по vegetation IoU ${decimal(selected.vegetationMeanIouPercent, 2)}% при shadow p95 ${decimal(selected.shadowLatencyP95Ms, 2)} ms. Визуальный результат доступен локально без Worker.`,
|
||||
limitation: "RAVNOVES00 не размечен по fine-64, поэтому это перенос и визуальная проверка, а не доказательство точности или безопасности. Камерная семантика не может очищать жёсткую LiDAR/TGS occupancy.",
|
||||
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 и не используется как главное визуальное доказательство.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
@@ -62,29 +93,22 @@ export function VegetationShadowResultView({
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<>
|
||||
<LaboratoryEvidence
|
||||
eyebrow="RAVNOVES00 · AUTONOMOUS VISUAL SHADOW"
|
||||
title="Источник, обе модели и три правила миссии на одинаковых кадрах"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<VegetationRouteVisual result={result} />
|
||||
</LaboratoryEvidence>
|
||||
<LaboratoryEvidence
|
||||
eyebrow="GOOSE · EXTERNAL VALIDATION EVIDENCE"
|
||||
title="Независимая разметка: truth против DDRNet и PPLiteSeg"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<VegetationValidationVisual result={result} />
|
||||
</LaboratoryEvidence>
|
||||
</>
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
|
||||
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<M48MaskComparisonVisual
|
||||
cases={comparisonCases(result)}
|
||||
initialCandidate={result.selectedCandidate}
|
||||
/>
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Готовые веса дают рабочую точку старта, но ещё не право ехать"
|
||||
status={`${selected.loadedModelName} выбран для shadow`}
|
||||
title="DDRNet — стартовые веса; перенос на ровер ещё не доказан"
|
||||
status={`${selected.loadedModelName} выбран только как vegetation candidate`}
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{
|
||||
@@ -98,7 +122,7 @@ export function VegetationShadowResultView({
|
||||
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`,
|
||||
hint: "чистый inference · одна тяжёлая модель за раз",
|
||||
},
|
||||
@@ -108,7 +132,7 @@ export function VegetationShadowResultView({
|
||||
hint: "один явный inference до допуска кадров; исключён из steady-state p95",
|
||||
},
|
||||
{
|
||||
label: "RAVNOVES throughput",
|
||||
label: "Worker throughput",
|
||||
value: `${decimal(selected.shadowThroughputFps, 1)} / ${decimal(alternative.shadowThroughputFps, 1)} FPS`,
|
||||
hint: "изолированный Worker 006 · не realtime graph целиком",
|
||||
},
|
||||
@@ -118,15 +142,15 @@ export function VegetationShadowResultView({
|
||||
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
|
||||
},
|
||||
{
|
||||
label: "Autonomous evidence",
|
||||
value: "12 route + 12 validation",
|
||||
hint: "только текущий кадр загружается в viewer; Worker не требуется",
|
||||
label: "Hard-case evidence",
|
||||
value: "12 truth-backed cases",
|
||||
hint: "8 vegetation strata · Worker для открытия не требуется",
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Обе официальные fine-64 модели запускаются на Worker 006, проходят полный GOOSE validation и дают воспроизводимые растительные маски на 12 фиксированных RAVNOVES00 кадрах. Urban/rural/off-road policy-проекции формируются без повторного inference.",
|
||||
notProved: "Не доказаны accuracy на нашем fisheye-домене, различение тонкой травы от толстого ствола во всех условиях, temporal stability, collision safety и physical-live поведение ровера.",
|
||||
decision: "Сохранить выбранную модель как shadow provider. Следующий критический блок — разметить небольшой hard-case island нашего офф-роуда: трава, папоротник, куст с толстыми стволами и дерево; затем калибровать policy без ослабления LiDAR/TGS fail-closed геометрии.",
|
||||
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 геометрию не ослаблять.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>> = {
|
||||
"lab-v1-vegetation-shadow": {
|
||||
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",
|
||||
experimentName: "GOOSE ready weights → RAVNOVES vegetation policy",
|
||||
variantName: "LAB V1 · DDRNet vs PPLiteSeg · urban/rural/off-road presets",
|
||||
experimentName: "DDRNet vs PPLiteSeg · truth-backed vegetation hard cases",
|
||||
variantName: "LAB V1 · готовые vegetation weights · GOOSE truth",
|
||||
},
|
||||
"m48-object-centric-quality": {
|
||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
@@ -51,7 +52,7 @@ function candidate(candidateKey, vegetationIou) {
|
||||
function visualCase(sourceKind, index) {
|
||||
const caseId = `case-${index}`;
|
||||
const keys = sourceKind === "goose"
|
||||
? ["source", "truth", "ddrnet", "ppliteseg"]
|
||||
? ["source", "truth", "ddrnet", "ppliteseg", "ddrnet_error", "ppliteseg_error"]
|
||||
: ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"];
|
||||
return {
|
||||
case_id: caseId,
|
||||
@@ -60,6 +61,13 @@ function visualCase(sourceKind, index) {
|
||||
height: sourceKind === "goose" ? 512 : 600,
|
||||
center_crop_xyxy: sourceKind === "goose" ? [0, 0, 512, 512] : [100, 0, 700, 600],
|
||||
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, {
|
||||
path: `visual/${sourceKind}/${caseId}/${key}.png`,
|
||||
sha256: "d".repeat(64),
|
||||
@@ -101,7 +109,7 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
|
||||
},
|
||||
catalogs: {
|
||||
goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)),
|
||||
ravnoves: Array.from({ length: 12 }, (_, index) => visualCase("ravnoves", index)),
|
||||
ravnoves: [],
|
||||
},
|
||||
access: "read-only",
|
||||
}), { 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.candidates[0].vegetationMeanIouPercent, 64);
|
||||
assert.equal(result.routeCases.length, 12);
|
||||
assert.equal(result.routeCases.length, 0);
|
||||
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, {
|
||||
commandsEnabled: false,
|
||||
navigationOrSafetyAccepted: false,
|
||||
@@ -123,3 +132,17 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
|
||||
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" },
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user