feat(perception): split vegetation evidence layers
This commit is contained in:
@@ -48,9 +48,13 @@ import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
|||||||
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
||||||
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
|
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
|
||||||
import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow";
|
import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow";
|
||||||
import { fetchVegetationShadowResult } from "./vegetationShadow";
|
import {
|
||||||
|
fetchVegetationBenchmarkResult,
|
||||||
|
fetchVegetationShadowResult,
|
||||||
|
} from "./vegetationShadow";
|
||||||
|
|
||||||
export type AdvancedLaboratoryWorkId =
|
export type AdvancedLaboratoryWorkId =
|
||||||
|
| "lab-v1-vegetation-benchmark"
|
||||||
| "lab-v1-vegetation-shadow"
|
| "lab-v1-vegetation-shadow"
|
||||||
| "m48-object-centric-quality"
|
| "m48-object-centric-quality"
|
||||||
| "m48-small-static-passage-regression"
|
| "m48-small-static-passage-regression"
|
||||||
@@ -102,6 +106,7 @@ export interface AdvancedLaboratoryIndexItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||||
|
"lab-v1-vegetation-benchmark",
|
||||||
"lab-v1-vegetation-shadow",
|
"lab-v1-vegetation-shadow",
|
||||||
"m48-object-centric-quality",
|
"m48-object-centric-quality",
|
||||||
"m48-small-static-passage-regression",
|
"m48-small-static-passage-regression",
|
||||||
@@ -148,6 +153,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
||||||
|
"lab-v1-vegetation-benchmark": "lab-v1-vegetation-benchmark",
|
||||||
"lab-v1-vegetation-shadow": "lab-v1-vegetation-shadow",
|
"lab-v1-vegetation-shadow": "lab-v1-vegetation-shadow",
|
||||||
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
|
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
|
||||||
"m48-small-static-passage-regression": "m48-small-static-passage-regression",
|
"m48-small-static-passage-regression": "m48-small-static-passage-regression",
|
||||||
@@ -201,6 +207,7 @@ export function isAdvancedLaboratoryWorkId(
|
|||||||
|
|
||||||
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
||||||
return {
|
return {
|
||||||
|
vegetationBenchmark: null,
|
||||||
vegetationShadow: null,
|
vegetationShadow: null,
|
||||||
m47Graph: null,
|
m47Graph: null,
|
||||||
m48: null,
|
m48: null,
|
||||||
@@ -335,7 +342,8 @@ export function advancedLaboratoryResultAvailable(
|
|||||||
workId: AdvancedLaboratoryWorkId,
|
workId: AdvancedLaboratoryWorkId,
|
||||||
results: AdvancedLaboratoryResults,
|
results: AdvancedLaboratoryResults,
|
||||||
): boolean {
|
): boolean {
|
||||||
return workId === "lab-v1-vegetation-shadow" ? results.vegetationShadow !== null
|
return workId === "lab-v1-vegetation-benchmark" ? results.vegetationBenchmark !== null
|
||||||
|
: workId === "lab-v1-vegetation-shadow" ? results.vegetationShadow !== null
|
||||||
: workId === "m48-object-centric-quality" ? results.m48 !== null
|
: workId === "m48-object-centric-quality" ? results.m48 !== null
|
||||||
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
|
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
|
||||||
: workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null
|
: workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null
|
||||||
@@ -393,7 +401,10 @@ export async function fetchAdvancedLaboratoryResult(
|
|||||||
} = {},
|
} = {},
|
||||||
): Promise<AdvancedLaboratoryResults> {
|
): Promise<AdvancedLaboratoryResults> {
|
||||||
const results = emptyAdvancedLaboratoryResults();
|
const results = emptyAdvancedLaboratoryResults();
|
||||||
if (workId === "lab-v1-vegetation-shadow") {
|
if (workId === "lab-v1-vegetation-benchmark") {
|
||||||
|
if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation benchmark identity не выбрана.");
|
||||||
|
results.vegetationBenchmark = await fetchVegetationBenchmarkResult(resultId, { fetcher, signal });
|
||||||
|
} else if (workId === "lab-v1-vegetation-shadow") {
|
||||||
if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation LAB identity не выбрана.");
|
if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation LAB identity не выбрана.");
|
||||||
results.vegetationShadow = await fetchVegetationShadowResult(resultId, { fetcher, signal });
|
results.vegetationShadow = await fetchVegetationShadowResult(resultId, { fetcher, signal });
|
||||||
} else if (workId === "m48-object-centric-quality") {
|
} else if (workId === "m48-object-centric-quality") {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import type { M49TgsFullShadowResult } from "./m49TgsFullShadow";
|
|||||||
import type { VegetationShadowResult } from "./vegetationShadow";
|
import type { VegetationShadowResult } from "./vegetationShadow";
|
||||||
|
|
||||||
export interface AdvancedLaboratoryResults {
|
export interface AdvancedLaboratoryResults {
|
||||||
|
vegetationBenchmark: VegetationShadowResult | null;
|
||||||
vegetationShadow: VegetationShadowResult | null;
|
vegetationShadow: VegetationShadowResult | null;
|
||||||
m47Graph: M47ReferenceGraphLabResult | null;
|
m47Graph: M47ReferenceGraphLabResult | null;
|
||||||
m48: M48AdvancedResult | null;
|
m48: M48AdvancedResult | null;
|
||||||
|
|||||||
@@ -967,6 +967,7 @@ export async function fetchAdvancedLaboratoryResults({
|
|||||||
const e39 = settledCatalogValue(settled[7]);
|
const e39 = settledCatalogValue(settled[7]);
|
||||||
const e40 = settledCatalogValue(settled[8]);
|
const e40 = settledCatalogValue(settled[8]);
|
||||||
return {
|
return {
|
||||||
|
vegetationBenchmark: null,
|
||||||
vegetationShadow: null,
|
vegetationShadow: null,
|
||||||
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
||||||
m48r3StaticOccupancy: null,
|
m48r3StaticOccupancy: null,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LaboratoryFetch } from "./advancedResults";
|
import type { LaboratoryFetch } from "./advancedResults";
|
||||||
|
|
||||||
const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
|
const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
|
||||||
|
const BENCHMARK_RESULT_ID = /^lab-v1-vegetation-benchmark-[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;
|
||||||
@@ -75,6 +76,7 @@ export interface VegetationRouteVideo {
|
|||||||
aggregatePredictionPixels: readonly number[];
|
aggregatePredictionPixels: readonly number[];
|
||||||
policyPresets: Readonly<Record<string, Readonly<Record<string, string>>>> | null;
|
policyPresets: Readonly<Record<string, Readonly<Record<string, string>>>> | null;
|
||||||
fusionMode: "synchronised-multilayer-review" | null;
|
fusionMode: "synchronised-multilayer-review" | null;
|
||||||
|
validFovMaskSha256: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VegetationShadowResult {
|
export interface VegetationShadowResult {
|
||||||
@@ -192,6 +194,7 @@ function visualCaseValue(
|
|||||||
value: unknown,
|
value: unknown,
|
||||||
resultId: string,
|
resultId: string,
|
||||||
expectedKind: "goose" | "ravnoves",
|
expectedKind: "goose" | "ravnoves",
|
||||||
|
endpointRoot: string,
|
||||||
): VegetationVisualCase {
|
): VegetationVisualCase {
|
||||||
const row = objectValue(value, `vegetation.${expectedKind}.case`);
|
const row = objectValue(value, `vegetation.${expectedKind}.case`);
|
||||||
exact(row.source_kind, expectedKind, "vegetation.case.source_kind");
|
exact(row.source_kind, expectedKind, "vegetation.case.source_kind");
|
||||||
@@ -210,7 +213,7 @@ function visualCaseValue(
|
|||||||
if (!SHA256.test(sha256) || !path.startsWith(`visual/${expectedKind}/${caseId}/`)) {
|
if (!SHA256.test(sha256) || !path.startsWith(`visual/${expectedKind}/${caseId}/`)) {
|
||||||
throw new VegetationShadowContractError(`vegetation.case.assets.${key}: proof invalid.`);
|
throw new VegetationShadowContractError(`vegetation.case.assets.${key}: proof invalid.`);
|
||||||
}
|
}
|
||||||
projected[key] = `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/assets/${path
|
projected[key] = `${endpointRoot}/${encodeURIComponent(resultId)}/assets/${path
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(encodeURIComponent)
|
.map(encodeURIComponent)
|
||||||
.join("/")}`;
|
.join("/")}`;
|
||||||
@@ -342,10 +345,16 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
|
|||||||
evidenceState,
|
evidenceState,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const expectedClassCount = viewKind === "coarse-material-policy-review" ? 9 : 64;
|
const expectedClassCount = viewKind === "coarse-material-policy-review" ? 10 : 64;
|
||||||
if (classes.length !== expectedClassCount) {
|
if (classes.length !== expectedClassCount) {
|
||||||
throw new VegetationShadowContractError("vegetation.route_video: taxonomy size changed.");
|
throw new VegetationShadowContractError("vegetation.route_video: taxonomy size changed.");
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
viewKind === "coarse-material-policy-review"
|
||||||
|
&& (classes[9]?.disposition !== "undefined" || classes[9]?.evidenceState !== "UNOBSERVED")
|
||||||
|
) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: valid-FOV class changed.");
|
||||||
|
}
|
||||||
const aggregatePredictionPixels = arrayValue(
|
const aggregatePredictionPixels = arrayValue(
|
||||||
row.aggregate_prediction_pixels,
|
row.aggregate_prediction_pixels,
|
||||||
"vegetation.route_video.aggregate_prediction_pixels",
|
"vegetation.route_video.aggregate_prediction_pixels",
|
||||||
@@ -364,7 +373,18 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
|
|||||||
integerValue(maskArchive.byte_length, "vegetation.route_video.mask_archive.byte_length");
|
integerValue(maskArchive.byte_length, "vegetation.route_video.mask_archive.byte_length");
|
||||||
let policyPresets: VegetationRouteVideo["policyPresets"] = null;
|
let policyPresets: VegetationRouteVideo["policyPresets"] = null;
|
||||||
let fusionMode: VegetationRouteVideo["fusionMode"] = null;
|
let fusionMode: VegetationRouteVideo["fusionMode"] = null;
|
||||||
|
let validFovMaskSha256: string | null = null;
|
||||||
if (viewKind === "coarse-material-policy-review") {
|
if (viewKind === "coarse-material-policy-review") {
|
||||||
|
const validFov = objectValue(row.valid_fov, "vegetation.route_video.valid_fov");
|
||||||
|
exact(validFov.mask_path, "video/valid-fov-mask.png", "vegetation.route_video.valid_fov.path");
|
||||||
|
validFovMaskSha256 = textValue(
|
||||||
|
validFov.mask_sha256,
|
||||||
|
"vegetation.route_video.valid_fov.sha256",
|
||||||
|
);
|
||||||
|
if (!SHA256.test(validFovMaskSha256)) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: valid-FOV digest invalid.");
|
||||||
|
}
|
||||||
|
exact(validFov.outside_valid_fov_class_id, 9, "vegetation.route_video.valid_fov.class_id");
|
||||||
const policy = objectValue(row.policy, "vegetation.route_video.policy");
|
const policy = objectValue(row.policy, "vegetation.route_video.policy");
|
||||||
const presets = objectValue(policy.presets, "vegetation.route_video.policy.presets");
|
const presets = objectValue(policy.presets, "vegetation.route_video.policy.presets");
|
||||||
policyPresets = Object.fromEntries(Object.entries(presets).map(([presetId, rawRules]) => {
|
policyPresets = Object.fromEntries(Object.entries(presets).map(([presetId, rawRules]) => {
|
||||||
@@ -399,10 +419,15 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null {
|
|||||||
aggregatePredictionPixels,
|
aggregatePredictionPixels,
|
||||||
policyPresets,
|
policyPresets,
|
||||||
fusionMode,
|
fusionMode,
|
||||||
|
validFovMaskSha256,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
function parseResult(
|
||||||
|
value: unknown,
|
||||||
|
resultId: string,
|
||||||
|
endpointRoot: string,
|
||||||
|
): VegetationShadowResult {
|
||||||
const payload = objectValue(value, "Vegetation LAB");
|
const payload = objectValue(value, "Vegetation LAB");
|
||||||
exact(payload.schema_version, "missioncore.lab-v1-vegetation-shadow/v1", "vegetation.schema");
|
exact(payload.schema_version, "missioncore.lab-v1-vegetation-shadow/v1", "vegetation.schema");
|
||||||
exact(payload.result_id, resultId, "vegetation.result_id");
|
exact(payload.result_id, resultId, "vegetation.result_id");
|
||||||
@@ -438,9 +463,9 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
|||||||
"vegetation.authority.camera_semantics_can_clear_rigid_geometry",
|
"vegetation.authority.camera_semantics_can_clear_rigid_geometry",
|
||||||
);
|
);
|
||||||
const routeCases = arrayValue(catalogs.ravnoves, "vegetation.catalogs.ravnoves")
|
const routeCases = arrayValue(catalogs.ravnoves, "vegetation.catalogs.ravnoves")
|
||||||
.map((item) => visualCaseValue(item, resultId, "ravnoves"));
|
.map((item) => visualCaseValue(item, resultId, "ravnoves", endpointRoot));
|
||||||
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", endpointRoot));
|
||||||
if (routeCases.length !== 0 || validationCases.length !== 12) {
|
if (routeCases.length !== 0 || validationCases.length !== 12) {
|
||||||
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
|
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
|
||||||
}
|
}
|
||||||
@@ -490,5 +515,36 @@ export async function fetchVegetationShadowResult(
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`);
|
throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`);
|
||||||
}
|
}
|
||||||
return parseResult(await response.json(), resultId);
|
return parseResult(
|
||||||
|
await response.json(),
|
||||||
|
resultId,
|
||||||
|
"/api/v1/laboratory/vegetation-shadow",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchVegetationBenchmarkResult(
|
||||||
|
resultId: string,
|
||||||
|
{
|
||||||
|
fetcher = fetch,
|
||||||
|
signal,
|
||||||
|
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
|
): Promise<VegetationShadowResult> {
|
||||||
|
if (!BENCHMARK_RESULT_ID.test(resultId)) {
|
||||||
|
throw new VegetationShadowContractError("Vegetation benchmark identity недопустима.");
|
||||||
|
}
|
||||||
|
const endpointRoot = "/api/v1/laboratory/vegetation-benchmark";
|
||||||
|
const response = await fetcher(
|
||||||
|
`${endpointRoot}/${encodeURIComponent(resultId)}`,
|
||||||
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new VegetationShadowContractError(
|
||||||
|
`Vegetation benchmark недоступен: HTTP ${response.status}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = parseResult(await response.json(), resultId, endpointRoot);
|
||||||
|
if (result.routeVideo) {
|
||||||
|
throw new VegetationShadowContractError("Vegetation benchmark содержит route video.");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,21 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"] {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"]
|
||||||
|
> .m4-replay-threat-visual__pane-layer-controls {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"]
|
||||||
|
> .m4-replay-threat-visual__pane-mode-controls {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]:has(
|
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]:has(
|
||||||
.m4-replay-threat-visual__review-controls
|
.m4-replay-threat-visual__review-controls
|
||||||
) .m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] {
|
) .m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
|||||||
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
|
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
|
||||||
import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult";
|
import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult";
|
||||||
import { VegetationShadowResultView } from "./VegetationShadowResult";
|
import { VegetationShadowResultView } from "./VegetationShadowResult";
|
||||||
|
import { VegetationBenchmarkResultView } from "./VegetationBenchmarkResult";
|
||||||
|
|
||||||
export { isAdvancedLaboratoryWorkId };
|
export { isAdvancedLaboratoryWorkId };
|
||||||
export type { AdvancedLaboratoryWorkId };
|
export type { AdvancedLaboratoryWorkId };
|
||||||
@@ -93,6 +94,9 @@ export function AdvancedLaboratoryResult({
|
|||||||
failedSessionId: string | null;
|
failedSessionId: string | null;
|
||||||
replayError: string | null;
|
replayError: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
if (workId === "lab-v1-vegetation-benchmark" && results.vegetationBenchmark) {
|
||||||
|
return <VegetationBenchmarkResultView rigLabel={rigLabel} result={results.vegetationBenchmark} />;
|
||||||
|
}
|
||||||
if (workId === "lab-v1-vegetation-shadow" && results.vegetationShadow) {
|
if (workId === "lab-v1-vegetation-shadow" && results.vegetationShadow) {
|
||||||
return <VegetationShadowResultView rigLabel={rigLabel} result={results.vegetationShadow} />;
|
return <VegetationShadowResultView rigLabel={rigLabel} result={results.vegetationShadow} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export function M49TgsFullShadowEvidence({
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
setSemantic(null);
|
setSemantic(null);
|
||||||
setSemanticError(null);
|
setSemanticError(null);
|
||||||
if (semanticOverride) return () => controller.abort();
|
|
||||||
void fetchE47SemanticSlamResult({
|
void fetchE47SemanticSlamResult({
|
||||||
resultId: result.source.linkedSemanticResultId,
|
resultId: result.source.linkedSemanticResultId,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@@ -87,7 +86,7 @@ export function M49TgsFullShadowEvidence({
|
|||||||
if (!controller.signal.aborted) setSemanticError(message(caught));
|
if (!controller.signal.aborted) setSemanticError(message(caught));
|
||||||
});
|
});
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId, semanticOverride]);
|
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -201,15 +200,28 @@ export function M49TgsFullShadowEvidence({
|
|||||||
const handleSequenceChange = useCallback((sequence: number | null) => {
|
const handleSequenceChange = useCallback((sequence: number | null) => {
|
||||||
setActiveSequence(sequence);
|
setActiveSequence(sequence);
|
||||||
}, []);
|
}, []);
|
||||||
|
const semanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(() => [
|
||||||
|
...(semantic ? [{
|
||||||
|
id: "urban",
|
||||||
|
controlLabel: "ГОРОД · EoMT",
|
||||||
|
resultId: semantic.resultId,
|
||||||
|
taxonomy: semantic.taxonomy,
|
||||||
|
label: "EoMT Cityscapes semantic · recorded video",
|
||||||
|
maskAriaLabel: "EoMT urban semantic prediction",
|
||||||
|
}] : []),
|
||||||
|
...(semanticOverride ? [{
|
||||||
|
...semanticOverride,
|
||||||
|
id: semanticOverride.id ?? "vegetation",
|
||||||
|
controlLabel: semanticOverride.controlLabel ?? "ПРИРОДА · DDRNet",
|
||||||
|
}] : []),
|
||||||
|
], [semantic, semanticOverride]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<M4ReplayThreatVisual
|
<M4ReplayThreatVisual
|
||||||
resultId={result.source.linkedVisualResultId}
|
resultId={result.source.linkedVisualResultId}
|
||||||
semantic={semanticOverride ?? (semantic ? {
|
semanticLayers={semanticLayers}
|
||||||
resultId: semantic.resultId,
|
initialSemanticLayerId={semanticOverride ? "vegetation" : "urban"}
|
||||||
taxonomy: semantic.taxonomy,
|
|
||||||
} : undefined)}
|
|
||||||
showReviewAnchorBoxes={false}
|
showReviewAnchorBoxes={false}
|
||||||
reviewLabel="4 489 source-paced TGS frames"
|
reviewLabel="4 489 source-paced TGS frames"
|
||||||
evidenceLabel={evidenceLabel}
|
evidenceLabel={evidenceLabel}
|
||||||
@@ -227,7 +239,7 @@ export function M49TgsFullShadowEvidence({
|
|||||||
replacePointCloud: false,
|
replacePointCloud: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!semanticOverride && semanticError ? (
|
{semanticError ? (
|
||||||
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
||||||
Semantic overlay недоступен: {semanticError}
|
Semantic overlay недоступен: {semanticError}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ function SpatialState({ message: text }: { message: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface M4ReplayThreatSemanticLayer {
|
export interface M4ReplayThreatSemanticLayer {
|
||||||
|
id?: string;
|
||||||
|
controlLabel?: string;
|
||||||
resultId: string;
|
resultId: string;
|
||||||
spatialResultId?: string | null;
|
spatialResultId?: string | null;
|
||||||
maskUrl?: (sequence: number) => string;
|
maskUrl?: (sequence: number) => string;
|
||||||
@@ -155,6 +157,8 @@ const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
|||||||
export function M4ReplayThreatVisual({
|
export function M4ReplayThreatVisual({
|
||||||
resultId,
|
resultId,
|
||||||
semantic,
|
semantic,
|
||||||
|
semanticLayers,
|
||||||
|
initialSemanticLayerId,
|
||||||
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
||||||
showReviewAnchorBoxes = true,
|
showReviewAnchorBoxes = true,
|
||||||
reviewLabel = "Контрольные примеры M4.8R1",
|
reviewLabel = "Контрольные примеры M4.8R1",
|
||||||
@@ -168,6 +172,8 @@ export function M4ReplayThreatVisual({
|
|||||||
}: {
|
}: {
|
||||||
resultId: string;
|
resultId: string;
|
||||||
semantic?: M4ReplayThreatSemanticLayer;
|
semantic?: M4ReplayThreatSemanticLayer;
|
||||||
|
semanticLayers?: readonly M4ReplayThreatSemanticLayer[];
|
||||||
|
initialSemanticLayerId?: string;
|
||||||
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
||||||
showReviewAnchorBoxes?: boolean;
|
showReviewAnchorBoxes?: boolean;
|
||||||
reviewLabel?: string;
|
reviewLabel?: string;
|
||||||
@@ -199,6 +205,35 @@ export function M4ReplayThreatVisual({
|
|||||||
));
|
));
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
|
const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0);
|
||||||
|
const availableSemanticLayers = useMemo<readonly M4ReplayThreatSemanticLayer[]>(
|
||||||
|
() => semanticLayers?.length ? semanticLayers : semantic ? [semantic] : [],
|
||||||
|
[semantic, semanticLayers],
|
||||||
|
);
|
||||||
|
const semanticLayerIdentity = availableSemanticLayers
|
||||||
|
.map((layer, index) => layer.id ?? `${layer.resultId}:${index}`)
|
||||||
|
.join("|");
|
||||||
|
const [selectedSemanticLayerId, setSelectedSemanticLayerId] = useState(
|
||||||
|
initialSemanticLayerId ?? "",
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!availableSemanticLayers.length) {
|
||||||
|
setSelectedSemanticLayerId("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedStillExists = availableSemanticLayers.some(
|
||||||
|
(layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId,
|
||||||
|
);
|
||||||
|
if (selectedStillExists) return;
|
||||||
|
const preferred = initialSemanticLayerId
|
||||||
|
? availableSemanticLayers.find((layer) => layer.id === initialSemanticLayerId)
|
||||||
|
: null;
|
||||||
|
const next = preferred ?? availableSemanticLayers[0]!;
|
||||||
|
const nextIndex = availableSemanticLayers.indexOf(next);
|
||||||
|
setSelectedSemanticLayerId(next.id ?? `${next.resultId}:${nextIndex}`);
|
||||||
|
}, [availableSemanticLayers, initialSemanticLayerId, semanticLayerIdentity, selectedSemanticLayerId]);
|
||||||
|
const activeSemantic = availableSemanticLayers.find(
|
||||||
|
(layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId,
|
||||||
|
) ?? availableSemanticLayers[0];
|
||||||
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
const metricSceneRef = useRef<LaboratoryMetricEvidenceSceneHandle | null>(null);
|
||||||
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
|
const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot);
|
||||||
const playbackRange = useMemo(() => metadata.timeline ? ({
|
const playbackRange = useMemo(() => metadata.timeline ? ({
|
||||||
@@ -298,19 +333,21 @@ export function M4ReplayThreatVisual({
|
|||||||
sequence: frame?.sequence ?? null,
|
sequence: frame?.sequence ?? null,
|
||||||
endpointRoot: timelineEndpointRoot,
|
endpointRoot: timelineEndpointRoot,
|
||||||
});
|
});
|
||||||
const semanticSpatialResultId = semantic
|
const semanticSpatialResultId = activeSemantic
|
||||||
? semantic.spatialResultId === undefined ? semantic.resultId : semantic.spatialResultId
|
? activeSemantic.spatialResultId === undefined
|
||||||
|
? activeSemantic.resultId
|
||||||
|
: activeSemantic.spatialResultId
|
||||||
: null;
|
: null;
|
||||||
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
|
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
|
||||||
() => semanticSpatialResultId && semantic
|
() => semanticSpatialResultId && activeSemantic
|
||||||
? semantic.taxonomy.map((item) => ({
|
? activeSemantic.taxonomy.map((item) => ({
|
||||||
classId: item.classId,
|
classId: item.classId,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
|
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
|
||||||
colorRgb: item.colorRgb,
|
colorRgb: item.colorRgb,
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
[semantic, semanticSpatialResultId],
|
[activeSemantic, semanticSpatialResultId],
|
||||||
);
|
);
|
||||||
const semanticTimeline = useE47SemanticTimelineFrame({
|
const semanticTimeline = useE47SemanticTimelineFrame({
|
||||||
resultId: semanticSpatialResultId,
|
resultId: semanticSpatialResultId,
|
||||||
@@ -386,14 +423,14 @@ export function M4ReplayThreatVisual({
|
|||||||
[frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes],
|
[frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes],
|
||||||
);
|
);
|
||||||
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||||
() => semantic?.taxonomy.map((item) => ({
|
() => activeSemantic?.taxonomy.map((item) => ({
|
||||||
id: item.classId,
|
id: item.classId,
|
||||||
label: `semantic: ${item.label}`,
|
label: `semantic: ${item.label}`,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
[semantic?.taxonomy],
|
[activeSemantic?.taxonomy],
|
||||||
);
|
);
|
||||||
const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
|
const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
|
||||||
() => semantic?.taxonomy.map((item) => ({
|
() => activeSemantic?.taxonomy.map((item) => ({
|
||||||
classId: item.classId,
|
classId: item.classId,
|
||||||
color: item.disposition === "undefined"
|
color: item.disposition === "undefined"
|
||||||
? { kind: "transparent" as const }
|
? { kind: "transparent" as const }
|
||||||
@@ -404,7 +441,7 @@ export function M4ReplayThreatVisual({
|
|||||||
? 0
|
? 0
|
||||||
: item.disposition === "ambiguous" ? 0.52 : 0.92,
|
: item.disposition === "ambiguous" ? 0.52 : 0.92,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
[semantic?.taxonomy],
|
[activeSemantic?.taxonomy],
|
||||||
);
|
);
|
||||||
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
|
const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence
|
||||||
? semanticTimeline.activeFrame
|
? semanticTimeline.activeFrame
|
||||||
@@ -422,7 +459,7 @@ export function M4ReplayThreatVisual({
|
|||||||
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
|
&& lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence
|
||||||
? lastSpatialSemanticFrameRef.current.frame
|
? lastSpatialSemanticFrameRef.current.frame
|
||||||
: null;
|
: null;
|
||||||
const semanticIntegrityError = semantic && spatialFrame && spatialSemanticFrame && (
|
const semanticIntegrityError = activeSemantic && spatialFrame && spatialSemanticFrame && (
|
||||||
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|
spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount
|
||||||
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|
|| spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount
|
||||||
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
|
|| spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount
|
||||||
@@ -431,7 +468,7 @@ export function M4ReplayThreatVisual({
|
|||||||
: null;
|
: null;
|
||||||
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
|
const alignedSemanticPointIds = useMemo<readonly (number | null)[] | undefined>(() => {
|
||||||
if (
|
if (
|
||||||
!semantic
|
!activeSemantic
|
||||||
|| !showSpatialSemantic
|
|| !showSpatialSemantic
|
||||||
|| !spatialFrame
|
|| !spatialFrame
|
||||||
|| !spatialSemanticFrame
|
|| !spatialSemanticFrame
|
||||||
@@ -441,7 +478,7 @@ export function M4ReplayThreatVisual({
|
|||||||
const status = spatialSemanticFrame.statusCodes[index];
|
const status = spatialSemanticFrame.statusCodes[index];
|
||||||
return status === 2 || status === 3 ? classId : null;
|
return status === 2 || status === 3 ? classId : null;
|
||||||
});
|
});
|
||||||
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
}, [activeSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
||||||
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
|
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
|
||||||
? spatialFrame
|
? spatialFrame
|
||||||
: null;
|
: null;
|
||||||
@@ -610,19 +647,19 @@ export function M4ReplayThreatVisual({
|
|||||||
},
|
},
|
||||||
), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]);
|
), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]);
|
||||||
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
|
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
|
||||||
semantic && showMediaSemantic && frame
|
activeSemantic && showMediaSemantic && frame
|
||||||
? {
|
? {
|
||||||
src: semantic.maskUrl?.(frame.sequence)
|
src: activeSemantic.maskUrl?.(frame.sequence)
|
||||||
?? e47SemanticMaskUrl(semantic.resultId, frame.sequence),
|
?? e47SemanticMaskUrl(activeSemantic.resultId, frame.sequence),
|
||||||
prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1)
|
prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1)
|
||||||
.map((offset) => frame.sequence + offset)
|
.map((offset) => frame.sequence + offset)
|
||||||
.filter((sequence) => sequence < (metadata.timeline?.frameCount ?? 0))
|
.filter((sequence) => sequence < (metadata.timeline?.frameCount ?? 0))
|
||||||
.map((sequence) => semantic.maskUrl?.(sequence)
|
.map((sequence) => activeSemantic.maskUrl?.(sequence)
|
||||||
?? e47SemanticMaskUrl(semantic.resultId, sequence)),
|
?? e47SemanticMaskUrl(activeSemantic.resultId, sequence)),
|
||||||
classes: semanticClasses,
|
classes: semanticClasses,
|
||||||
palette: semanticPalette,
|
palette: semanticPalette,
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
ariaLabel: `${semantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`,
|
ariaLabel: `${activeSemantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence
|
const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence
|
||||||
@@ -692,7 +729,7 @@ export function M4ReplayThreatVisual({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const mediaLayerControls = semantic
|
const mediaLayerControls = activeSemantic
|
||||||
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
|
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
|
||||||
|| (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
|
|| (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
|
||||||
<div
|
<div
|
||||||
@@ -700,7 +737,7 @@ export function M4ReplayThreatVisual({
|
|||||||
role="group"
|
role="group"
|
||||||
aria-label="Слои камеры и видео"
|
aria-label="Слои камеры и видео"
|
||||||
>
|
>
|
||||||
{semantic ? (
|
{activeSemantic ? (
|
||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
shape="pill"
|
shape="pill"
|
||||||
@@ -711,6 +748,20 @@ export function M4ReplayThreatVisual({
|
|||||||
SEMANTICS
|
SEMANTICS
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{availableSemanticLayers.length > 1 ? (
|
||||||
|
<SegmentedControl
|
||||||
|
value={selectedSemanticLayerId}
|
||||||
|
items={availableSemanticLayers.map((layer, index) => ({
|
||||||
|
value: layer.id ?? `${layer.resultId}:${index}`,
|
||||||
|
label: layer.controlLabel ?? layer.label ?? `SEMANTIC ${index + 1}`,
|
||||||
|
}))}
|
||||||
|
label="Источник семантики"
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedSemanticLayerId(value);
|
||||||
|
setShowMediaSemantic(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery ? (
|
{showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery ? (
|
||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
@@ -1022,6 +1073,7 @@ export function M4ReplayThreatVisual({
|
|||||||
<div
|
<div
|
||||||
className="m4-replay-threat-visual__pane-toolbar"
|
className="m4-replay-threat-visual__pane-toolbar"
|
||||||
data-pane-toolbar="media"
|
data-pane-toolbar="media"
|
||||||
|
data-multi-semantic={availableSemanticLayers.length > 1 ? "true" : undefined}
|
||||||
>
|
>
|
||||||
{mediaLayerControls}
|
{mediaLayerControls}
|
||||||
{mediaModeControls}
|
{mediaModeControls}
|
||||||
@@ -1238,8 +1290,8 @@ export function M4ReplayThreatVisual({
|
|||||||
return (
|
return (
|
||||||
<div className="l3-visual-audit m4-replay-threat-visual">
|
<div className="l3-visual-audit m4-replay-threat-visual">
|
||||||
<LaboratoryEvidenceViewer
|
<LaboratoryEvidenceViewer
|
||||||
label={semantic
|
label={activeSemantic
|
||||||
? semantic.label ?? "Semantic diagnostic replay"
|
? activeSemantic.label ?? "Semantic diagnostic replay"
|
||||||
: `${evidenceLabel} recorded-realtime replay`}
|
: `${evidenceLabel} recorded-realtime replay`}
|
||||||
className="m4-replay-threat-evidence-viewer"
|
className="m4-replay-threat-evidence-viewer"
|
||||||
mode={mediaMode ?? "none"}
|
mode={mediaMode ?? "none"}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import {
|
||||||
|
LaboratoryEvidence,
|
||||||
|
LaboratoryResultSummary,
|
||||||
|
LaboratorySummary,
|
||||||
|
LaboratoryWorkTemplate,
|
||||||
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
|
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
|
||||||
|
import {
|
||||||
|
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 VegetationBenchmarkResultView({
|
||||||
|
rigLabel,
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
rigLabel: string;
|
||||||
|
result: VegetationShadowResult;
|
||||||
|
}) {
|
||||||
|
const selected = result.candidates.find(
|
||||||
|
(candidate) => candidate.candidate === result.selectedCandidate,
|
||||||
|
)!;
|
||||||
|
const alternative = result.candidates.find(
|
||||||
|
(candidate) => candidate.candidate !== result.selectedCandidate,
|
||||||
|
)!;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LaboratoryWorkTemplate
|
||||||
|
summary={(
|
||||||
|
<LaboratorySummary
|
||||||
|
title="M4.8 · архивный benchmark растительности"
|
||||||
|
description="Отдельный truth-backed контур GOOSE для сравнения готовых fine-64 весов. Он не является частью RAVNOVES00 realtime LAB и открывается автономно без Worker 006."
|
||||||
|
status="ARCHIVE ANALYSIS · model qualification only · commands OFF"
|
||||||
|
statusTone="warning"
|
||||||
|
facts={[
|
||||||
|
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 hard cases" },
|
||||||
|
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
|
||||||
|
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
|
||||||
|
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
|
||||||
|
]}
|
||||||
|
brief={{
|
||||||
|
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
|
||||||
|
approach: "Обе модели прогнаны на 962 кадрах, а 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов. Viewer показывает source, ручной truth, prediction и error.",
|
||||||
|
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%.`,
|
||||||
|
limitation: "GOOSE — внешний размеченный домен. Результат выбирает стартовые веса, но не доказывает качество на fisheye RAVNOVES00 и не даёт navigation authority.",
|
||||||
|
}}
|
||||||
|
method={{
|
||||||
|
completeness: "complete",
|
||||||
|
executionClass: "ai-inference",
|
||||||
|
pipelineId: "goose-fine64-ready-weights-benchmark-archive/v1",
|
||||||
|
components: result.candidates.map((candidate) => ({
|
||||||
|
kind: "model" as const,
|
||||||
|
name: candidate.loadedModelName,
|
||||||
|
version: candidate.candidate,
|
||||||
|
role: candidate.candidate === result.selectedCandidate
|
||||||
|
? "selected vegetation candidate"
|
||||||
|
: "comparison candidate",
|
||||||
|
identitySha256: candidate.checkpointSha256,
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
evidence={(
|
||||||
|
<LaboratoryEvidence
|
||||||
|
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
|
||||||
|
title="TRUTH — ручная разметка · PREDICTION — ответ модели · ERROR — расхождение"
|
||||||
|
kind="diagnostic-model"
|
||||||
|
resizable
|
||||||
|
>
|
||||||
|
<M48MaskComparisonVisual
|
||||||
|
cases={comparisonCases(result)}
|
||||||
|
initialCandidate={result.selectedCandidate}
|
||||||
|
/>
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
|
<LaboratoryResultSummary
|
||||||
|
title="DDRNet выбран как стартовый vegetation candidate"
|
||||||
|
status={`${selected.loadedModelName} · перенос на ровер не доказан`}
|
||||||
|
statusTone="warning"
|
||||||
|
metrics={[
|
||||||
|
{
|
||||||
|
label: "GOOSE mIoU",
|
||||||
|
value: `${decimal(selected.meanIouPercent, 2)}% / ${decimal(alternative.meanIouPercent, 2)}%`,
|
||||||
|
hint: `${selected.candidate} / ${alternative.candidate} · полный validation split`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Vegetation IoU",
|
||||||
|
value: `${decimal(selected.vegetationMeanIouPercent, 2)}% / ${decimal(alternative.vegetationMeanIouPercent, 2)}%`,
|
||||||
|
hint: "grass/vegetation/bush/tree и родственные fine-64 labels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Worker shadow p95",
|
||||||
|
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`,
|
||||||
|
hint: "чистый inference · одна тяжёлая модель за раз",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Peak VRAM",
|
||||||
|
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} / ${decimal(alternative.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
|
||||||
|
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
conclusion={{
|
||||||
|
proved: "Обе готовые fine-64 модели воспроизводимо запускаются; DDRNet лучше по aggregate vegetation IoU.",
|
||||||
|
notProved: "Не доказаны accuracy на нашем fisheye, temporal stability, collision safety и физическое поведение ровера.",
|
||||||
|
decision: "Хранить как архив квалификации весов. Проверку на RAVNOVES00 вести только в основной многослойной LAB.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,10 +14,6 @@ import {
|
|||||||
fetchM49TgsFullShadowResult,
|
fetchM49TgsFullShadowResult,
|
||||||
type M49TgsFullShadowResult,
|
type M49TgsFullShadowResult,
|
||||||
} from "../../core/laboratory/m49TgsFullShadow";
|
} from "../../core/laboratory/m49TgsFullShadow";
|
||||||
import {
|
|
||||||
M48MaskComparisonVisual,
|
|
||||||
type M48MaskComparisonCase,
|
|
||||||
} from "./M48FailureAtlasVisual";
|
|
||||||
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
||||||
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
||||||
|
|
||||||
@@ -25,37 +21,6 @@ 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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
|
function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) {
|
||||||
const route = result.routeVideo!;
|
const route = result.routeVideo!;
|
||||||
const [tgs, setTgs] = useState<M49TgsFullShadowResult | null>(null);
|
const [tgs, setTgs] = useState<M49TgsFullShadowResult | null>(null);
|
||||||
@@ -83,16 +48,14 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
|
|||||||
}, [route.baseM4ResultId, route.linkedTgsResultId]);
|
}, [route.baseM4ResultId, route.linkedTgsResultId]);
|
||||||
|
|
||||||
const semantic = {
|
const semantic = {
|
||||||
|
id: "vegetation",
|
||||||
|
controlLabel: "ПРИРОДА · DDRNet",
|
||||||
resultId: route.workerResultId,
|
resultId: route.workerResultId,
|
||||||
spatialResultId: null,
|
spatialResultId: null,
|
||||||
taxonomy: route.taxonomy,
|
taxonomy: route.taxonomy,
|
||||||
maskUrl: (sequence: number) => vegetationVideoMaskUrl(result.resultId, sequence),
|
maskUrl: (sequence: number) => vegetationVideoMaskUrl(result.resultId, sequence),
|
||||||
label: route.viewKind === "coarse-material-policy-review"
|
label: "DDRNet coarse vegetation material · recorded video",
|
||||||
? "Coarse material evidence · recorded video"
|
maskAriaLabel: "DDRNet vegetation material prediction",
|
||||||
: "DDRNet vegetation prediction · recorded video",
|
|
||||||
maskAriaLabel: route.viewKind === "coarse-material-policy-review"
|
|
||||||
? "Coarse material policy evidence"
|
|
||||||
: "DDRNet vegetation prediction",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
if (route.linkedTgsResultId && tgs) {
|
if (route.linkedTgsResultId && tgs) {
|
||||||
@@ -100,19 +63,23 @@ function VegetationRouteEvidence({ result }: { result: VegetationShadowResult })
|
|||||||
<M49TgsFullShadowEvidence
|
<M49TgsFullShadowEvidence
|
||||||
result={tgs}
|
result={tgs}
|
||||||
semanticOverride={semantic}
|
semanticOverride={semantic}
|
||||||
evidenceLabel="LAB V1 · MATERIAL + YOLOX + TGS"
|
evidenceLabel="LAB V1 · EoMT + DDRNet + YOLOX + TGS"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (route.linkedTgsResultId && !tgsError) {
|
if (route.linkedTgsResultId && !tgsError) {
|
||||||
return <div className="m4-replay-threat-visual__pane-status" role="status">Открываем sealed TGS и coarse material timeline…</div>;
|
return (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
|
Открываем sealed EoMT, TGS и coarse vegetation timeline…
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<M4ReplayThreatVisual
|
<M4ReplayThreatVisual
|
||||||
resultId={route.baseM4ResultId}
|
resultId={route.baseM4ResultId}
|
||||||
evidenceLabel="LAB V1 · DDRNet"
|
evidenceLabel="LAB V1 · DDRNet"
|
||||||
showReferenceMediaLayers={route.viewKind === "coarse-material-policy-review"}
|
showReferenceMediaLayers
|
||||||
showSpatialOverlaySummary={false}
|
showSpatialOverlaySummary={false}
|
||||||
semantic={semantic}
|
semantic={semantic}
|
||||||
/>
|
/>
|
||||||
@@ -132,146 +99,117 @@ export function VegetationShadowResultView({
|
|||||||
rigLabel: string;
|
rigLabel: string;
|
||||||
result: VegetationShadowResult;
|
result: VegetationShadowResult;
|
||||||
}) {
|
}) {
|
||||||
|
const route = result.routeVideo;
|
||||||
const selected = result.candidates.find(
|
const selected = result.candidates.find(
|
||||||
(candidate) => candidate.candidate === result.selectedCandidate,
|
(candidate) => candidate.candidate === result.selectedCandidate,
|
||||||
)!;
|
)!;
|
||||||
const alternative = result.candidates.find(
|
|
||||||
(candidate) => candidate.candidate !== result.selectedCandidate,
|
|
||||||
)!;
|
|
||||||
return (
|
return (
|
||||||
<LaboratoryWorkTemplate
|
<LaboratoryWorkTemplate
|
||||||
summary={(
|
summary={(
|
||||||
<LaboratorySummary
|
<LaboratorySummary
|
||||||
title="LAB V1 · готовые модели растительности"
|
title="LAB V1 · карта ровера · город + растительность"
|
||||||
description={result.routeVideo
|
description="Один recorded-контур RAVNOVES00 синхронно показывает городской EoMT, природный DDRNet, frozen YOLOX detections и causal TGS. Семантические маски переключаются, чтобы их цвета не скрывали друг друга; геометрическое veto остаётся независимым."
|
||||||
? result.routeVideo.viewKind === "coarse-material-policy-review"
|
status={route
|
||||||
? "M4.8 сохраняет truth-backed сравнение моделей, а штатный M4.7 синхронно показывает coarse material evidence, frozen YOLOX vetoes и causal TGS на всей записи RAVNOVES00. Все слои запечатаны локально и открываются без Worker 006."
|
? "MULTILAYER RECORDED REVIEW · commands OFF · route truth отсутствует"
|
||||||
: "M4.8 сохраняет truth-backed сравнение моделей, а штатный M4.7 viewer показывает фактический DDRNet prediction на всей записи RAVNOVES00. Все 4489 масок запечатаны локально и открываются без Worker 006."
|
: "ROUTE EVIDENCE MISSING · commands OFF"}
|
||||||
: "Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."}
|
|
||||||
status={result.routeVideo
|
|
||||||
? result.routeVideo.viewKind === "coarse-material-policy-review"
|
|
||||||
? "MULTILAYER POLICY REVIEW · commands OFF · route truth отсутствует"
|
|
||||||
: "DDRNet full-video prediction ready · route truth отсутствует"
|
|
||||||
: "Truth-backed model comparison · route transfer не принят"}
|
|
||||||
statusTone="warning"
|
statusTone="warning"
|
||||||
facts={[
|
facts={[
|
||||||
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" },
|
{ label: "Источник", value: "RAVNOVES00 · sensor.camera.right · 4489 recorded frames" },
|
||||||
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
|
{ label: "Город", value: "EoMT Cityscapes · sealed E47 semantic archive" },
|
||||||
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
|
{ label: "Растительность", value: "DDRNet-39 fine-64 → coarse mission-neutral materials" },
|
||||||
...(result.routeVideo ? [{
|
{ label: "Safety", value: "YOLOX object boxes + causal TGS · semantic masks не снимают veto" },
|
||||||
label: "Видео",
|
{ label: "Authority", value: `${rigLabel} · VISUAL REVIEW ONLY · commands OFF` },
|
||||||
value: result.routeVideo.viewKind === "coarse-material-policy-review"
|
|
||||||
? "RAVNOVES00 · 4489/4489 coarse masks + YOLOX + TGS · exact sequence"
|
|
||||||
: "RAVNOVES00 · 4489/4489 DDRNet masks · exact recorded sequence",
|
|
||||||
}] : []),
|
|
||||||
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
|
|
||||||
]}
|
]}
|
||||||
brief={{
|
brief={{
|
||||||
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
|
question: "Можно ли одновременно видеть городской и природный semantic stack, не теряя независимую геометрическую защиту?",
|
||||||
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.",
|
approach: "EoMT и DDRNet сохранены как два независимых sealed слоя на одной M4 timeline. В штатном M4.7 viewer пользователь переключает только отображаемую маску; YOLOX и TGS остаются активными слоями evidence.",
|
||||||
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. ${result.routeVideo?.viewKind === "coarse-material-policy-review" ? "Fine-64 prediction сведён к mission-neutral материалам; YOLOX и TGS сохраняют независимое veto." : result.routeVideo ? "Его фактическая temporal stability теперь видна на всех 4489 кадрах штатного recorded viewer." : "Ошибки по каждому типу проверяются в одном штатном инструменте."}`,
|
principalResult: route
|
||||||
limitation: "GOOSE — внешний размеченный домен; RAVNOVES00 — наш fisheye, но без ручной truth-разметки. Материалы — prediction, а не доказательство проходимости. TGS не проецируется в пиксели без отдельной принятой калибровки.",
|
? "Оба semantic archive доступны в одном viewer. Это не пиксельный fusion и не единая новая модель: городской и природный ответы остаются раздельными."
|
||||||
|
: "Route archive для этой immutable identity отсутствует.",
|
||||||
|
limitation: "RAVNOVES00 не имеет ручной truth. DDRNet заметно прыгает между HIGH GRASS, WOODY и UNKNOWN; поэтому subtype нельзя подавать напрямую в planner. Отсутствие класса никогда не означает свободный путь.",
|
||||||
}}
|
}}
|
||||||
method={{
|
method={{
|
||||||
completeness: "complete",
|
completeness: route ? "complete" : "legacy-partial",
|
||||||
executionClass: "ai-inference",
|
executionClass: "ai-inference",
|
||||||
pipelineId: "goose-fine64-ready-weights-to-ravnoves-policy-shadow/v1",
|
pipelineId: "ravnoves-eomt-ddrnet-yolox-causal-tgs-recorded-review/v1",
|
||||||
components: result.candidates.map((candidate) => ({
|
components: [
|
||||||
kind: "model" as const,
|
{
|
||||||
name: candidate.loadedModelName,
|
kind: "model",
|
||||||
version: candidate.candidate,
|
name: "EoMT Cityscapes semantic",
|
||||||
role: candidate.candidate === result.selectedCandidate ? "selected policy provider" : "comparison candidate",
|
version: "sealed E47 archive",
|
||||||
identitySha256: candidate.checkpointSha256,
|
role: "urban semantic review",
|
||||||
})),
|
identitySha256: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "model",
|
||||||
|
name: selected.loadedModelName,
|
||||||
|
version: selected.candidate,
|
||||||
|
role: "vegetation material candidate",
|
||||||
|
identitySha256: selected.checkpointSha256,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "algorithm",
|
||||||
|
name: "Frozen YOLOX + causal TGS",
|
||||||
|
version: "linked M4/M4.9 archives",
|
||||||
|
role: "independent object and geometry veto",
|
||||||
|
identitySha256: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
evidence={(
|
evidence={route ? (
|
||||||
<>
|
<LaboratoryEvidence
|
||||||
<LaboratoryEvidence
|
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
||||||
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
|
title="EoMT CITY / DDRNet VEGETATION + YOLOX + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
|
||||||
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
|
kind="diagnostic-model"
|
||||||
kind="diagnostic-model"
|
resizable
|
||||||
resizable
|
>
|
||||||
>
|
<VegetationRouteEvidence result={result} />
|
||||||
<M48MaskComparisonVisual
|
</LaboratoryEvidence>
|
||||||
cases={comparisonCases(result)}
|
) : (
|
||||||
initialCandidate={result.selectedCandidate}
|
<LaboratoryEvidence
|
||||||
/>
|
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
||||||
</LaboratoryEvidence>
|
title="ROUTE ARCHIVE отсутствует"
|
||||||
{result.routeVideo ? (
|
kind="diagnostic-model"
|
||||||
<LaboratoryEvidence
|
>
|
||||||
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
||||||
title={result.routeVideo.viewKind === "coarse-material-policy-review"
|
Для этой immutable identity нет полного route video evidence.
|
||||||
? "COARSE MATERIAL + YOLOX VETO + CAUSAL TGS · 4489/4489 · TRUTH отсутствует"
|
</div>
|
||||||
: "DDRNet PREDICTION · 4489/4489 кадров · TRUTH для этой записи отсутствует"}
|
</LaboratoryEvidence>
|
||||||
kind="diagnostic-model"
|
|
||||||
resizable
|
|
||||||
>
|
|
||||||
<VegetationRouteEvidence result={result} />
|
|
||||||
</LaboratoryEvidence>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
result={(
|
result={(
|
||||||
<LaboratoryResultSummary
|
<LaboratoryResultSummary
|
||||||
title={result.routeVideo?.viewKind === "coarse-material-policy-review"
|
title="Многослойный visual review собран; управление не авторизовано"
|
||||||
? "Слои собраны для визуального policy review; управление не авторизовано"
|
status="Semantics advisory · YOLOX/TGS veto cannot be cleared"
|
||||||
: "DDRNet — стартовые веса; перенос на ровер ещё не доказан"}
|
|
||||||
status={result.routeVideo?.viewKind === "coarse-material-policy-review"
|
|
||||||
? "Materials are advisory · YOLOX/TGS veto cannot be cleared"
|
|
||||||
: `${selected.loadedModelName} выбран только как vegetation candidate`}
|
|
||||||
statusTone="warning"
|
statusTone="warning"
|
||||||
metrics={[
|
metrics={[
|
||||||
{
|
{
|
||||||
label: "GOOSE mIoU",
|
label: "Route masks",
|
||||||
value: `${decimal(selected.meanIouPercent, 2)}% / ${decimal(alternative.meanIouPercent, 2)}%`,
|
value: route ? `${route.frameCount}/${route.frameCount}` : "0/4489",
|
||||||
hint: `${selected.candidate} / ${alternative.candidate} · полный validation split`,
|
hint: "sealed local playback · Worker для открытия не нужен",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Vegetation IoU",
|
label: "Semantic sources",
|
||||||
value: `${decimal(selected.vegetationMeanIouPercent, 2)}% / ${decimal(alternative.vegetationMeanIouPercent, 2)}%`,
|
value: route ? "2 independent layers" : "0",
|
||||||
hint: "агрегация классов grass/vegetation/bush/tree и родственных fine-64 labels",
|
hint: "EoMT CITY / DDRNet VEGETATION · display switches, evidence does not fuse",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Worker shadow p95",
|
label: "Vegetation worker p95",
|
||||||
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`,
|
value: `${decimal(selected.shadowLatencyP95Ms, 2)} ms`,
|
||||||
hint: "чистый inference · одна тяжёлая модель за раз",
|
hint: "изолированный DDRNet inference; не совместный realtime stack",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Cold prewarm",
|
label: "Vegetation peak VRAM",
|
||||||
value: `${decimal(selected.shadowPrewarmLatencyMs, 1)} / ${decimal(alternative.shadowPrewarmLatencyMs, 1)} ms`,
|
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
|
||||||
hint: "один явный inference до допуска кадров; исключён из steady-state p95",
|
hint: "DDRNet candidate на Worker 006",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Worker throughput",
|
|
||||||
value: `${decimal(selected.shadowThroughputFps, 1)} / ${decimal(alternative.shadowThroughputFps, 1)} FPS`,
|
|
||||||
hint: "изолированный Worker 006 · не realtime graph целиком",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Peak VRAM",
|
|
||||||
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} / ${decimal(alternative.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
|
|
||||||
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Hard-case evidence",
|
|
||||||
value: "12 truth-backed cases",
|
|
||||||
hint: "8 vegetation strata · Worker для открытия не требуется",
|
|
||||||
},
|
|
||||||
...(result.routeVideo ? [{
|
|
||||||
label: "Route video",
|
|
||||||
value: "4489/4489 masks",
|
|
||||||
hint: result.routeVideo.viewKind === "coarse-material-policy-review"
|
|
||||||
? "9 coarse states · YOLOX + causal TGS · Worker-independent playback"
|
|
||||||
: "DDRNet prediction · exact sequence · Worker-independent playback",
|
|
||||||
}] : []),
|
|
||||||
]}
|
]}
|
||||||
conclusion={{
|
conclusion={{
|
||||||
proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.",
|
proved: "На одной recorded timeline доступны городской EoMT, природный DDRNet, YOLOX detections и causal TGS; LAB автономна от Worker.",
|
||||||
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, collision safety и physical-live поведение ровера. Видео позволяет увидеть temporal stability, но без truth не превращает её в метрику качества.",
|
notProved: "Не доказаны совместный live-runtime EoMT+DDRNet, truth accuracy на fisheye, стабильные vegetation subtypes и безопасное управление ровером.",
|
||||||
decision: result.routeVideo?.viewKind === "coarse-material-policy-review"
|
decision: "Использовать маски только для диагностики. Следующий qualification gate — motion-aware temporal vegetation fusion и отдельный совместный realtime load test; до него planner/actuation остаются OFF.",
|
||||||
? "На одном M4.7 проверить ложные LOW GRASS/HIGH GRASS кандидаты против YOLOX и TGS. До truth-кейсов и integrated load этот слой не подключать к planner/actuation."
|
|
||||||
: "Смотреть полный prediction на видео и собирать конкретные temporal/domain failure cases. DDRNet остаётся diagnostic candidate; LiDAR/TGS fail-closed геометрию не ослаблять.",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type LaboratoryProfileId =
|
|||||||
| "rig-camera-local-surface-v1"
|
| "rig-camera-local-surface-v1"
|
||||||
| "rig-track-geometry-temporal-v1"
|
| "rig-track-geometry-temporal-v1"
|
||||||
| "rig-ravnoves-perception-gate-v1"
|
| "rig-ravnoves-perception-gate-v1"
|
||||||
|
| "rig-goose-vegetation-benchmark-v1"
|
||||||
| "rig-pointpillars-transfer-v1"
|
| "rig-pointpillars-transfer-v1"
|
||||||
| "rig-right-yolox-lidar-range-v1"
|
| "rig-right-yolox-lidar-range-v1"
|
||||||
| "rig-nvidia-ready-stack-v1"
|
| "rig-nvidia-ready-stack-v1"
|
||||||
@@ -63,12 +64,19 @@ interface KnownWorkDefinition {
|
|||||||
const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг";
|
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-benchmark": {
|
||||||
|
profileId: "rig-goose-vegetation-benchmark-v1",
|
||||||
|
profileName: (rigLabel) => `${rig(rigLabel)} · GOOSE vegetation archive`,
|
||||||
|
experimentId: "lab-v1-vegetation-benchmark-archive",
|
||||||
|
experimentName: "DDRNet vs PPLiteSeg · truth-backed archival comparison",
|
||||||
|
variantName: "M4.8 · GOOSE truth · архивный анализ моделей",
|
||||||
|
},
|
||||||
"lab-v1-vegetation-shadow": {
|
"lab-v1-vegetation-shadow": {
|
||||||
profileId: "rig-ravnoves-perception-gate-v1",
|
profileId: "rig-ravnoves-perception-gate-v1",
|
||||||
profileName: (rigLabel) => `${rig(rigLabel)} · GOOSE vegetation qualification`,
|
profileName: (rigLabel) => `${rig(rigLabel)} · RAVNOVES00 rover perception gate`,
|
||||||
experimentId: "lab-v1-vegetation-mission-policy",
|
experimentId: "lab-v1-vegetation-mission-policy",
|
||||||
experimentName: "DDRNet vs PPLiteSeg · truth-backed vegetation hard cases",
|
experimentName: "RAVNOVES00 · city + vegetation + TGS review",
|
||||||
variantName: "LAB V1 · готовые vegetation weights · GOOSE truth",
|
variantName: "LAB V1 · EoMT + DDRNet + YOLOX + TGS · commands OFF",
|
||||||
},
|
},
|
||||||
"m48-object-centric-quality": {
|
"m48-object-centric-quality": {
|
||||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ function mergeResults(
|
|||||||
next: AdvancedLaboratoryResults,
|
next: AdvancedLaboratoryResults,
|
||||||
): AdvancedLaboratoryResults {
|
): AdvancedLaboratoryResults {
|
||||||
return {
|
return {
|
||||||
|
vegetationBenchmark: next.vegetationBenchmark ?? current.vegetationBenchmark,
|
||||||
vegetationShadow: next.vegetationShadow ?? current.vegetationShadow,
|
vegetationShadow: next.vegetationShadow ?? current.vegetationShadow,
|
||||||
m47Graph: next.m47Graph ?? current.m47Graph,
|
m47Graph: next.m47Graph ?? current.m47Graph,
|
||||||
m48: next.m48 ?? current.m48,
|
m48: next.m48 ?? current.m48,
|
||||||
@@ -122,6 +123,7 @@ export function useAdvancedLaboratoryCatalog({
|
|||||||
const indexedResultId = index.find((item) => item.workId === selectedWorkId)?.resultId;
|
const indexedResultId = index.find((item) => item.workId === selectedWorkId)?.resultId;
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
|
"lab-v1-vegetation-benchmark",
|
||||||
"lab-v1-vegetation-shadow",
|
"lab-v1-vegetation-shadow",
|
||||||
"m47-reference-graph-shadow",
|
"m47-reference-graph-shadow",
|
||||||
"m48-object-centric-quality",
|
"m48-object-centric-quality",
|
||||||
|
|||||||
@@ -108,8 +108,13 @@ test("M4.9T5 viewer prefers autonomous chunks and keeps a sealed legacy fallback
|
|||||||
assert.doesNotMatch(source, /centersXyM\.map\(/);
|
assert.doesNotMatch(source, /centersXyM\.map\(/);
|
||||||
assert.match(source, /fetchE47SemanticSlamResult/);
|
assert.match(source, /fetchE47SemanticSlamResult/);
|
||||||
assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/);
|
assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/);
|
||||||
assert.match(source, /semantic=\{semanticOverride \?\? \(semantic \? \{/);
|
assert.match(source, /semanticLayers=\{semanticLayers\}/);
|
||||||
|
assert.match(source, /ГОРОД · EoMT/);
|
||||||
|
assert.match(source, /ПРИРОДА · DDRNet/);
|
||||||
assert.match(source, /semanticOverride/);
|
assert.match(source, /semanticOverride/);
|
||||||
|
assert.doesNotMatch(source, /if \(semanticOverride\) return/);
|
||||||
|
assert.match(visual, /label="Источник семантики"/);
|
||||||
|
assert.match(visual, /availableSemanticLayers\.length > 1/);
|
||||||
assert.doesNotMatch(visual, /classifiedSpatialLayer \|\| !showReferenceMediaLayers \? \[\]/);
|
assert.doesNotMatch(visual, /classifiedSpatialLayer \|\| !showReferenceMediaLayers \? \[\]/);
|
||||||
assert.match(
|
assert.match(
|
||||||
visual,
|
visual,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { after, before, test } from "node:test";
|
|||||||
import { createServer } from "vite";
|
import { createServer } from "vite";
|
||||||
|
|
||||||
let server;
|
let server;
|
||||||
|
let fetchVegetationBenchmarkResult;
|
||||||
let fetchVegetationShadowResult;
|
let fetchVegetationShadowResult;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
@@ -13,7 +14,7 @@ before(async () => {
|
|||||||
logLevel: "silent",
|
logLevel: "silent",
|
||||||
server: { middlewareMode: true },
|
server: { middlewareMode: true },
|
||||||
});
|
});
|
||||||
({ fetchVegetationShadowResult } = await server.ssrLoadModule(
|
({ fetchVegetationBenchmarkResult, fetchVegetationShadowResult } = await server.ssrLoadModule(
|
||||||
"/src/core/laboratory/vegetationShadow.ts",
|
"/src/core/laboratory/vegetationShadow.ts",
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@@ -23,6 +24,7 @@ after(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resultId = `lab-v1-vegetation-shadow-${"a".repeat(64)}`;
|
const resultId = `lab-v1-vegetation-shadow-${"a".repeat(64)}`;
|
||||||
|
const benchmarkResultId = `lab-v1-vegetation-benchmark-${"d".repeat(64)}`;
|
||||||
|
|
||||||
function candidate(candidateKey, vegetationIou) {
|
function candidate(candidateKey, vegetationIou) {
|
||||||
return {
|
return {
|
||||||
@@ -111,21 +113,26 @@ function coarseRouteVideo() {
|
|||||||
linked_tgs_result_id: `m49-tgs-full-shadow-${"2".repeat(64)}`,
|
linked_tgs_result_id: `m49-tgs-full-shadow-${"2".repeat(64)}`,
|
||||||
taxonomy: {
|
taxonomy: {
|
||||||
schema_version: "missioncore.lab-v1-terrain-policy-taxonomy/v1",
|
schema_version: "missioncore.lab-v1-terrain-policy-taxonomy/v1",
|
||||||
classes: Array.from({ length: 9 }, (_, classId) => ({
|
classes: Array.from({ length: 10 }, (_, classId) => ({
|
||||||
class_id: classId,
|
class_id: classId,
|
||||||
label: `policy-${classId}`,
|
label: `policy-${classId}`,
|
||||||
color_rgb: [classId, classId, classId],
|
color_rgb: [classId, classId, classId],
|
||||||
disposition: classId === 0 ? "ambiguous" : "prediction",
|
disposition: classId === 0 ? "ambiguous" : classId === 9 ? "undefined" : "prediction",
|
||||||
material_class: classId === 0 ? null : "grass",
|
material_class: classId === 0 || classId === 9 ? null : "grass",
|
||||||
evidence_state: classId === 0 ? "UNOBSERVED" : "SUPPORTED_GROUND",
|
evidence_state: classId === 0 || classId === 9 ? "UNOBSERVED" : "SUPPORTED_GROUND",
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
aggregate_prediction_pixels: Array(9).fill(0),
|
aggregate_prediction_pixels: Array(10).fill(0),
|
||||||
mask_archive: {
|
mask_archive: {
|
||||||
path: "video/coarse-material-policy-masks.zip",
|
path: "video/coarse-material-policy-masks.zip",
|
||||||
sha256: "8".repeat(64),
|
sha256: "8".repeat(64),
|
||||||
byte_length: 2048,
|
byte_length: 2048,
|
||||||
},
|
},
|
||||||
|
valid_fov: {
|
||||||
|
mask_path: "video/valid-fov-mask.png",
|
||||||
|
mask_sha256: "7".repeat(64),
|
||||||
|
outside_valid_fov_class_id: 9,
|
||||||
|
},
|
||||||
policy: {
|
policy: {
|
||||||
presets: {
|
presets: {
|
||||||
urban: { grass: "NO_GO" },
|
urban: { grass: "NO_GO" },
|
||||||
@@ -220,23 +227,55 @@ test("vegetation LAB parses coarse material policy and sealed TGS binding", asyn
|
|||||||
});
|
});
|
||||||
assert.equal(result.routeVideo.viewKind, "coarse-material-policy-review");
|
assert.equal(result.routeVideo.viewKind, "coarse-material-policy-review");
|
||||||
assert.match(result.routeVideo.linkedTgsResultId, /^m49-tgs-full-shadow-/);
|
assert.match(result.routeVideo.linkedTgsResultId, /^m49-tgs-full-shadow-/);
|
||||||
assert.equal(result.routeVideo.taxonomy.length, 9);
|
assert.equal(result.routeVideo.taxonomy.length, 10);
|
||||||
assert.equal(result.routeVideo.taxonomy[0].evidenceState, "UNOBSERVED");
|
assert.equal(result.routeVideo.taxonomy[0].evidenceState, "UNOBSERVED");
|
||||||
assert.equal(result.routeVideo.policyPresets.urban.grass, "NO_GO");
|
assert.equal(result.routeVideo.policyPresets.urban.grass, "NO_GO");
|
||||||
assert.equal(result.routeVideo.fusionMode, "synchronised-multilayer-review");
|
assert.equal(result.routeVideo.fusionMode, "synchronised-multilayer-review");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("vegetation LAB reuses the admitted M4.8 and M4.7 instruments", async () => {
|
test("vegetation GOOSE benchmark opens through its separate archival endpoint", async () => {
|
||||||
const resultSource = await readFile(
|
let requestedUrl = "";
|
||||||
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
const result = await fetchVegetationBenchmarkResult(benchmarkResultId, {
|
||||||
"utf8",
|
fetcher: async (url) => {
|
||||||
|
requestedUrl = String(url);
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
...labPayload(null),
|
||||||
|
result_id: benchmarkResultId,
|
||||||
|
}), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
requestedUrl,
|
||||||
|
`/api/v1/laboratory/vegetation-benchmark/${benchmarkResultId}`,
|
||||||
);
|
);
|
||||||
assert.match(resultSource, /M48MaskComparisonVisual/);
|
assert.equal(result.routeVideo, null);
|
||||||
|
assert.equal(result.validationCases.length, 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("vegetation realtime LAB and archival benchmark use separate admitted instruments", async () => {
|
||||||
|
const [resultSource, benchmarkSource] = await Promise.all([
|
||||||
|
readFile(
|
||||||
|
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
readFile(
|
||||||
|
new URL("../src/workspaces/laboratory/VegetationBenchmarkResult.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
|
||||||
assert.match(resultSource, /M4ReplayThreatVisual/);
|
assert.match(resultSource, /M4ReplayThreatVisual/);
|
||||||
assert.match(resultSource, /M49TgsFullShadowEvidence/);
|
assert.match(resultSource, /M49TgsFullShadowEvidence/);
|
||||||
assert.match(resultSource, /semanticOverride/);
|
assert.match(resultSource, /semanticOverride/);
|
||||||
|
assert.match(resultSource, /EoMT CITY \/ DDRNet VEGETATION/);
|
||||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
|
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
|
||||||
assert.match(resultSource, /linkedTgsResultId/);
|
assert.match(resultSource, /linkedTgsResultId/);
|
||||||
|
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||||
|
assert.doesNotMatch(benchmarkSource, /M49TgsFullShadowEvidence/);
|
||||||
|
assert.equal(benchmarkSource.match(/<LaboratoryEvidence\b/g)?.length, 1);
|
||||||
assert.doesNotMatch(resultSource, /VegetationRouteVisual|urban\/rural\/off-road presets/);
|
assert.doesNotMatch(resultSource, /VegetationRouteVisual|urban\/rural\/off-road presets/);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
access(new URL("../src/workspaces/laboratory/VegetationShadowVisual.tsx", import.meta.url)),
|
access(new URL("../src/workspaces/laboratory/VegetationShadowVisual.tsx", import.meta.url)),
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||||
|
"work_id": "lab-v1-vegetation-benchmark",
|
||||||
|
"evidence": {
|
||||||
|
"runtime_relative_root": "lab-v1-vegetation-benchmark/results",
|
||||||
|
"result_id_prefix": "lab-v1-vegetation-benchmark",
|
||||||
|
"document_name": "result.json",
|
||||||
|
"schema_version": "missioncore.lab-v1-vegetation-shadow/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,6 +212,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"legacy_work_ids": [
|
"legacy_work_ids": [
|
||||||
|
"lab-v1-vegetation-benchmark",
|
||||||
"m48r3-static-occupancy-shadow",
|
"m48r3-static-occupancy-shadow",
|
||||||
"m47-reference-graph-shadow",
|
"m47-reference-graph-shadow",
|
||||||
"e31-source-binding",
|
"e31-source-binding",
|
||||||
|
|||||||
@@ -282,10 +282,17 @@
|
|||||||
"lifecycle": "current",
|
"lifecycle": "current",
|
||||||
"visual_evidence": "available"
|
"visual_evidence": "available"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"catalog_id": "lab-v1-vegetation-benchmark",
|
||||||
|
"evidence_id": "lab-v1-vegetation-benchmark-a8944d6c2d1102d81da78bcb4963760c9288db0421d9f2686afcbdd14b610d3d",
|
||||||
|
"signal": "progress",
|
||||||
|
"lifecycle": "current",
|
||||||
|
"visual_evidence": "available"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"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-394a5bca860e49a619a90c0f267259fd17e550a0abc4b51f821b1f482040c00e",
|
||||||
"signal": "failed",
|
"signal": "progress",
|
||||||
"lifecycle": "current",
|
"lifecycle": "current",
|
||||||
"visual_evidence": "available"
|
"visual_evidence": "available"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""Seal a benchmark-only vegetation result into its archival LAB namespace."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
|
||||||
|
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||||
|
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
|
||||||
|
|
||||||
|
_SOURCE_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||||
|
work_id="lab-v1-vegetation-shadow",
|
||||||
|
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
|
||||||
|
result_id_prefix="lab-v1-vegetation-shadow",
|
||||||
|
document_name="result.json",
|
||||||
|
result_schema_version=LAB_SCHEMA,
|
||||||
|
)
|
||||||
|
_ARCHIVE_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||||
|
work_id="lab-v1-vegetation-benchmark",
|
||||||
|
runtime_relative_root=PurePosixPath("lab-v1-vegetation-benchmark/results"),
|
||||||
|
result_id_prefix="lab-v1-vegetation-benchmark",
|
||||||
|
document_name="result.json",
|
||||||
|
result_schema_version=LAB_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VegetationBenchmarkArchiveError(ValueError):
|
||||||
|
"""The source result is not a valid benchmark-only immutable result."""
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(value: object) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
value,
|
||||||
|
ensure_ascii=False,
|
||||||
|
separators=(",", ":"),
|
||||||
|
sort_keys=True,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _object(value: object, label: str) -> dict[str, Any]:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise VegetationBenchmarkArchiveError(f"{label} is invalid")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def seal_vegetation_benchmark_archive(
|
||||||
|
*,
|
||||||
|
source_result_root: Path,
|
||||||
|
output_root: Path,
|
||||||
|
) -> Path:
|
||||||
|
source = source_result_root.resolve(strict=True)
|
||||||
|
verify_laboratory_evidence_result(_SOURCE_DEFINITION, source)
|
||||||
|
manifest = _object(
|
||||||
|
json.loads((source / "result.json").read_text("utf-8")),
|
||||||
|
"source result",
|
||||||
|
)
|
||||||
|
if manifest.get("route_video") is not None:
|
||||||
|
raise VegetationBenchmarkArchiveError("benchmark archive source contains route video")
|
||||||
|
artifacts = manifest.get("artifacts")
|
||||||
|
if not isinstance(artifacts, list):
|
||||||
|
raise VegetationBenchmarkArchiveError("source artifacts are invalid")
|
||||||
|
|
||||||
|
identity = copy.deepcopy(_object(manifest.get("identity"), "source identity"))
|
||||||
|
identity.update(
|
||||||
|
{
|
||||||
|
"lab_id": "lab-v1-vegetation-benchmark-archive",
|
||||||
|
"archived_from_result_id": source.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||||
|
result_id = f"lab-v1-vegetation-benchmark-{identity_sha256}"
|
||||||
|
|
||||||
|
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
destination = output_root / result_id
|
||||||
|
if destination.exists():
|
||||||
|
verify_laboratory_evidence_result(_ARCHIVE_DEFINITION, destination)
|
||||||
|
return destination
|
||||||
|
|
||||||
|
temporary = Path(tempfile.mkdtemp(prefix=".vegetation-benchmark-", dir=output_root))
|
||||||
|
try:
|
||||||
|
for raw in artifacts:
|
||||||
|
descriptor = _object(raw, "artifact descriptor")
|
||||||
|
relative_text = descriptor.get("path")
|
||||||
|
if not isinstance(relative_text, str):
|
||||||
|
raise VegetationBenchmarkArchiveError("artifact path is invalid")
|
||||||
|
relative = PurePosixPath(relative_text)
|
||||||
|
if relative.is_absolute() or any(part in {"", ".", ".."} for part in relative.parts):
|
||||||
|
raise VegetationBenchmarkArchiveError("artifact path is unsafe")
|
||||||
|
source_path = source.joinpath(*relative.parts)
|
||||||
|
destination_path = temporary.joinpath(*relative.parts)
|
||||||
|
destination_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(source_path, destination_path)
|
||||||
|
|
||||||
|
archived = copy.deepcopy(manifest)
|
||||||
|
archived.update(
|
||||||
|
{
|
||||||
|
"result_id": result_id,
|
||||||
|
"identity": identity,
|
||||||
|
"identity_sha256": identity_sha256,
|
||||||
|
"archived_from_result_id": source.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(temporary / "result.json").write_bytes(_canonical_json(archived) + b"\n")
|
||||||
|
temporary.rename(destination)
|
||||||
|
verify_laboratory_evidence_result(_ARCHIVE_DEFINITION, destination)
|
||||||
|
return destination
|
||||||
|
except Exception:
|
||||||
|
shutil.rmtree(temporary, ignore_errors=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--source-result-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
print(
|
||||||
|
seal_vegetation_benchmark_archive(
|
||||||
|
source_result_root=args.source_result_root,
|
||||||
|
output_root=args.output_root,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"VegetationBenchmarkArchiveError",
|
||||||
|
"seal_vegetation_benchmark_archive",
|
||||||
|
]
|
||||||
@@ -112,6 +112,7 @@ def seal_vegetation_policy_review(
|
|||||||
mission_policy_path: Path,
|
mission_policy_path: Path,
|
||||||
provider_label_map_path: Path,
|
provider_label_map_path: Path,
|
||||||
m49_tgs_full_shadow_root: Path,
|
m49_tgs_full_shadow_root: Path,
|
||||||
|
valid_fov_mask_path: Path,
|
||||||
output_root: Path,
|
output_root: Path,
|
||||||
created_at_utc: str | None = None,
|
created_at_utc: str | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
@@ -142,6 +143,7 @@ def seal_vegetation_policy_review(
|
|||||||
raise VegetationPolicyReviewError("fine mask archive identity changed")
|
raise VegetationPolicyReviewError("fine mask archive identity changed")
|
||||||
raw_archive_path = base_root / "video" / "ddrnet-semantic-masks.zip"
|
raw_archive_path = base_root / "video" / "ddrnet-semantic-masks.zip"
|
||||||
fine_taxonomy = _object(base_route.get("taxonomy"), "fine taxonomy")
|
fine_taxonomy = _object(base_route.get("taxonomy"), "fine taxonomy")
|
||||||
|
valid_fov_source = valid_fov_mask_path.resolve(strict=True)
|
||||||
|
|
||||||
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-policy-", dir=output_root))
|
temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-policy-", dir=output_root))
|
||||||
@@ -152,11 +154,23 @@ def seal_vegetation_policy_review(
|
|||||||
artifacts=base.get("artifacts"),
|
artifacts=base.get("artifacts"),
|
||||||
)
|
)
|
||||||
policy_archive = temporary / "video" / "coarse-material-policy-masks.zip"
|
policy_archive = temporary / "video" / "coarse-material-policy-masks.zip"
|
||||||
|
valid_fov_destination = temporary / "video" / "valid-fov-mask.png"
|
||||||
|
valid_fov_destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(valid_fov_source, valid_fov_destination)
|
||||||
|
valid_fov_proof = {
|
||||||
|
"role": "route-camera-valid-fov-mask",
|
||||||
|
"path": "video/valid-fov-mask.png",
|
||||||
|
"byte_length": valid_fov_destination.stat().st_size,
|
||||||
|
"sha256": sha256_path(valid_fov_destination),
|
||||||
|
"media_type": "image/png",
|
||||||
|
}
|
||||||
|
artifacts.append(valid_fov_proof)
|
||||||
policy_counts = build_policy_mask_archive(
|
policy_counts = build_policy_mask_archive(
|
||||||
source_archive=raw_archive_path,
|
source_archive=raw_archive_path,
|
||||||
destination_archive=policy_archive,
|
destination_archive=policy_archive,
|
||||||
fine_taxonomy=fine_taxonomy,
|
fine_taxonomy=fine_taxonomy,
|
||||||
provider_label_map=provider_map,
|
provider_label_map=provider_map,
|
||||||
|
valid_fov_mask=valid_fov_destination,
|
||||||
)
|
)
|
||||||
policy_archive_proof = {
|
policy_archive_proof = {
|
||||||
"role": "route-coarse-material-mask-archive",
|
"role": "route-coarse-material-mask-archive",
|
||||||
@@ -180,6 +194,11 @@ def seal_vegetation_policy_review(
|
|||||||
"taxonomy": policy_taxonomy(),
|
"taxonomy": policy_taxonomy(),
|
||||||
"aggregate_prediction_pixels": policy_counts,
|
"aggregate_prediction_pixels": policy_counts,
|
||||||
"linked_tgs_result_id": tgs.result_id,
|
"linked_tgs_result_id": tgs.result_id,
|
||||||
|
"valid_fov": {
|
||||||
|
"mask_path": valid_fov_proof["path"],
|
||||||
|
"mask_sha256": valid_fov_proof["sha256"],
|
||||||
|
"outside_valid_fov_class_id": 9,
|
||||||
|
},
|
||||||
"policy": {
|
"policy": {
|
||||||
"profile_id": mission_policy["profile_id"],
|
"profile_id": mission_policy["profile_id"],
|
||||||
"profile_sha256": sha256_path(mission_policy_path),
|
"profile_sha256": sha256_path(mission_policy_path),
|
||||||
@@ -196,6 +215,7 @@ def seal_vegetation_policy_review(
|
|||||||
"spatial_safety_veto_layer": "M4.9 full TGS gravity-local costmap",
|
"spatial_safety_veto_layer": "M4.9 full TGS gravity-local costmap",
|
||||||
"temporal_consensus_owner": "TGS causal rolling 1 s and metric obstacle tracks",
|
"temporal_consensus_owner": "TGS causal rolling 1 s and metric obstacle tracks",
|
||||||
"camera_semantic_temporal_filter": "none",
|
"camera_semantic_temporal_filter": "none",
|
||||||
|
"camera_valid_fov_filter": "sealed exact KB4 valid-FOV mask",
|
||||||
"reason": "No admitted TGS-to-camera pixel projection exists.",
|
"reason": "No admitted TGS-to-camera pixel projection exists.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -238,7 +258,7 @@ def seal_vegetation_policy_review(
|
|||||||
"Vegetation semantics never clears YOLOX, LiDAR, metric obstacle "
|
"Vegetation semantics never clears YOLOX, LiDAR, metric obstacle "
|
||||||
"or TGS vetoes."
|
"or TGS vetoes."
|
||||||
),
|
),
|
||||||
"Undefined pixels outside the 600x600 center crop remain fail-closed.",
|
"Pixels outside the exact KB4 valid FOV are transparent UNOBSERVED evidence.",
|
||||||
(
|
(
|
||||||
"TGS remains in gravity-local space; no uncalibrated pixel "
|
"TGS remains in gravity-local space; no uncalibrated pixel "
|
||||||
"projection is fabricated."
|
"projection is fabricated."
|
||||||
@@ -269,6 +289,7 @@ def main() -> None:
|
|||||||
parser.add_argument("--mission-policy-path", type=Path, required=True)
|
parser.add_argument("--mission-policy-path", type=Path, required=True)
|
||||||
parser.add_argument("--provider-label-map-path", type=Path, required=True)
|
parser.add_argument("--provider-label-map-path", type=Path, required=True)
|
||||||
parser.add_argument("--m49-tgs-full-shadow-root", type=Path, required=True)
|
parser.add_argument("--m49-tgs-full-shadow-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--valid-fov-mask-path", type=Path, required=True)
|
||||||
parser.add_argument("--output-root", type=Path, required=True)
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
print(seal_vegetation_policy_review(**vars(args)))
|
print(seal_vegetation_policy_review(**vars(args)))
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ POLICY_CLASSES: Final = (
|
|||||||
"material_class": "vegetation_unknown",
|
"material_class": "vegetation_unknown",
|
||||||
"evidence_state": "VEGETATION_UNKNOWN",
|
"evidence_state": "VEGETATION_UNKNOWN",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"class_id": 9,
|
||||||
|
"label": "OUTSIDE VALID FOV · NO SENSOR EVIDENCE",
|
||||||
|
"color_rgb": [0, 0, 0],
|
||||||
|
"disposition": "undefined",
|
||||||
|
"material_class": None,
|
||||||
|
"evidence_state": "UNOBSERVED",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
_MATERIAL_TO_CLASS: Final = {
|
_MATERIAL_TO_CLASS: Final = {
|
||||||
@@ -155,10 +163,18 @@ def build_policy_mask_archive(
|
|||||||
destination_archive: Path,
|
destination_archive: Path,
|
||||||
fine_taxonomy: dict[str, object],
|
fine_taxonomy: dict[str, object],
|
||||||
provider_label_map: dict[str, Any],
|
provider_label_map: dict[str, Any],
|
||||||
|
valid_fov_mask: Path,
|
||||||
) -> list[int]:
|
) -> list[int]:
|
||||||
"""Map every fine mask to coarse evidence; safety vetoes remain separate layers."""
|
"""Map every fine mask to coarse evidence; safety vetoes remain separate layers."""
|
||||||
|
|
||||||
lut = fine_to_policy_lut(fine_taxonomy, provider_label_map)
|
lut = fine_to_policy_lut(fine_taxonomy, provider_label_map)
|
||||||
|
try:
|
||||||
|
with Image.open(valid_fov_mask) as image:
|
||||||
|
valid_fov = np.asarray(image.convert("L"), dtype=np.uint8) > 0
|
||||||
|
except OSError as exc:
|
||||||
|
raise VegetationPolicyVideoError("valid-FOV mask is unreadable") from exc
|
||||||
|
if valid_fov.shape != (HEIGHT, WIDTH) or not np.any(valid_fov) or np.all(valid_fov):
|
||||||
|
raise VegetationPolicyVideoError("valid-FOV mask geometry is invalid")
|
||||||
counts = np.zeros(len(POLICY_CLASSES), dtype=np.int64)
|
counts = np.zeros(len(POLICY_CLASSES), dtype=np.int64)
|
||||||
destination_archive.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
destination_archive.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
try:
|
try:
|
||||||
@@ -175,6 +191,7 @@ def build_policy_mask_archive(
|
|||||||
f"fine mask {member} has shape {fine.shape}, expected {(HEIGHT, WIDTH)}"
|
f"fine mask {member} has shape {fine.shape}, expected {(HEIGHT, WIDTH)}"
|
||||||
)
|
)
|
||||||
coarse = lut[fine]
|
coarse = lut[fine]
|
||||||
|
coarse[~valid_fov] = 9
|
||||||
counts += np.bincount(
|
counts += np.bincount(
|
||||||
coarse.reshape(-1),
|
coarse.reshape(-1),
|
||||||
minlength=len(POLICY_CLASSES),
|
minlength=len(POLICY_CLASSES),
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ def seal_vegetation_shadow_lab(
|
|||||||
mission_policy_path: Path | None = None,
|
mission_policy_path: Path | None = None,
|
||||||
provider_label_map_path: Path | None = None,
|
provider_label_map_path: Path | None = None,
|
||||||
m49_tgs_full_shadow_root: Path | None = None,
|
m49_tgs_full_shadow_root: Path | None = None,
|
||||||
|
valid_fov_mask_path: Path | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
roots = {
|
roots = {
|
||||||
("ddrnet", "goose"): ddrnet_goose_root.resolve(),
|
("ddrnet", "goose"): ddrnet_goose_root.resolve(),
|
||||||
@@ -349,6 +350,7 @@ def seal_vegetation_shadow_lab(
|
|||||||
mission_policy_path,
|
mission_policy_path,
|
||||||
provider_label_map_path,
|
provider_label_map_path,
|
||||||
m49_tgs_full_shadow_root,
|
m49_tgs_full_shadow_root,
|
||||||
|
valid_fov_mask_path,
|
||||||
)
|
)
|
||||||
if any(value is not None for value in policy_inputs) and not all(
|
if any(value is not None for value in policy_inputs) and not all(
|
||||||
value is not None for value in policy_inputs
|
value is not None for value in policy_inputs
|
||||||
@@ -385,6 +387,7 @@ def seal_vegetation_shadow_lab(
|
|||||||
mission_policy_path is not None
|
mission_policy_path is not None
|
||||||
and provider_label_map_path is not None
|
and provider_label_map_path is not None
|
||||||
and m49_tgs_full_shadow_root is not None
|
and m49_tgs_full_shadow_root is not None
|
||||||
|
and valid_fov_mask_path is not None
|
||||||
and route_video is not None
|
and route_video is not None
|
||||||
):
|
):
|
||||||
repository_root = mission_policy_path.resolve().parents[2]
|
repository_root = mission_policy_path.resolve().parents[2]
|
||||||
@@ -543,13 +546,25 @@ def seal_vegetation_shadow_lab(
|
|||||||
and linked_tgs_result_id is not None
|
and linked_tgs_result_id is not None
|
||||||
and mission_policy_path is not None
|
and mission_policy_path is not None
|
||||||
and provider_label_map_path is not None
|
and provider_label_map_path is not None
|
||||||
|
and valid_fov_mask_path is not None
|
||||||
):
|
):
|
||||||
policy_archive = temporary / "video" / "coarse-material-policy-masks.zip"
|
policy_archive = temporary / "video" / "coarse-material-policy-masks.zip"
|
||||||
|
valid_fov_destination = temporary / "video" / "valid-fov-mask.png"
|
||||||
|
shutil.copyfile(valid_fov_mask_path.resolve(strict=True), valid_fov_destination)
|
||||||
|
valid_fov_descriptor = {
|
||||||
|
"role": "route-camera-valid-fov-mask",
|
||||||
|
"path": "video/valid-fov-mask.png",
|
||||||
|
"byte_length": valid_fov_destination.stat().st_size,
|
||||||
|
"sha256": sha256_path(valid_fov_destination),
|
||||||
|
"media_type": "image/png",
|
||||||
|
}
|
||||||
|
artifacts.append(valid_fov_descriptor)
|
||||||
policy_counts = build_policy_mask_archive(
|
policy_counts = build_policy_mask_archive(
|
||||||
source_archive=route_video_archive,
|
source_archive=route_video_archive,
|
||||||
destination_archive=policy_archive,
|
destination_archive=policy_archive,
|
||||||
fine_taxonomy=_object(route_video["taxonomy"], "fine video taxonomy"),
|
fine_taxonomy=_object(route_video["taxonomy"], "fine video taxonomy"),
|
||||||
provider_label_map=provider_label_map,
|
provider_label_map=provider_label_map,
|
||||||
|
valid_fov_mask=valid_fov_destination,
|
||||||
)
|
)
|
||||||
policy_descriptor = {
|
policy_descriptor = {
|
||||||
"role": "route-coarse-material-mask-archive",
|
"role": "route-coarse-material-mask-archive",
|
||||||
@@ -571,6 +586,11 @@ def seal_vegetation_shadow_lab(
|
|||||||
"taxonomy": policy_taxonomy(),
|
"taxonomy": policy_taxonomy(),
|
||||||
"aggregate_prediction_pixels": policy_counts,
|
"aggregate_prediction_pixels": policy_counts,
|
||||||
"linked_tgs_result_id": linked_tgs_result_id,
|
"linked_tgs_result_id": linked_tgs_result_id,
|
||||||
|
"valid_fov": {
|
||||||
|
"mask_path": valid_fov_descriptor["path"],
|
||||||
|
"mask_sha256": valid_fov_descriptor["sha256"],
|
||||||
|
"outside_valid_fov_class_id": 9,
|
||||||
|
},
|
||||||
"policy": {
|
"policy": {
|
||||||
"profile_id": mission_policy["profile_id"],
|
"profile_id": mission_policy["profile_id"],
|
||||||
"profile_sha256": sha256_path(mission_policy_path),
|
"profile_sha256": sha256_path(mission_policy_path),
|
||||||
@@ -591,6 +611,7 @@ def seal_vegetation_shadow_lab(
|
|||||||
"TGS causal rolling 1 s and metric obstacle tracks"
|
"TGS causal rolling 1 s and metric obstacle tracks"
|
||||||
),
|
),
|
||||||
"camera_semantic_temporal_filter": "none",
|
"camera_semantic_temporal_filter": "none",
|
||||||
|
"camera_valid_fov_filter": "sealed exact KB4 valid-FOV mask",
|
||||||
"reason": "No admitted TGS-to-camera pixel projection exists.",
|
"reason": "No admitted TGS-to-camera pixel projection exists.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -680,7 +701,11 @@ def seal_vegetation_shadow_lab(
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
"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.",
|
(
|
||||||
|
"Pixels outside the exact KB4 valid FOV are transparent UNOBSERVED evidence."
|
||||||
|
if mission_policy is not None
|
||||||
|
else "Undefined pixels outside the 600x600 center crop remain fail-closed."
|
||||||
|
),
|
||||||
*(
|
*(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
@@ -723,6 +748,7 @@ def _parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--mission-policy-path", type=Path)
|
parser.add_argument("--mission-policy-path", type=Path)
|
||||||
parser.add_argument("--provider-label-map-path", type=Path)
|
parser.add_argument("--provider-label-map-path", type=Path)
|
||||||
parser.add_argument("--m49-tgs-full-shadow-root", type=Path)
|
parser.add_argument("--m49-tgs-full-shadow-root", type=Path)
|
||||||
|
parser.add_argument("--valid-fov-mask-path", type=Path)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -739,6 +765,7 @@ def main() -> None:
|
|||||||
mission_policy_path=args.mission_policy_path,
|
mission_policy_path=args.mission_policy_path,
|
||||||
provider_label_map_path=args.provider_label_map_path,
|
provider_label_map_path=args.provider_label_map_path,
|
||||||
m49_tgs_full_shadow_root=args.m49_tgs_full_shadow_root,
|
m49_tgs_full_shadow_root=args.m49_tgs_full_shadow_root,
|
||||||
|
valid_fov_mask_path=args.valid_fov_mask_path,
|
||||||
)
|
)
|
||||||
print(destination)
|
print(destination)
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -138,7 +138,6 @@ from k1link.web.m49_physical_safety_playback_api import (
|
|||||||
)
|
)
|
||||||
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
||||||
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
|
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
|
||||||
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
|
|
||||||
from k1link.web.map_api import (
|
from k1link.web.map_api import (
|
||||||
MapGatewayConfiguration,
|
MapGatewayConfiguration,
|
||||||
MapGatewayProxy,
|
MapGatewayProxy,
|
||||||
@@ -165,6 +164,10 @@ from k1link.web.session_api import build_session_router
|
|||||||
from k1link.web.simulation_projects_api import build_simulation_projects_router
|
from k1link.web.simulation_projects_api import build_simulation_projects_router
|
||||||
from k1link.web.simulation_world_provider_api import build_simulation_world_provider_router
|
from k1link.web.simulation_world_provider_api import build_simulation_world_provider_router
|
||||||
from k1link.web.system_telemetry_api import build_system_telemetry_router
|
from k1link.web.system_telemetry_api import build_system_telemetry_router
|
||||||
|
from k1link.web.vegetation_shadow_lab_api import (
|
||||||
|
build_vegetation_benchmark_lab_router,
|
||||||
|
build_vegetation_shadow_lab_router,
|
||||||
|
)
|
||||||
from k1link.web.viewer_diagnostics_api import build_viewer_diagnostics_router
|
from k1link.web.viewer_diagnostics_api import build_viewer_diagnostics_router
|
||||||
|
|
||||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
|
||||||
@@ -1031,6 +1034,17 @@ app.include_router(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
app.include_router(
|
||||||
|
build_vegetation_benchmark_lab_router(
|
||||||
|
root_provider=lambda: (
|
||||||
|
REPOSITORY_ROOT
|
||||||
|
/ ".runtime"
|
||||||
|
/ "compute-experiments"
|
||||||
|
/ "lab-v1-vegetation-benchmark"
|
||||||
|
/ "results"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
build_m49_physical_safety_playback_router(
|
build_m49_physical_safety_playback_router(
|
||||||
root_provider=lambda: (
|
root_provider=lambda: (
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
import copy
|
import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
@@ -20,10 +19,9 @@ from k1link.laboratory.evidence_report import (
|
|||||||
LaboratoryEvidenceReportError,
|
LaboratoryEvidenceReportError,
|
||||||
verify_laboratory_evidence_result,
|
verify_laboratory_evidence_result,
|
||||||
)
|
)
|
||||||
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA, RESULT_PREFIX
|
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA
|
||||||
|
|
||||||
RootProvider = Callable[[], Path | None]
|
RootProvider = Callable[[], Path | None]
|
||||||
RESULT_ID: Final = re.compile(rf"^{re.escape(RESULT_PREFIX)}[a-f0-9]{{64}}$")
|
|
||||||
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
|
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
|
||||||
_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||||
work_id="lab-v1-vegetation-shadow",
|
work_id="lab-v1-vegetation-shadow",
|
||||||
@@ -32,25 +30,55 @@ _DEFINITION: Final = LaboratoryEvidenceDefinition(
|
|||||||
document_name="result.json",
|
document_name="result.json",
|
||||||
result_schema_version=LAB_SCHEMA,
|
result_schema_version=LAB_SCHEMA,
|
||||||
)
|
)
|
||||||
|
_BENCHMARK_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||||
|
work_id="lab-v1-vegetation-benchmark",
|
||||||
|
runtime_relative_root=PurePosixPath("lab-v1-vegetation-benchmark/results"),
|
||||||
|
result_id_prefix="lab-v1-vegetation-benchmark",
|
||||||
|
document_name="result.json",
|
||||||
|
result_schema_version=LAB_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_vegetation_shadow_lab_router(
|
def build_vegetation_shadow_lab_router(
|
||||||
*, root_provider: RootProvider = lambda: None,
|
*, root_provider: RootProvider = lambda: None,
|
||||||
) -> APIRouter:
|
) -> APIRouter:
|
||||||
router = APIRouter(
|
return _build_vegetation_lab_router(
|
||||||
prefix="/api/v1/laboratory/vegetation-shadow",
|
prefix="/api/v1/laboratory/vegetation-shadow",
|
||||||
|
definition=_DEFINITION,
|
||||||
|
root_provider=root_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_vegetation_benchmark_lab_router(
|
||||||
|
*, root_provider: RootProvider = lambda: None,
|
||||||
|
) -> APIRouter:
|
||||||
|
return _build_vegetation_lab_router(
|
||||||
|
prefix="/api/v1/laboratory/vegetation-benchmark",
|
||||||
|
definition=_BENCHMARK_DEFINITION,
|
||||||
|
root_provider=root_provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_vegetation_lab_router(
|
||||||
|
*,
|
||||||
|
prefix: str,
|
||||||
|
definition: LaboratoryEvidenceDefinition,
|
||||||
|
root_provider: RootProvider,
|
||||||
|
) -> APIRouter:
|
||||||
|
router = APIRouter(
|
||||||
|
prefix=prefix,
|
||||||
tags=["laboratory"],
|
tags=["laboratory"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/{result_id}")
|
@router.get("/{result_id}")
|
||||||
def get_result(result_id: str) -> dict[str, object]:
|
def get_result(result_id: str) -> dict[str, object]:
|
||||||
candidate = _resolve_candidate(root_provider, result_id)
|
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||||
return {**copy.deepcopy(_read_verified(candidate)), "access": "read-only"}
|
return {**copy.deepcopy(_read_verified(candidate, definition)), "access": "read-only"}
|
||||||
|
|
||||||
@router.get("/{result_id}/assets/{asset_path:path}")
|
@router.get("/{result_id}/assets/{asset_path:path}")
|
||||||
def get_asset(result_id: str, asset_path: str) -> FileResponse:
|
def get_asset(result_id: str, asset_path: str) -> FileResponse:
|
||||||
candidate = _resolve_candidate(root_provider, result_id)
|
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||||
manifest = _read_verified(candidate)
|
manifest = _read_verified(candidate, definition)
|
||||||
artifacts = manifest.get("artifacts")
|
artifacts = manifest.get("artifacts")
|
||||||
if not isinstance(artifacts, list):
|
if not isinstance(artifacts, list):
|
||||||
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
|
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
|
||||||
@@ -90,8 +118,8 @@ def build_vegetation_shadow_lab_router(
|
|||||||
|
|
||||||
@router.get("/{result_id}/masks/{sequence}")
|
@router.get("/{result_id}/masks/{sequence}")
|
||||||
def get_video_mask(result_id: str, sequence: int) -> Response:
|
def get_video_mask(result_id: str, sequence: int) -> Response:
|
||||||
candidate = _resolve_candidate(root_provider, result_id)
|
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||||
manifest = _read_verified(candidate)
|
manifest = _read_verified(candidate, definition)
|
||||||
route_video = manifest.get("route_video")
|
route_video = manifest.get("route_video")
|
||||||
if (
|
if (
|
||||||
not isinstance(route_video, dict)
|
not isinstance(route_video, dict)
|
||||||
@@ -168,9 +196,13 @@ def _configured_root(provider: RootProvider) -> Path | None:
|
|||||||
return root if root.is_dir() else None
|
return root if root.is_dir() else None
|
||||||
|
|
||||||
|
|
||||||
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
|
def _resolve_candidate(
|
||||||
|
provider: RootProvider,
|
||||||
|
definition: LaboratoryEvidenceDefinition,
|
||||||
|
result_id: str,
|
||||||
|
) -> Path:
|
||||||
root = _configured_root(provider)
|
root = _configured_root(provider)
|
||||||
if root is None or RESULT_ID.fullmatch(result_id) is None:
|
if root is None or definition.result_id_pattern.fullmatch(result_id) is None:
|
||||||
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
|
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
|
||||||
candidate = root / result_id
|
candidate = root / result_id
|
||||||
if candidate.is_symlink():
|
if candidate.is_symlink():
|
||||||
@@ -184,7 +216,10 @@ def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
|
|||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
def _read_verified(candidate: Path) -> dict[str, Any]:
|
def _read_verified(
|
||||||
|
candidate: Path,
|
||||||
|
definition: LaboratoryEvidenceDefinition,
|
||||||
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
rows: list[tuple[str, int, int, int, int]] = []
|
rows: list[tuple[str, int, int, int, int]] = []
|
||||||
for path in sorted(candidate.rglob("*"), key=lambda item: item.as_posix()):
|
for path in sorted(candidate.rglob("*"), key=lambda item: item.as_posix()):
|
||||||
@@ -202,19 +237,39 @@ def _read_verified(candidate: Path) -> dict[str, Any]:
|
|||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Vegetation LAB evidence failed verification",
|
detail="Vegetation LAB evidence failed verification",
|
||||||
) from None
|
) from None
|
||||||
return _read_verified_cached(str(candidate), signature)
|
return _read_verified_cached(
|
||||||
|
str(candidate),
|
||||||
|
signature,
|
||||||
|
definition.work_id,
|
||||||
|
str(definition.runtime_relative_root),
|
||||||
|
definition.result_id_prefix,
|
||||||
|
definition.document_name,
|
||||||
|
definition.result_schema_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=16)
|
@lru_cache(maxsize=16)
|
||||||
def _read_verified_cached(
|
def _read_verified_cached(
|
||||||
candidate_text: str,
|
candidate_text: str,
|
||||||
signature: tuple[tuple[str, int, int, int, int], ...],
|
signature: tuple[tuple[str, int, int, int, int], ...],
|
||||||
|
work_id: str,
|
||||||
|
runtime_relative_root: str,
|
||||||
|
result_id_prefix: str,
|
||||||
|
document_name: str,
|
||||||
|
result_schema_version: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
del signature
|
del signature
|
||||||
candidate = Path(candidate_text)
|
candidate = Path(candidate_text)
|
||||||
|
definition = LaboratoryEvidenceDefinition(
|
||||||
|
work_id=work_id,
|
||||||
|
runtime_relative_root=PurePosixPath(runtime_relative_root),
|
||||||
|
result_id_prefix=result_id_prefix,
|
||||||
|
document_name=document_name,
|
||||||
|
result_schema_version=result_schema_version,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
verify_laboratory_evidence_result(_DEFINITION, candidate)
|
verify_laboratory_evidence_result(definition, candidate)
|
||||||
path = candidate / "result.json"
|
path = candidate / definition.document_name
|
||||||
if path.stat().st_size > _MAX_DOCUMENT_BYTES:
|
if path.stat().st_size > _MAX_DOCUMENT_BYTES:
|
||||||
raise LaboratoryEvidenceReportError("Vegetation LAB document is too large")
|
raise LaboratoryEvidenceReportError("Vegetation LAB document is too large")
|
||||||
payload = json.loads(path.read_text("utf-8"))
|
payload = json.loads(path.read_text("utf-8"))
|
||||||
@@ -228,4 +283,7 @@ def _read_verified_cached(
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["build_vegetation_shadow_lab_router"]
|
__all__ = [
|
||||||
|
"build_vegetation_benchmark_lab_router",
|
||||||
|
"build_vegetation_shadow_lab_router",
|
||||||
|
]
|
||||||
|
|||||||
@@ -127,9 +127,10 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
|||||||
repository_root / "config" / "laboratories"
|
repository_root / "config" / "laboratories"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(registry.definitions) == 43
|
assert len(registry.definitions) == 44
|
||||||
assert {item.work_id for item in registry.definitions} >= {
|
assert {item.work_id for item in registry.definitions} >= {
|
||||||
"lab-v1-vegetation-shadow",
|
"lab-v1-vegetation-shadow",
|
||||||
|
"lab-v1-vegetation-benchmark",
|
||||||
"e31-source-binding",
|
"e31-source-binding",
|
||||||
"e46j-raw-fisheye-realtime",
|
"e46j-raw-fisheye-realtime",
|
||||||
"e47-semantic-slam-shadow",
|
"e47-semantic-slam-shadow",
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
|||||||
root / "config" / "laboratory-value-review.json"
|
root / "config" / "laboratory-value-review.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(registry.entries) == 41
|
assert len(registry.entries) == 42
|
||||||
assert {entry.catalog_id for entry in registry.entries} >= {
|
assert {entry.catalog_id for entry in registry.entries} >= {
|
||||||
"e28-local-surface",
|
"e28-local-surface",
|
||||||
"e46d-temporal-failure-audit",
|
"e46d-temporal-failure-audit",
|
||||||
@@ -94,4 +94,5 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
|||||||
"m49-tgs-fail-closed-evidence",
|
"m49-tgs-fail-closed-evidence",
|
||||||
"m49-tgs-full-shadow",
|
"m49-tgs-full-shadow",
|
||||||
"lab-v1-vegetation-shadow",
|
"lab-v1-vegetation-shadow",
|
||||||
|
"lab-v1-vegetation-benchmark",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
import k1link.laboratory.vegetation_policy_review as policy_review_module
|
import k1link.laboratory.vegetation_policy_review as policy_review_module
|
||||||
|
import k1link.laboratory.vegetation_policy_video as policy_video_module
|
||||||
import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module
|
import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module
|
||||||
from k1link.laboratory import LaboratoryEvidenceRegistry
|
from k1link.laboratory import LaboratoryEvidenceRegistry
|
||||||
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||||
@@ -21,6 +25,45 @@ from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_rou
|
|||||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_coarse_policy_masks_mark_every_outside_fov_pixel_undefined(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(policy_video_module, "FRAME_COUNT", 1)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
policy_video_module,
|
||||||
|
"fine_to_policy_lut",
|
||||||
|
lambda _taxonomy, _provider_map: np.full(256, 4, dtype=np.uint8),
|
||||||
|
)
|
||||||
|
source = tmp_path / "fine.zip"
|
||||||
|
fine_buffer = io.BytesIO()
|
||||||
|
Image.new("L", (800, 600), color=1).save(fine_buffer, format="PNG")
|
||||||
|
with zipfile.ZipFile(source, "w") as archive:
|
||||||
|
archive.writestr("masks/frame-000001.png", fine_buffer.getvalue())
|
||||||
|
|
||||||
|
valid_fov = np.zeros((600, 800), dtype=np.uint8)
|
||||||
|
valid_fov[:, :400] = 255
|
||||||
|
valid_fov_path = tmp_path / "valid-fov.png"
|
||||||
|
Image.fromarray(valid_fov, mode="L").save(valid_fov_path)
|
||||||
|
destination = tmp_path / "coarse.zip"
|
||||||
|
counts = policy_video_module.build_policy_mask_archive(
|
||||||
|
source_archive=source,
|
||||||
|
destination_archive=destination,
|
||||||
|
fine_taxonomy={},
|
||||||
|
provider_label_map={},
|
||||||
|
valid_fov_mask=valid_fov_path,
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
zipfile.ZipFile(destination) as archive,
|
||||||
|
Image.open(io.BytesIO(archive.read("masks/frame-000001.png"))) as image,
|
||||||
|
):
|
||||||
|
coarse = np.asarray(image.convert("L"))
|
||||||
|
assert np.all(coarse[:, :400] == 4)
|
||||||
|
assert np.all(coarse[:, 400:] == 9)
|
||||||
|
assert counts[4] == 600 * 400
|
||||||
|
assert counts[9] == 600 * 400
|
||||||
|
|
||||||
|
|
||||||
def _sha256(path: Path) -> str:
|
def _sha256(path: Path) -> str:
|
||||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
|
||||||
@@ -290,9 +333,12 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
|
|||||||
|
|
||||||
def fake_policy_archive(**kwargs) -> list[int]:
|
def fake_policy_archive(**kwargs) -> list[int]:
|
||||||
shutil.copyfile(kwargs["source_archive"], kwargs["destination_archive"])
|
shutil.copyfile(kwargs["source_archive"], kwargs["destination_archive"])
|
||||||
return [4489 * 800 * 600, *([0] * 8)]
|
assert kwargs["valid_fov_mask"].is_file()
|
||||||
|
return [4489 * 800 * 600, *([0] * 9)]
|
||||||
|
|
||||||
monkeypatch.setattr(policy_review_module, "build_policy_mask_archive", fake_policy_archive)
|
monkeypatch.setattr(policy_review_module, "build_policy_mask_archive", fake_policy_archive)
|
||||||
|
valid_fov_mask = tmp_path / "valid-fov-mask.png"
|
||||||
|
Image.new("L", (800, 600), color=255).save(valid_fov_mask)
|
||||||
result_root = seal_vegetation_policy_review(
|
result_root = seal_vegetation_policy_review(
|
||||||
base_lab_root=base_root,
|
base_lab_root=base_root,
|
||||||
mission_policy_path=REPOSITORY_ROOT
|
mission_policy_path=REPOSITORY_ROOT
|
||||||
@@ -300,6 +346,7 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
|
|||||||
provider_label_map_path=REPOSITORY_ROOT
|
provider_label_map_path=REPOSITORY_ROOT
|
||||||
/ "config/perception/lab-v1-vegetation-provider-label-map-v1.json",
|
/ "config/perception/lab-v1-vegetation-provider-label-map-v1.json",
|
||||||
m49_tgs_full_shadow_root=tmp_path / "sealed-tgs",
|
m49_tgs_full_shadow_root=tmp_path / "sealed-tgs",
|
||||||
|
valid_fov_mask_path=valid_fov_mask,
|
||||||
output_root=tmp_path / "results",
|
output_root=tmp_path / "results",
|
||||||
created_at_utc="2026-08-28T08:00:00+00:00",
|
created_at_utc="2026-08-28T08:00:00+00:00",
|
||||||
)
|
)
|
||||||
@@ -312,8 +359,9 @@ def test_policy_review_reuses_sealed_video_and_links_yolox_tgs(
|
|||||||
assert route["taxonomy"]["schema_version"] == (
|
assert route["taxonomy"]["schema_version"] == (
|
||||||
"missioncore.lab-v1-terrain-policy-taxonomy/v1"
|
"missioncore.lab-v1-terrain-policy-taxonomy/v1"
|
||||||
)
|
)
|
||||||
assert len(route["taxonomy"]["classes"]) == 9
|
assert len(route["taxonomy"]["classes"]) == 10
|
||||||
assert len(manifest["artifacts"]) == 79
|
assert route["valid_fov"]["outside_valid_fov_class_id"] == 9
|
||||||
|
assert len(manifest["artifacts"]) == 80
|
||||||
assert manifest["authority"]["commands_enabled"] is False
|
assert manifest["authority"]["commands_enabled"] is False
|
||||||
assert manifest["decision"]["multilayer_policy_review_ready"] is True
|
assert manifest["decision"]["multilayer_policy_review_ready"] is True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user