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 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" },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
+126
-5
@@ -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",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user