feat(perception): add DDRNet full-video vegetation replay
This commit is contained in:
@@ -51,6 +51,26 @@ export interface VegetationVisualCase {
|
|||||||
assets: Readonly<Record<string, string>>;
|
assets: Readonly<Record<string, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VegetationVideoSemanticClass {
|
||||||
|
classId: number;
|
||||||
|
label: string;
|
||||||
|
colorRgb: readonly [number, number, number];
|
||||||
|
disposition: "prediction" | "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VegetationRouteVideo {
|
||||||
|
workerResultId: string;
|
||||||
|
m47ReferenceGraphResultId: string;
|
||||||
|
baseM4ResultId: string;
|
||||||
|
frameCount: 4489;
|
||||||
|
width: 800;
|
||||||
|
height: 600;
|
||||||
|
centerCropXyxy: readonly [100, 0, 700, 600];
|
||||||
|
outsideCropState: "undefined";
|
||||||
|
taxonomy: readonly VegetationVideoSemanticClass[];
|
||||||
|
aggregatePredictionPixels: readonly number[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface VegetationShadowResult {
|
export interface VegetationShadowResult {
|
||||||
resultId: string;
|
resultId: string;
|
||||||
createdAtUtc: string;
|
createdAtUtc: string;
|
||||||
@@ -59,6 +79,7 @@ export interface VegetationShadowResult {
|
|||||||
candidates: readonly VegetationCandidateMetrics[];
|
candidates: readonly VegetationCandidateMetrics[];
|
||||||
routeCases: readonly VegetationVisualCase[];
|
routeCases: readonly VegetationVisualCase[];
|
||||||
validationCases: readonly VegetationVisualCase[];
|
validationCases: readonly VegetationVisualCase[];
|
||||||
|
routeVideo: VegetationRouteVideo | null;
|
||||||
limitations: readonly string[];
|
limitations: readonly string[];
|
||||||
visualShadowReady: true;
|
visualShadowReady: true;
|
||||||
missionPolicyReadyForConfiguration: true;
|
missionPolicyReadyForConfiguration: true;
|
||||||
@@ -227,6 +248,98 @@ function visualCaseValue(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeVideoValue(value: unknown): VegetationRouteVideo | null {
|
||||||
|
if (value === null || value === undefined) return null;
|
||||||
|
const row = objectValue(value, "vegetation.route_video");
|
||||||
|
const workerResultId = textValue(row.worker_result_id, "vegetation.route_video.worker_result_id");
|
||||||
|
const m47ReferenceGraphResultId = textValue(
|
||||||
|
row.m47_reference_graph_result_id,
|
||||||
|
"vegetation.route_video.m47_reference_graph_result_id",
|
||||||
|
);
|
||||||
|
const baseM4ResultId = textValue(row.base_m4_result_id, "vegetation.route_video.base_m4_result_id");
|
||||||
|
if (
|
||||||
|
!/^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$/.test(workerResultId)
|
||||||
|
|| !/^m47-reference-graph-lab-[a-f0-9]{64}$/.test(m47ReferenceGraphResultId)
|
||||||
|
|| !/^m4-threat-replay-[a-f0-9]{64}$/.test(baseM4ResultId)
|
||||||
|
) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: identity invalid.");
|
||||||
|
}
|
||||||
|
exact(row.frame_count, 4489, "vegetation.route_video.frame_count");
|
||||||
|
exact(row.width, 800, "vegetation.route_video.width");
|
||||||
|
exact(row.height, 600, "vegetation.route_video.height");
|
||||||
|
exact(row.outside_crop_state, "undefined", "vegetation.route_video.outside_crop_state");
|
||||||
|
exact(
|
||||||
|
row.sequence_binding,
|
||||||
|
"sequence-0-to-masks/frame-000001.png",
|
||||||
|
"vegetation.route_video.sequence_binding",
|
||||||
|
);
|
||||||
|
const crop = arrayValue(row.center_crop_xyxy, "vegetation.route_video.center_crop_xyxy")
|
||||||
|
.map((item, index) => integerValue(item, `vegetation.route_video.crop[${index}]`));
|
||||||
|
if (crop.join(",") !== "100,0,700,600") {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: crop contract changed.");
|
||||||
|
}
|
||||||
|
const taxonomy = objectValue(row.taxonomy, "vegetation.route_video.taxonomy");
|
||||||
|
exact(
|
||||||
|
taxonomy.schema_version,
|
||||||
|
"missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||||
|
"vegetation.route_video.taxonomy.schema",
|
||||||
|
);
|
||||||
|
const classes = arrayValue(taxonomy.classes, "vegetation.route_video.taxonomy.classes")
|
||||||
|
.map((value, expectedId): VegetationVideoSemanticClass => {
|
||||||
|
const item = objectValue(value, `vegetation.route_video.taxonomy[${expectedId}]`);
|
||||||
|
const classId = integerValue(item.class_id, `vegetation.route_video.class_id[${expectedId}]`);
|
||||||
|
if (classId !== expectedId) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: taxonomy order changed.");
|
||||||
|
}
|
||||||
|
const color = arrayValue(item.color_rgb, `vegetation.route_video.color[${expectedId}]`)
|
||||||
|
.map((channel, index) => integerValue(channel, `vegetation.route_video.color[${expectedId}][${index}]`));
|
||||||
|
if (color.length !== 3 || color.some((channel) => channel > 255)) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: taxonomy color invalid.");
|
||||||
|
}
|
||||||
|
const disposition: VegetationVideoSemanticClass["disposition"] = expectedId === 0
|
||||||
|
? "undefined"
|
||||||
|
: "prediction";
|
||||||
|
if (item.disposition !== disposition) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: taxonomy disposition changed.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
classId,
|
||||||
|
label: textValue(item.label, `vegetation.route_video.label[${expectedId}]`),
|
||||||
|
colorRgb: color as unknown as readonly [number, number, number],
|
||||||
|
disposition,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (classes.length !== 64) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: taxonomy must contain 64 classes.");
|
||||||
|
}
|
||||||
|
const aggregatePredictionPixels = arrayValue(
|
||||||
|
row.aggregate_prediction_pixels,
|
||||||
|
"vegetation.route_video.aggregate_prediction_pixels",
|
||||||
|
).map((value, index) => integerValue(value, `vegetation.route_video.pixels[${index}]`));
|
||||||
|
if (aggregatePredictionPixels.length !== 64) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: class accounting changed.");
|
||||||
|
}
|
||||||
|
const maskArchive = objectValue(row.mask_archive, "vegetation.route_video.mask_archive");
|
||||||
|
exact(maskArchive.path, "video/ddrnet-semantic-masks.zip", "vegetation.route_video.mask_archive.path");
|
||||||
|
const archiveSha256 = textValue(maskArchive.sha256, "vegetation.route_video.mask_archive.sha256");
|
||||||
|
if (!SHA256.test(archiveSha256)) {
|
||||||
|
throw new VegetationShadowContractError("vegetation.route_video: archive digest invalid.");
|
||||||
|
}
|
||||||
|
integerValue(maskArchive.byte_length, "vegetation.route_video.mask_archive.byte_length");
|
||||||
|
return {
|
||||||
|
workerResultId,
|
||||||
|
m47ReferenceGraphResultId,
|
||||||
|
baseM4ResultId,
|
||||||
|
frameCount: 4489,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
centerCropXyxy: [100, 0, 700, 600],
|
||||||
|
outsideCropState: "undefined",
|
||||||
|
taxonomy: classes,
|
||||||
|
aggregatePredictionPixels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
function parseResult(value: unknown, resultId: 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");
|
||||||
@@ -277,6 +390,7 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
|||||||
candidates: CANDIDATES.map((candidate) => candidateMetricsValue(candidates[candidate], candidate)),
|
candidates: CANDIDATES.map((candidate) => candidateMetricsValue(candidates[candidate], candidate)),
|
||||||
routeCases,
|
routeCases,
|
||||||
validationCases,
|
validationCases,
|
||||||
|
routeVideo: routeVideoValue(payload.route_video),
|
||||||
limitations: arrayValue(payload.limitations, "vegetation.limitations")
|
limitations: arrayValue(payload.limitations, "vegetation.limitations")
|
||||||
.map((item, index) => textValue(item, `vegetation.limitations[${index}]`)),
|
.map((item, index) => textValue(item, `vegetation.limitations[${index}]`)),
|
||||||
visualShadowReady: true,
|
visualShadowReady: true,
|
||||||
@@ -290,6 +404,13 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function vegetationVideoMaskUrl(resultId: string, sequence: number): string {
|
||||||
|
if (!RESULT_ID.test(resultId) || !Number.isInteger(sequence) || sequence < 0 || sequence >= 4489) {
|
||||||
|
throw new VegetationShadowContractError("Vegetation video mask identity недопустима.");
|
||||||
|
}
|
||||||
|
return `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/masks/${sequence}`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchVegetationShadowResult(
|
export async function fetchVegetationShadowResult(
|
||||||
resultId: string,
|
resultId: string,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -97,7 +97,16 @@ function SpatialState({ message: text }: { message: string }) {
|
|||||||
|
|
||||||
export interface M4ReplayThreatSemanticLayer {
|
export interface M4ReplayThreatSemanticLayer {
|
||||||
resultId: string;
|
resultId: string;
|
||||||
taxonomy: readonly E47SemanticClass[];
|
spatialResultId?: string | null;
|
||||||
|
maskUrl?: (sequence: number) => string;
|
||||||
|
label?: string;
|
||||||
|
maskAriaLabel?: string;
|
||||||
|
taxonomy: readonly {
|
||||||
|
classId: number;
|
||||||
|
label: string;
|
||||||
|
disposition: "labeled" | "ambiguous" | "prediction" | "undefined";
|
||||||
|
colorRgb: readonly [number, number, number];
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface M4ReplayThreatReviewAnchor {
|
export interface M4ReplayThreatReviewAnchor {
|
||||||
@@ -153,6 +162,8 @@ export function M4ReplayThreatVisual({
|
|||||||
evidenceLabel = "M4.6",
|
evidenceLabel = "M4.6",
|
||||||
initialSpatialMode = null,
|
initialSpatialMode = null,
|
||||||
classifiedSpatialLayer,
|
classifiedSpatialLayer,
|
||||||
|
showReferenceMediaLayers = true,
|
||||||
|
showSpatialOverlaySummary = true,
|
||||||
onActiveSequenceChange,
|
onActiveSequenceChange,
|
||||||
}: {
|
}: {
|
||||||
resultId: string;
|
resultId: string;
|
||||||
@@ -164,6 +175,8 @@ export function M4ReplayThreatVisual({
|
|||||||
evidenceLabel?: string;
|
evidenceLabel?: string;
|
||||||
initialSpatialMode?: LaboratoryMetricSceneMode | null;
|
initialSpatialMode?: LaboratoryMetricSceneMode | null;
|
||||||
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
|
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
|
||||||
|
showReferenceMediaLayers?: boolean;
|
||||||
|
showSpatialOverlaySummary?: boolean;
|
||||||
onActiveSequenceChange?: (sequence: number | null) => void;
|
onActiveSequenceChange?: (sequence: number | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
|
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
|
||||||
@@ -280,16 +293,30 @@ export function M4ReplayThreatVisual({
|
|||||||
? lastSpatialFrameRef.current.frame
|
? lastSpatialFrameRef.current.frame
|
||||||
: null;
|
: null;
|
||||||
const cameraPointOverlay = useM4ThreatCameraPointOverlay({
|
const cameraPointOverlay = useM4ThreatCameraPointOverlay({
|
||||||
enabled: showMediaPoints,
|
enabled: showReferenceMediaLayers && showMediaPoints,
|
||||||
resultId,
|
resultId,
|
||||||
sequence: frame?.sequence ?? null,
|
sequence: frame?.sequence ?? null,
|
||||||
endpointRoot: timelineEndpointRoot,
|
endpointRoot: timelineEndpointRoot,
|
||||||
});
|
});
|
||||||
|
const semanticSpatialResultId = semantic
|
||||||
|
? semantic.spatialResultId === undefined ? semantic.resultId : semantic.spatialResultId
|
||||||
|
: null;
|
||||||
|
const spatialSemanticTaxonomy = useMemo<readonly E47SemanticClass[]>(
|
||||||
|
() => semanticSpatialResultId && semantic
|
||||||
|
? semantic.taxonomy.map((item) => ({
|
||||||
|
classId: item.classId,
|
||||||
|
label: item.label,
|
||||||
|
disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled",
|
||||||
|
colorRgb: item.colorRgb,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
[semantic, semanticSpatialResultId],
|
||||||
|
);
|
||||||
const semanticTimeline = useE47SemanticTimelineFrame({
|
const semanticTimeline = useE47SemanticTimelineFrame({
|
||||||
resultId: semantic?.resultId ?? null,
|
resultId: semanticSpatialResultId,
|
||||||
activeSequence: frame?.sequence ?? timelineFrame.activeSequence,
|
activeSequence: frame?.sequence ?? timelineFrame.activeSequence,
|
||||||
frameCount: metadata.timeline?.frameCount ?? 0,
|
frameCount: metadata.timeline?.frameCount ?? 0,
|
||||||
taxonomy: semantic?.taxonomy ?? [],
|
taxonomy: spatialSemanticTaxonomy,
|
||||||
});
|
});
|
||||||
const displayingBufferedFrame = Boolean(
|
const displayingBufferedFrame = Boolean(
|
||||||
frame
|
frame
|
||||||
@@ -339,7 +366,8 @@ export function M4ReplayThreatVisual({
|
|||||||
const staticObstacleBoxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
|
const staticObstacleBoxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
|
||||||
const timeline = metadata.timeline;
|
const timeline = metadata.timeline;
|
||||||
if (
|
if (
|
||||||
!frame
|
!showReferenceMediaLayers
|
||||||
|
|| !frame
|
||||||
|| !timeline?.cameraObstacleProjectionDelivery
|
|| !timeline?.cameraObstacleProjectionDelivery
|
||||||
|| !showStaticObstacles
|
|| !showStaticObstacles
|
||||||
) return [];
|
) return [];
|
||||||
@@ -348,14 +376,14 @@ export function M4ReplayThreatVisual({
|
|||||||
timeline.imageWidth,
|
timeline.imageWidth,
|
||||||
timeline.imageHeight,
|
timeline.imageHeight,
|
||||||
);
|
);
|
||||||
}, [frame, metadata.timeline, showStaticObstacles]);
|
}, [frame, metadata.timeline, showReferenceMediaLayers, showStaticObstacles]);
|
||||||
const activeBoxes = useMemo(
|
const activeBoxes = useMemo(
|
||||||
() => classifiedSpatialLayer ? [] : [
|
() => classifiedSpatialLayer || !showReferenceMediaLayers ? [] : [
|
||||||
...boxes(frame?.cameraProposals ?? []),
|
...boxes(frame?.cameraProposals ?? []),
|
||||||
...staticObstacleBoxes,
|
...staticObstacleBoxes,
|
||||||
...reviewAnchorBoxes,
|
...reviewAnchorBoxes,
|
||||||
],
|
],
|
||||||
[classifiedSpatialLayer, frame, reviewAnchorBoxes, staticObstacleBoxes],
|
[classifiedSpatialLayer, frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes],
|
||||||
);
|
);
|
||||||
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||||
() => semantic?.taxonomy.map((item) => ({
|
() => semantic?.taxonomy.map((item) => ({
|
||||||
@@ -367,10 +395,14 @@ export function M4ReplayThreatVisual({
|
|||||||
const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
|
const semanticPalette = useMemo<readonly RecordedEvidenceSemanticPaletteEntry[]>(
|
||||||
() => semantic?.taxonomy.map((item) => ({
|
() => semantic?.taxonomy.map((item) => ({
|
||||||
classId: item.classId,
|
classId: item.classId,
|
||||||
color: item.disposition === "ambiguous"
|
color: item.disposition === "undefined"
|
||||||
|
? { kind: "transparent" as const }
|
||||||
|
: item.disposition === "ambiguous"
|
||||||
? { kind: "token" as const, token: "--nodedc-warning-rgb" as const }
|
? { kind: "token" as const, token: "--nodedc-warning-rgb" as const }
|
||||||
: { kind: "diagnostic" as const, rgb: item.colorRgb },
|
: { kind: "diagnostic" as const, rgb: item.colorRgb },
|
||||||
opacity: item.disposition === "ambiguous" ? 0.52 : 0.92,
|
opacity: item.disposition === "undefined"
|
||||||
|
? 0
|
||||||
|
: item.disposition === "ambiguous" ? 0.52 : 0.92,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
[semantic?.taxonomy],
|
[semantic?.taxonomy],
|
||||||
);
|
);
|
||||||
@@ -580,15 +612,17 @@ export function M4ReplayThreatVisual({
|
|||||||
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
|
const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined =
|
||||||
semantic && showMediaSemantic && frame
|
semantic && showMediaSemantic && frame
|
||||||
? {
|
? {
|
||||||
src: e47SemanticMaskUrl(semantic.resultId, frame.sequence),
|
src: semantic.maskUrl?.(frame.sequence)
|
||||||
|
?? e47SemanticMaskUrl(semantic.resultId, frame.sequence),
|
||||||
prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1)
|
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) => e47SemanticMaskUrl(semantic.resultId, sequence)),
|
.map((sequence) => semantic.maskUrl?.(sequence)
|
||||||
|
?? e47SemanticMaskUrl(semantic.resultId, sequence)),
|
||||||
classes: semanticClasses,
|
classes: semanticClasses,
|
||||||
palette: semanticPalette,
|
palette: semanticPalette,
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
ariaLabel: `E47 semantic mask frame ${frame.sequence + 1}`,
|
ariaLabel: `${semantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence
|
const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence
|
||||||
@@ -659,8 +693,8 @@ export function M4ReplayThreatVisual({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mediaLayerControls = semantic
|
const mediaLayerControls = semantic
|
||||||
|| metadata.timeline?.cameraPointDelivery
|
|| (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery)
|
||||||
|| metadata.timeline?.cameraObstacleProjectionDelivery ? (
|
|| (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
|
||||||
<div
|
<div
|
||||||
className="m4-replay-threat-visual__pane-layer-controls"
|
className="m4-replay-threat-visual__pane-layer-controls"
|
||||||
role="group"
|
role="group"
|
||||||
@@ -677,7 +711,7 @@ export function M4ReplayThreatVisual({
|
|||||||
SEMANTICS
|
SEMANTICS
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{metadata.timeline?.cameraPointDelivery ? (
|
{showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery ? (
|
||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
shape="pill"
|
shape="pill"
|
||||||
@@ -689,7 +723,7 @@ export function M4ReplayThreatVisual({
|
|||||||
POINTS
|
POINTS
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{metadata.timeline?.cameraObstacleProjectionDelivery ? (
|
{showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery ? (
|
||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
shape="pill"
|
shape="pill"
|
||||||
@@ -738,7 +772,7 @@ export function M4ReplayThreatVisual({
|
|||||||
>
|
>
|
||||||
{classifiedSpatialLayer.cellLayerLabel}
|
{classifiedSpatialLayer.cellLayerLabel}
|
||||||
</Button>
|
</Button>
|
||||||
{semantic ? (
|
{semanticSpatialResultId ? (
|
||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
shape="pill"
|
shape="pill"
|
||||||
@@ -796,7 +830,7 @@ export function M4ReplayThreatVisual({
|
|||||||
LOW-STEP
|
LOW-STEP
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{semantic ? (
|
{semanticSpatialResultId ? (
|
||||||
<Button
|
<Button
|
||||||
size="compact"
|
size="compact"
|
||||||
shape="pill"
|
shape="pill"
|
||||||
@@ -901,9 +935,11 @@ export function M4ReplayThreatVisual({
|
|||||||
: playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
|
: playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{showSpatialOverlaySummary ? (
|
||||||
<span>Spatial evidence</span>
|
<>
|
||||||
<strong>{classifiedSpatialLayer
|
<div>
|
||||||
|
<span>Spatial evidence</span>
|
||||||
|
<strong>{classifiedSpatialLayer
|
||||||
? classifiedSpatialFrame
|
? classifiedSpatialFrame
|
||||||
? replaceClassifiedPointCloud
|
? replaceClassifiedPointCloud
|
||||||
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedCellCount.toLocaleString("ru-RU")} cells`
|
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedCellCount.toLocaleString("ru-RU")} cells`
|
||||||
@@ -939,15 +975,15 @@ export function M4ReplayThreatVisual({
|
|||||||
: showMediaPoints && cameraPointOverlay.error
|
: showMediaPoints && cameraPointOverlay.error
|
||||||
? " · накопленное camera cloud недоступно"
|
? " · накопленное camera cloud недоступно"
|
||||||
: ""}
|
: ""}
|
||||||
{semantic && spatialSemanticFrame
|
{semanticSpatialResultId && spatialSemanticFrame
|
||||||
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
||||||
: semantic ? " · semantic buffer" : ""}
|
: semanticSpatialResultId ? " · semantic buffer" : ""}
|
||||||
</>
|
</>
|
||||||
)}</small>
|
)}</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
|
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
|
||||||
<strong>{classifiedSpatialLayer
|
<strong>{classifiedSpatialLayer
|
||||||
? classifiedSpatialFrame
|
? classifiedSpatialFrame
|
||||||
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
|
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
|
||||||
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
|
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
|
||||||
@@ -957,7 +993,9 @@ export function M4ReplayThreatVisual({
|
|||||||
? `${classifiedCellCounts.ground} ground-support · visual review only · navigation authority OFF`
|
? `${classifiedCellCounts.ground} ground-support · visual review only · navigation authority OFF`
|
||||||
: "visual review only · navigation authority OFF"
|
: "visual review only · navigation authority OFF"
|
||||||
: `${metadata.timeline.corridor.forwardLengthM} м · body ${metadata.timeline.rig.lengthM}×${metadata.timeline.rig.widthM} м · REPLAY-SIMULATED`}</small>
|
: `${metadata.timeline.corridor.forwardLengthM} м · body ${metadata.timeline.rig.lengthM}×${metadata.timeline.rig.widthM} м · REPLAY-SIMULATED`}</small>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
@@ -1153,13 +1191,13 @@ export function M4ReplayThreatVisual({
|
|||||||
<span>{timelineFrame.error}</span>
|
<span>{timelineFrame.error}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{semantic && semanticTimeline.loading ? (
|
{semanticSpatialResultId && semanticTimeline.loading ? (
|
||||||
<div className="m4-replay-threat-visual__buffering" role="status">
|
<div className="m4-replay-threat-visual__buffering" role="status">
|
||||||
<span className="busy-indicator" aria-hidden="true" />
|
<span className="busy-indicator" aria-hidden="true" />
|
||||||
<span>Догружаем semantic-point evidence E47</span>
|
<span>Догружаем semantic-point evidence E47</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{semanticTimeline.error ? (
|
{semanticSpatialResultId && semanticTimeline.error ? (
|
||||||
<div className="m4-replay-threat-visual__buffering" role="alert">
|
<div className="m4-replay-threat-visual__buffering" role="alert">
|
||||||
<Icon name="alert" size={16} />
|
<Icon name="alert" size={16} />
|
||||||
<span>{semanticTimeline.error}</span>
|
<span>{semanticTimeline.error}</span>
|
||||||
@@ -1201,7 +1239,7 @@ export function M4ReplayThreatVisual({
|
|||||||
<div className="l3-visual-audit m4-replay-threat-visual">
|
<div className="l3-visual-audit m4-replay-threat-visual">
|
||||||
<LaboratoryEvidenceViewer
|
<LaboratoryEvidenceViewer
|
||||||
label={semantic
|
label={semantic
|
||||||
? "E47 semantic + SLAM diagnostic replay"
|
? semantic.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"}
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import {
|
|||||||
LaboratorySummary,
|
LaboratorySummary,
|
||||||
LaboratoryWorkTemplate,
|
LaboratoryWorkTemplate,
|
||||||
} from "../../components/laboratory/LaboratoryPresentation";
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
|
import {
|
||||||
|
vegetationVideoMaskUrl,
|
||||||
|
type VegetationShadowResult,
|
||||||
|
} from "../../core/laboratory/vegetationShadow";
|
||||||
import {
|
import {
|
||||||
M48MaskComparisonVisual,
|
M48MaskComparisonVisual,
|
||||||
type M48MaskComparisonCase,
|
type M48MaskComparisonCase,
|
||||||
} from "./M48FailureAtlasVisual";
|
} from "./M48FailureAtlasVisual";
|
||||||
|
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
||||||
|
|
||||||
function decimal(value: number, digits = 1): string {
|
function decimal(value: number, digits = 1): string {
|
||||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||||
@@ -63,20 +67,28 @@ export function VegetationShadowResultView({
|
|||||||
summary={(
|
summary={(
|
||||||
<LaboratorySummary
|
<LaboratorySummary
|
||||||
title="LAB V1 · готовые модели растительности"
|
title="LAB V1 · готовые модели растительности"
|
||||||
description="Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."
|
description={result.routeVideo
|
||||||
status="Truth-backed model comparison · route transfer не принят"
|
? "M4.8 сохраняет truth-backed сравнение моделей, а штатный M4.7 viewer показывает фактический DDRNet prediction на всей записи RAVNOVES00. Все 4489 масок запечатаны локально и открываются без Worker 006."
|
||||||
|
: "Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."}
|
||||||
|
status={result.routeVideo
|
||||||
|
? "DDRNet full-video prediction ready · route truth отсутствует"
|
||||||
|
: "Truth-backed model comparison · route transfer не принят"}
|
||||||
statusTone="warning"
|
statusTone="warning"
|
||||||
facts={[
|
facts={[
|
||||||
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" },
|
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" },
|
||||||
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
|
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
|
||||||
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
|
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
|
||||||
|
...(result.routeVideo ? [{
|
||||||
|
label: "Видео",
|
||||||
|
value: "RAVNOVES00 · 4489/4489 DDRNet masks · exact recorded sequence",
|
||||||
|
}] : []),
|
||||||
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
|
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
|
||||||
]}
|
]}
|
||||||
brief={{
|
brief={{
|
||||||
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
|
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
|
||||||
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.",
|
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.",
|
||||||
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. Ошибки по каждому типу теперь проверяются в одном штатном инструменте.`,
|
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. ${result.routeVideo ? "Его фактическая temporal stability теперь видна на всех 4489 кадрах штатного recorded viewer." : "Ошибки по каждому типу проверяются в одном штатном инструменте."}`,
|
||||||
limitation: "Это внешний GOOSE-домен, а не наш fisheye/off-road маршрут. Папоротник отдельным классом отсутствует; RAVNOVES00 не содержит truth-backed vegetation island и не используется как главное визуальное доказательство.",
|
limitation: "GOOSE — внешний размеченный домен; RAVNOVES00 — наш fisheye, но без ручной truth-разметки. Full-video слой показывает prediction, а не доказывает правильность. Папоротник отдельным классом отсутствует.",
|
||||||
}}
|
}}
|
||||||
method={{
|
method={{
|
||||||
completeness: "complete",
|
completeness: "complete",
|
||||||
@@ -93,17 +105,42 @@ export function VegetationShadowResultView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
evidence={(
|
evidence={(
|
||||||
<LaboratoryEvidence
|
<>
|
||||||
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
|
<LaboratoryEvidence
|
||||||
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
|
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
|
||||||
kind="diagnostic-model"
|
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
|
||||||
resizable
|
kind="diagnostic-model"
|
||||||
>
|
resizable
|
||||||
<M48MaskComparisonVisual
|
>
|
||||||
cases={comparisonCases(result)}
|
<M48MaskComparisonVisual
|
||||||
initialCandidate={result.selectedCandidate}
|
cases={comparisonCases(result)}
|
||||||
/>
|
initialCandidate={result.selectedCandidate}
|
||||||
</LaboratoryEvidence>
|
/>
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
{result.routeVideo ? (
|
||||||
|
<LaboratoryEvidence
|
||||||
|
eyebrow="M4.7 · RAVNOVES00 FULL VIDEO"
|
||||||
|
title="DDRNet PREDICTION · 4489/4489 кадров · TRUTH для этой записи отсутствует"
|
||||||
|
kind="diagnostic-model"
|
||||||
|
resizable
|
||||||
|
>
|
||||||
|
<M4ReplayThreatVisual
|
||||||
|
resultId={result.routeVideo.baseM4ResultId}
|
||||||
|
evidenceLabel="LAB V1 · DDRNet"
|
||||||
|
showReferenceMediaLayers={false}
|
||||||
|
showSpatialOverlaySummary={false}
|
||||||
|
semantic={{
|
||||||
|
resultId: result.routeVideo.workerResultId,
|
||||||
|
spatialResultId: null,
|
||||||
|
taxonomy: result.routeVideo.taxonomy,
|
||||||
|
maskUrl: (sequence) => vegetationVideoMaskUrl(result.resultId, sequence),
|
||||||
|
label: "DDRNet vegetation prediction · recorded video",
|
||||||
|
maskAriaLabel: "DDRNet vegetation prediction",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
result={(
|
result={(
|
||||||
<LaboratoryResultSummary
|
<LaboratoryResultSummary
|
||||||
@@ -146,11 +183,16 @@ export function VegetationShadowResultView({
|
|||||||
value: "12 truth-backed cases",
|
value: "12 truth-backed cases",
|
||||||
hint: "8 vegetation strata · Worker для открытия не требуется",
|
hint: "8 vegetation strata · Worker для открытия не требуется",
|
||||||
},
|
},
|
||||||
|
...(result.routeVideo ? [{
|
||||||
|
label: "Route video",
|
||||||
|
value: "4489/4489 masks",
|
||||||
|
hint: "DDRNet prediction · exact sequence · Worker-independent playback",
|
||||||
|
}] : []),
|
||||||
]}
|
]}
|
||||||
conclusion={{
|
conclusion={{
|
||||||
proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.",
|
proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.",
|
||||||
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, temporal stability, collision safety и physical-live поведение ровера.",
|
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, collision safety и physical-live поведение ровера. Видео позволяет увидеть temporal stability, но без truth не превращает её в метрику качества.",
|
||||||
decision: "Сохранить DDRNet как стартовый vegetation candidate. Mission-policy и автоматическое переключение пресетов подключать только после truth-backed island нашего офф-роуда; LiDAR/TGS fail-closed геометрию не ослаблять.",
|
decision: "Смотреть полный prediction на видео и собирать конкретные temporal/domain failure cases. DDRNet остаётся diagnostic candidate; LiDAR/TGS fail-closed геометрию не ослаблять.",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,6 +75,35 @@ function visualCase(sourceKind, index) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeVideo() {
|
||||||
|
return {
|
||||||
|
worker_result_id: `lab-v1-ravnoves-video-ddrnet-${"e".repeat(64)}`,
|
||||||
|
m47_reference_graph_result_id: `m47-reference-graph-lab-${"f".repeat(64)}`,
|
||||||
|
base_m4_result_id: `m4-threat-replay-${"1".repeat(64)}`,
|
||||||
|
frame_count: 4489,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
center_crop_xyxy: [100, 0, 700, 600],
|
||||||
|
outside_crop_state: "undefined",
|
||||||
|
sequence_binding: "sequence-0-to-masks/frame-000001.png",
|
||||||
|
taxonomy: {
|
||||||
|
schema_version: "missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||||
|
classes: Array.from({ length: 64 }, (_, classId) => ({
|
||||||
|
class_id: classId,
|
||||||
|
label: classId === 0 ? "undefined" : `class-${classId}`,
|
||||||
|
color_rgb: [classId, classId, classId],
|
||||||
|
disposition: classId === 0 ? "undefined" : "prediction",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
aggregate_prediction_pixels: Array(64).fill(0),
|
||||||
|
mask_archive: {
|
||||||
|
path: "video/ddrnet-semantic-masks.zip",
|
||||||
|
sha256: "9".repeat(64),
|
||||||
|
byte_length: 1024,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
test("vegetation LAB keeps autonomous assets and fail-closed authority", async () => {
|
test("vegetation LAB keeps autonomous assets and fail-closed authority", async () => {
|
||||||
let requestedUrl = "";
|
let requestedUrl = "";
|
||||||
const result = await fetchVegetationShadowResult(resultId, {
|
const result = await fetchVegetationShadowResult(resultId, {
|
||||||
@@ -111,6 +140,7 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
|
|||||||
goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)),
|
goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)),
|
||||||
ravnoves: [],
|
ravnoves: [],
|
||||||
},
|
},
|
||||||
|
route_video: routeVideo(),
|
||||||
access: "read-only",
|
access: "read-only",
|
||||||
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
},
|
},
|
||||||
@@ -123,6 +153,8 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
|
|||||||
assert.equal(result.candidates[0].vegetationMeanIouPercent, 64);
|
assert.equal(result.candidates[0].vegetationMeanIouPercent, 64);
|
||||||
assert.equal(result.routeCases.length, 0);
|
assert.equal(result.routeCases.length, 0);
|
||||||
assert.equal(result.validationCases.length, 12);
|
assert.equal(result.validationCases.length, 12);
|
||||||
|
assert.equal(result.routeVideo.frameCount, 4489);
|
||||||
|
assert.equal(result.routeVideo.taxonomy[0].disposition, "undefined");
|
||||||
assert.equal(result.validationCases[0].focus.className, "high_grass");
|
assert.equal(result.validationCases[0].focus.className, "high_grass");
|
||||||
assert.match(result.validationCases[0].assets.ddrnet_error, /\/assets\/visual\/goose\//);
|
assert.match(result.validationCases[0].assets.ddrnet_error, /\/assets\/visual\/goose\//);
|
||||||
assert.deepEqual(result.authority, {
|
assert.deepEqual(result.authority, {
|
||||||
@@ -133,13 +165,15 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("vegetation LAB reuses the admitted M4.8 instrument", async () => {
|
test("vegetation LAB reuses the admitted M4.8 and M4.7 instruments", async () => {
|
||||||
const resultSource = await readFile(
|
const resultSource = await readFile(
|
||||||
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
assert.match(resultSource, /M48MaskComparisonVisual/);
|
assert.match(resultSource, /M48MaskComparisonVisual/);
|
||||||
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 1);
|
assert.match(resultSource, /M4ReplayThreatVisual/);
|
||||||
|
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 2);
|
||||||
|
assert.match(resultSource, /showReferenceMediaLayers=\{false\}/);
|
||||||
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)),
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
"ravnoves": {
|
"ravnoves": {
|
||||||
"source_id": "RAVNOVES00/right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
"source_id": "RAVNOVES00/right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||||
"source_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
"source_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||||
|
"base_m4_result_id": "m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324",
|
||||||
"expected_width": 800,
|
"expected_width": 800,
|
||||||
"expected_height": 600,
|
"expected_height": 600,
|
||||||
"expected_frame_count": 4489,
|
"expected_frame_count": 4489,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[ValidateSet("Build", "Probe", "Validate", "Ravnoves", "Status")]
|
[ValidateSet("Build", "Probe", "Validate", "Ravnoves", "RavnovesVideo", "Status")]
|
||||||
[string]$Mode = "Status",
|
[string]$Mode = "Status",
|
||||||
|
|
||||||
[ValidateSet("Ddrnet", "Ppliteseg")]
|
[ValidateSet("Ddrnet", "Ppliteseg")]
|
||||||
@@ -104,12 +104,13 @@ function New-RunRoot {
|
|||||||
|
|
||||||
function Invoke-IsolatedRun {
|
function Invoke-IsolatedRun {
|
||||||
param(
|
param(
|
||||||
[ValidateSet("goose", "ravnoves")][string]$RunMode,
|
[ValidateSet("goose", "ravnoves", "ravnoves-video")][string]$RunMode,
|
||||||
[string]$RunRoot,
|
[string]$RunRoot,
|
||||||
[int]$Limit,
|
[int]$Limit,
|
||||||
[string]$FramesRoot = ""
|
[string]$FramesRoot = ""
|
||||||
)
|
)
|
||||||
$containerName = "ndc-lab-v1-goose-$candidateKey-$([Guid]::NewGuid().ToString('N').Substring(0, 10))"
|
$containerName = "ndc-lab-v1-goose-$candidateKey-$([Guid]::NewGuid().ToString('N').Substring(0, 10))"
|
||||||
|
$visualCount = if ($RunMode -eq "ravnoves-video") { 0 } else { 12 }
|
||||||
$arguments = @(
|
$arguments = @(
|
||||||
"run", "--rm", "--name", $containerName,
|
"run", "--rm", "--name", $containerName,
|
||||||
"--gpus", "all",
|
"--gpus", "all",
|
||||||
@@ -136,9 +137,9 @@ function Invoke-IsolatedRun {
|
|||||||
"--dataset-root", "/data/goose",
|
"--dataset-root", "/data/goose",
|
||||||
"--output", "/output/result",
|
"--output", "/output/result",
|
||||||
"--limit", $Limit.ToString(),
|
"--limit", $Limit.ToString(),
|
||||||
"--visual-count", "12"
|
"--visual-count", $visualCount.ToString()
|
||||||
)
|
)
|
||||||
if ($RunMode -eq "ravnoves") {
|
if ($RunMode -in @("ravnoves", "ravnoves-video")) {
|
||||||
$arguments = @($arguments[0..($arguments.Count - 1)])
|
$arguments = @($arguments[0..($arguments.Count - 1)])
|
||||||
$arguments += @("--frames-root", "/input")
|
$arguments += @("--frames-root", "/input")
|
||||||
$mountIndex = [Array]::IndexOf($arguments, $image)
|
$mountIndex = [Array]::IndexOf($arguments, $image)
|
||||||
@@ -172,6 +173,20 @@ function Export-RavnovesFrames {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Export-RavnovesVideoFrames {
|
||||||
|
param([string]$Destination)
|
||||||
|
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $ravnovesSha256
|
||||||
|
New-Item -ItemType Directory -Path $Destination | Out-Null
|
||||||
|
& ffmpeg -hide_banner -loglevel error -i $RavnovesVideo -map 0:v:0 -fps_mode passthrough (Join-Path $Destination "frame-%06d.png")
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "RAVNOVES full-video frame extraction failed"
|
||||||
|
}
|
||||||
|
$frames = @(Get-ChildItem -LiteralPath $Destination -File -Filter "frame-*.png" | Sort-Object Name)
|
||||||
|
if ($frames.Count -ne 4489 -or $frames[0].Name -ne "frame-000001.png" -or $frames[-1].Name -ne "frame-004489.png") {
|
||||||
|
throw "RAVNOVES full-video frame sequence changed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($Mode -eq "Status") {
|
if ($Mode -eq "Status") {
|
||||||
$imageIdentity = & docker image inspect $image --format "{{.Id}}" 2>$null
|
$imageIdentity = & docker image inspect $image --format "{{.Id}}" 2>$null
|
||||||
[ordered]@{
|
[ordered]@{
|
||||||
@@ -221,6 +236,16 @@ try {
|
|||||||
Export-RavnovesFrames -Destination $framesRoot
|
Export-RavnovesFrames -Destination $framesRoot
|
||||||
Invoke-IsolatedRun -RunMode "ravnoves" -RunRoot $runRoot -Limit 0 -FramesRoot $framesRoot
|
Invoke-IsolatedRun -RunMode "ravnoves" -RunRoot $runRoot -Limit 0 -FramesRoot $framesRoot
|
||||||
}
|
}
|
||||||
|
elseif ($Mode -eq "RavnovesVideo") {
|
||||||
|
if ($candidateKey -ne "ddrnet") {
|
||||||
|
throw "Full-video shadow is admitted only for the selected DDRNet candidate"
|
||||||
|
}
|
||||||
|
$runRoot = New-RunRoot -Kind "ravnoves-video"
|
||||||
|
$framesRoot = Join-Path $runRoot "input-frames"
|
||||||
|
Export-RavnovesVideoFrames -Destination $framesRoot
|
||||||
|
Invoke-IsolatedRun -RunMode "ravnoves-video" -RunRoot $runRoot -Limit 0 -FramesRoot $framesRoot
|
||||||
|
Remove-Item -LiteralPath $framesRoot -Recurse -Force
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
$canonicalAfter = Get-CanonicalTritonIdentity
|
$canonicalAfter = Get-CanonicalTritonIdentity
|
||||||
|
|||||||
+103
-3
@@ -11,6 +11,7 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import statistics
|
import statistics
|
||||||
import time
|
import time
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -43,7 +44,11 @@ class RunnerError(RuntimeError):
|
|||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--mode", choices=("goose", "ravnoves"), required=True)
|
parser.add_argument(
|
||||||
|
"--mode",
|
||||||
|
choices=("goose", "ravnoves", "ravnoves-video"),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
parser.add_argument("--candidate", choices=tuple(MODEL_NAMES), required=True)
|
parser.add_argument("--candidate", choices=tuple(MODEL_NAMES), required=True)
|
||||||
parser.add_argument("--config", type=Path, required=True)
|
parser.add_argument("--config", type=Path, required=True)
|
||||||
parser.add_argument("--policy", type=Path, required=True)
|
parser.add_argument("--policy", type=Path, required=True)
|
||||||
@@ -338,6 +343,40 @@ def save_image(path: Path, value: Image.Image | np.ndarray, mode: str | None = N
|
|||||||
return sha256(path)
|
return sha256(path)
|
||||||
|
|
||||||
|
|
||||||
|
def write_mask_archive(
|
||||||
|
output: Path,
|
||||||
|
masks_root: Path,
|
||||||
|
frame_count: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
archive_path = output / "semantic-masks.zip"
|
||||||
|
expected = [f"frame-{sequence + 1:06d}.png" for sequence in range(frame_count)]
|
||||||
|
actual = sorted(path.name for path in masks_root.glob("frame-*.png"))
|
||||||
|
if actual != expected:
|
||||||
|
raise RunnerError("RAVNOVES video mask sequence is incomplete")
|
||||||
|
with zipfile.ZipFile(
|
||||||
|
archive_path,
|
||||||
|
mode="x",
|
||||||
|
compression=zipfile.ZIP_STORED,
|
||||||
|
allowZip64=True,
|
||||||
|
) as archive:
|
||||||
|
for name in expected:
|
||||||
|
archive.write(masks_root / name, arcname=f"masks/{name}")
|
||||||
|
for path in masks_root.iterdir():
|
||||||
|
path.unlink()
|
||||||
|
masks_root.rmdir()
|
||||||
|
return {
|
||||||
|
"path": archive_path.name,
|
||||||
|
"sha256": sha256(archive_path),
|
||||||
|
"byte_length": archive_path.stat().st_size,
|
||||||
|
"media_type": "application/zip",
|
||||||
|
"frame_count": frame_count,
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"encoding": "uint8-class-id-png",
|
||||||
|
"sequence_binding": "sequence-0-to-masks/frame-000001.png",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def expand_mask(
|
def expand_mask(
|
||||||
mask: np.ndarray,
|
mask: np.ndarray,
|
||||||
original_size: tuple[int, int],
|
original_size: tuple[int, int],
|
||||||
@@ -517,7 +556,7 @@ def run() -> None:
|
|||||||
(image.stem.removesuffix("_windshield_vis"), image, label)
|
(image.stem.removesuffix("_windshield_vis"), image, label)
|
||||||
for image, label in pairs
|
for image, label in pairs
|
||||||
]
|
]
|
||||||
else:
|
elif args.mode == "ravnoves":
|
||||||
if args.frames_root is None or not args.frames_root.is_dir():
|
if args.frames_root is None or not args.frames_root.is_dir():
|
||||||
raise RunnerError("frames-root is required for RAVNOVES mode")
|
raise RunnerError("frames-root is required for RAVNOVES mode")
|
||||||
frames = sorted(args.frames_root.glob("frame-*.png"))
|
frames = sorted(args.frames_root.glob("frame-*.png"))
|
||||||
@@ -525,6 +564,15 @@ def run() -> None:
|
|||||||
if {frame.stem for frame in frames} != expected:
|
if {frame.stem for frame in frames} != expected:
|
||||||
raise RunnerError("RAVNOVES frame island identity changed")
|
raise RunnerError("RAVNOVES frame island identity changed")
|
||||||
items = [(frame.stem, frame, None) for frame in frames]
|
items = [(frame.stem, frame, None) for frame in frames]
|
||||||
|
else:
|
||||||
|
if args.frames_root is None or not args.frames_root.is_dir():
|
||||||
|
raise RunnerError("frames-root is required for RAVNOVES video mode")
|
||||||
|
frames = sorted(args.frames_root.glob("frame-*.png"))
|
||||||
|
expected_count = config["ravnoves"]["expected_frame_count"]
|
||||||
|
expected_names = [f"frame-{sequence + 1:06d}.png" for sequence in range(expected_count)]
|
||||||
|
if len(frames) != expected_count or [frame.name for frame in frames] != expected_names:
|
||||||
|
raise RunnerError("RAVNOVES full-video frame sequence changed")
|
||||||
|
items = [(frame.stem, frame, None) for frame in frames]
|
||||||
|
|
||||||
if args.limit:
|
if args.limit:
|
||||||
items = items[: args.limit]
|
items = items[: args.limit]
|
||||||
@@ -539,8 +587,12 @@ def run() -> None:
|
|||||||
if args.visual_count != configured_visual_count:
|
if args.visual_count != configured_visual_count:
|
||||||
raise RunnerError("GOOSE visual count differs from the truth-focused contract")
|
raise RunnerError("GOOSE visual count differs from the truth-focused contract")
|
||||||
selected_visuals = truth_focused_visuals(items, names, visual_contract)
|
selected_visuals = truth_focused_visuals(items, names, visual_contract)
|
||||||
else:
|
elif args.mode == "ravnoves":
|
||||||
selected_visuals = visual_indices(len(items), args.visual_count)
|
selected_visuals = visual_indices(len(items), args.visual_count)
|
||||||
|
else:
|
||||||
|
if args.visual_count != 0 or args.limit:
|
||||||
|
raise RunnerError("RAVNOVES video mode requires the complete frame sequence")
|
||||||
|
selected_visuals = {}
|
||||||
|
|
||||||
args.output.mkdir(parents=True, exist_ok=False)
|
args.output.mkdir(parents=True, exist_ok=False)
|
||||||
torch.cuda.empty_cache()
|
torch.cuda.empty_cache()
|
||||||
@@ -552,12 +604,28 @@ def run() -> None:
|
|||||||
confusion = np.zeros((CLASS_COUNT, CLASS_COUNT), dtype=np.int64)
|
confusion = np.zeros((CLASS_COUNT, CLASS_COUNT), dtype=np.int64)
|
||||||
latencies_ms: list[float] = []
|
latencies_ms: list[float] = []
|
||||||
visuals: list[dict[str, Any]] = []
|
visuals: list[dict[str, Any]] = []
|
||||||
|
mask_root = args.output / "masks" if args.mode == "ravnoves-video" else None
|
||||||
|
if mask_root is not None:
|
||||||
|
mask_root.mkdir()
|
||||||
|
aggregate_prediction_pixels = np.zeros(CLASS_COUNT, dtype=np.int64)
|
||||||
|
|
||||||
for index, (case_id, source_path, label_path) in enumerate(items):
|
for index, (case_id, source_path, label_path) in enumerate(items):
|
||||||
source = Image.open(source_path).convert("RGB")
|
source = Image.open(source_path).convert("RGB")
|
||||||
|
if args.mode == "ravnoves-video" and source.size != (
|
||||||
|
config["ravnoves"]["expected_width"],
|
||||||
|
config["ravnoves"]["expected_height"],
|
||||||
|
):
|
||||||
|
raise RunnerError("RAVNOVES video frame dimensions changed")
|
||||||
tensor, crop_box = preprocess(source)
|
tensor, crop_box = preprocess(source)
|
||||||
prediction, latency_ms = infer(model, tensor)
|
prediction, latency_ms = infer(model, tensor)
|
||||||
latencies_ms.append(latency_ms)
|
latencies_ms.append(latency_ms)
|
||||||
|
if mask_root is not None:
|
||||||
|
expanded_prediction = expand_mask(prediction, source.size, crop_box)
|
||||||
|
save_image(mask_root / f"frame-{index + 1:06d}.png", expanded_prediction, "L")
|
||||||
|
aggregate_prediction_pixels += np.bincount(
|
||||||
|
expanded_prediction.reshape(-1),
|
||||||
|
minlength=CLASS_COUNT,
|
||||||
|
)
|
||||||
truth = preprocess_label(Image.open(label_path)) if label_path is not None else None
|
truth = preprocess_label(Image.open(label_path)) if label_path is not None else None
|
||||||
if truth is not None:
|
if truth is not None:
|
||||||
update_confusion(confusion, truth, prediction)
|
update_confusion(confusion, truth, prediction)
|
||||||
@@ -579,6 +647,23 @@ def run() -> None:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mask_archive = (
|
||||||
|
write_mask_archive(args.output, mask_root, len(items))
|
||||||
|
if mask_root is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
taxonomy = {
|
||||||
|
"schema_version": "missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||||
|
"classes": [
|
||||||
|
{
|
||||||
|
"class_id": label_id,
|
||||||
|
"label": names[label_id],
|
||||||
|
"color_rgb": semantic_palette[label_id, :3].astype(int).tolist(),
|
||||||
|
"disposition": "undefined" if label_id == 0 else "prediction",
|
||||||
|
}
|
||||||
|
for label_id in range(CLASS_COUNT)
|
||||||
|
],
|
||||||
|
}
|
||||||
rows = class_metrics(confusion, names) if args.mode == "goose" else []
|
rows = class_metrics(confusion, names) if args.mode == "goose" else []
|
||||||
valid_ious = [row["iou"] for row in rows if row["iou"] is not None]
|
valid_ious = [row["iou"] for row in rows if row["iou"] is not None]
|
||||||
vegetation_names = set(config["vegetation_class_names"])
|
vegetation_names = set(config["vegetation_class_names"])
|
||||||
@@ -615,6 +700,20 @@ def run() -> None:
|
|||||||
"ground_truth_available": args.mode == "goose",
|
"ground_truth_available": args.mode == "goose",
|
||||||
"mapping_sha256": dataset_config["mapping_sha256"],
|
"mapping_sha256": dataset_config["mapping_sha256"],
|
||||||
},
|
},
|
||||||
|
"video_semantics": {
|
||||||
|
"base_m4_result_id": config["ravnoves"].get("base_m4_result_id"),
|
||||||
|
"mask_archive": mask_archive,
|
||||||
|
"taxonomy": taxonomy,
|
||||||
|
"aggregate_prediction_pixels": aggregate_prediction_pixels.tolist()
|
||||||
|
if mask_archive is not None
|
||||||
|
else None,
|
||||||
|
"center_crop_xyxy": [100, 0, 700, 600]
|
||||||
|
if mask_archive is not None
|
||||||
|
else None,
|
||||||
|
"outside_crop_state": "undefined" if mask_archive is not None else None,
|
||||||
|
}
|
||||||
|
if args.mode == "ravnoves-video"
|
||||||
|
else None,
|
||||||
"preprocessing": dataset_config["preprocessing"],
|
"preprocessing": dataset_config["preprocessing"],
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"mean_iou": round(statistics.fmean(valid_ious), 8) if valid_ious else None,
|
"mean_iou": round(statistics.fmean(valid_ious), 8) if valid_ious else None,
|
||||||
@@ -650,6 +749,7 @@ def run() -> None:
|
|||||||
"schema_version": result["schema_version"],
|
"schema_version": result["schema_version"],
|
||||||
"candidate": result["candidate"],
|
"candidate": result["candidate"],
|
||||||
"source": result["source"],
|
"source": result["source"],
|
||||||
|
"video_semantics": result["video_semantics"],
|
||||||
"preprocessing": result["preprocessing"],
|
"preprocessing": result["preprocessing"],
|
||||||
"metrics": result["metrics"],
|
"metrics": result["metrics"],
|
||||||
"timing": result["timing"],
|
"timing": result["timing"],
|
||||||
|
|||||||
@@ -5,17 +5,28 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import zipfile
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from k1link.laboratory.m47_reference_graph import read_m47_reference_graph_lab
|
||||||
|
|
||||||
LAB_SCHEMA: Final = "missioncore.lab-v1-vegetation-shadow/v1"
|
LAB_SCHEMA: Final = "missioncore.lab-v1-vegetation-shadow/v1"
|
||||||
WORKER_SCHEMA: Final = "missioncore.lab-v1-goose-vegetation-run/v1"
|
WORKER_SCHEMA: Final = "missioncore.lab-v1-goose-vegetation-run/v1"
|
||||||
RESULT_PREFIX: Final = "lab-v1-vegetation-shadow-"
|
RESULT_PREFIX: Final = "lab-v1-vegetation-shadow-"
|
||||||
_CANDIDATES: Final = ("ddrnet", "ppliteseg")
|
_CANDIDATES: Final = ("ddrnet", "ppliteseg")
|
||||||
_MODES: Final = ("goose", "ravnoves")
|
_MODES: Final = ("goose", "ravnoves")
|
||||||
|
_VIDEO_MODE: Final = "ravnoves-video"
|
||||||
|
_VIDEO_FRAME_COUNT: Final = 4489
|
||||||
|
_M4_RESULT_ID: Final = re.compile(r"^m4-threat-replay-[a-f0-9]{64}$")
|
||||||
|
_VIDEO_WORKER_RESULT_ID: Final = re.compile(
|
||||||
|
r"^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$",
|
||||||
|
)
|
||||||
|
_SHA256: Final = re.compile(r"^[a-f0-9]{64}$")
|
||||||
_FOCUS_ORDER: Final = (
|
_FOCUS_ORDER: Final = (
|
||||||
"high_grass",
|
"high_grass",
|
||||||
"low_grass",
|
"low_grass",
|
||||||
@@ -195,6 +206,106 @@ def _validation_metric_summary(result: dict[str, Any], candidate: str) -> dict[s
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_video_semantics(
|
||||||
|
root: Path,
|
||||||
|
result: dict[str, Any],
|
||||||
|
) -> tuple[dict[str, object], Path]:
|
||||||
|
source = _object(result.get("source"), "DDRNet video source")
|
||||||
|
video = _object(result.get("video_semantics"), "DDRNet video semantics")
|
||||||
|
archive = _object(video.get("mask_archive"), "DDRNet video mask archive")
|
||||||
|
taxonomy = _object(video.get("taxonomy"), "DDRNet video taxonomy")
|
||||||
|
classes = taxonomy.get("classes")
|
||||||
|
base_m4_result_id = video.get("base_m4_result_id")
|
||||||
|
worker_result_id = result.get("result_id")
|
||||||
|
aggregate_prediction_pixels = video.get("aggregate_prediction_pixels")
|
||||||
|
if (
|
||||||
|
source.get("input_count") != _VIDEO_FRAME_COUNT
|
||||||
|
or source.get("ground_truth_available") is not False
|
||||||
|
or taxonomy.get("schema_version")
|
||||||
|
!= "missioncore.lab-v1-vegetation-taxonomy/v1"
|
||||||
|
or not isinstance(classes, list)
|
||||||
|
or len(classes) != 64
|
||||||
|
or not isinstance(base_m4_result_id, str)
|
||||||
|
or _M4_RESULT_ID.fullmatch(base_m4_result_id) is None
|
||||||
|
or not isinstance(worker_result_id, str)
|
||||||
|
or _VIDEO_WORKER_RESULT_ID.fullmatch(worker_result_id) is None
|
||||||
|
or not isinstance(aggregate_prediction_pixels, list)
|
||||||
|
or len(aggregate_prediction_pixels) != 64
|
||||||
|
or any(type(count) is not int or count < 0 for count in aggregate_prediction_pixels)
|
||||||
|
or sum(aggregate_prediction_pixels) != _VIDEO_FRAME_COUNT * 800 * 600
|
||||||
|
or video.get("center_crop_xyxy") != [100, 0, 700, 600]
|
||||||
|
or video.get("outside_crop_state") != "undefined"
|
||||||
|
or archive.get("path") != "semantic-masks.zip"
|
||||||
|
or archive.get("frame_count") != _VIDEO_FRAME_COUNT
|
||||||
|
or archive.get("width") != 800
|
||||||
|
or archive.get("height") != 600
|
||||||
|
or archive.get("encoding") != "uint8-class-id-png"
|
||||||
|
or archive.get("media_type") != "application/zip"
|
||||||
|
or archive.get("sequence_binding")
|
||||||
|
!= "sequence-0-to-masks/frame-000001.png"
|
||||||
|
):
|
||||||
|
raise VegetationShadowLabError("DDRNet full-video contract changed")
|
||||||
|
for expected_id, raw_class in enumerate(classes):
|
||||||
|
row = _object(raw_class, "DDRNet taxonomy class")
|
||||||
|
color = row.get("color_rgb")
|
||||||
|
if (
|
||||||
|
row.get("class_id") != expected_id
|
||||||
|
or not isinstance(row.get("label"), str)
|
||||||
|
or not row["label"]
|
||||||
|
or row.get("disposition")
|
||||||
|
not in ({"undefined"} if expected_id == 0 else {"prediction"})
|
||||||
|
or not isinstance(color, list)
|
||||||
|
or len(color) != 3
|
||||||
|
or any(not isinstance(channel, int) or not 0 <= channel <= 255 for channel in color)
|
||||||
|
):
|
||||||
|
raise VegetationShadowLabError("DDRNet video taxonomy changed")
|
||||||
|
archive_path = root / "semantic-masks.zip"
|
||||||
|
expected_sha256 = archive.get("sha256")
|
||||||
|
expected_bytes = archive.get("byte_length")
|
||||||
|
if (
|
||||||
|
archive_path.is_symlink()
|
||||||
|
or not archive_path.is_file()
|
||||||
|
or type(expected_bytes) is not int
|
||||||
|
or expected_bytes <= 0
|
||||||
|
or archive_path.stat().st_size != expected_bytes
|
||||||
|
or not isinstance(expected_sha256, str)
|
||||||
|
or _SHA256.fullmatch(expected_sha256) is None
|
||||||
|
or sha256_path(archive_path) != expected_sha256
|
||||||
|
):
|
||||||
|
raise VegetationShadowLabError("DDRNet video mask archive proof changed")
|
||||||
|
expected_members = [
|
||||||
|
f"masks/frame-{sequence + 1:06d}.png"
|
||||||
|
for sequence in range(_VIDEO_FRAME_COUNT)
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(archive_path) as frozen:
|
||||||
|
members = frozen.infolist()
|
||||||
|
if (
|
||||||
|
[member.filename for member in members] != expected_members
|
||||||
|
or any(
|
||||||
|
member.is_dir()
|
||||||
|
or member.file_size < 8
|
||||||
|
or member.file_size > 1024 * 1024
|
||||||
|
for member in members
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise VegetationShadowLabError("DDRNet video mask sequence changed")
|
||||||
|
except zipfile.BadZipFile as exc:
|
||||||
|
raise VegetationShadowLabError("DDRNet video mask archive is invalid") from exc
|
||||||
|
return {
|
||||||
|
"worker_result_id": worker_result_id,
|
||||||
|
"base_m4_result_id": base_m4_result_id,
|
||||||
|
"frame_count": _VIDEO_FRAME_COUNT,
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"center_crop_xyxy": [100, 0, 700, 600],
|
||||||
|
"outside_crop_state": "undefined",
|
||||||
|
"sequence_binding": archive["sequence_binding"],
|
||||||
|
"taxonomy": taxonomy,
|
||||||
|
"aggregate_prediction_pixels": aggregate_prediction_pixels,
|
||||||
|
}, archive_path
|
||||||
|
|
||||||
|
|
||||||
def seal_vegetation_shadow_lab(
|
def seal_vegetation_shadow_lab(
|
||||||
*,
|
*,
|
||||||
ddrnet_goose_root: Path,
|
ddrnet_goose_root: Path,
|
||||||
@@ -202,6 +313,8 @@ def seal_vegetation_shadow_lab(
|
|||||||
ddrnet_ravnoves_root: Path,
|
ddrnet_ravnoves_root: Path,
|
||||||
ppliteseg_ravnoves_root: Path,
|
ppliteseg_ravnoves_root: Path,
|
||||||
output_root: Path,
|
output_root: Path,
|
||||||
|
ddrnet_ravnoves_video_root: Path | None = None,
|
||||||
|
m47_reference_graph_lab_root: Path | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
roots = {
|
roots = {
|
||||||
("ddrnet", "goose"): ddrnet_goose_root.resolve(),
|
("ddrnet", "goose"): ddrnet_goose_root.resolve(),
|
||||||
@@ -218,6 +331,29 @@ def seal_vegetation_shadow_lab(
|
|||||||
if cases[("ddrnet", mode)].keys() != cases[("ppliteseg", mode)].keys():
|
if cases[("ddrnet", mode)].keys() != cases[("ppliteseg", mode)].keys():
|
||||||
raise VegetationShadowLabError(f"{mode} candidate case islands differ")
|
raise VegetationShadowLabError(f"{mode} candidate case islands differ")
|
||||||
selected = _selected_candidate(results)
|
selected = _selected_candidate(results)
|
||||||
|
if (ddrnet_ravnoves_video_root is None) != (m47_reference_graph_lab_root is None):
|
||||||
|
raise VegetationShadowLabError("full-video Worker and M4.7 roots must be paired")
|
||||||
|
route_video: dict[str, object] | None = None
|
||||||
|
route_video_archive: Path | None = None
|
||||||
|
video_result: dict[str, Any] | None = None
|
||||||
|
if ddrnet_ravnoves_video_root is not None and m47_reference_graph_lab_root is not None:
|
||||||
|
video_root = ddrnet_ravnoves_video_root.resolve()
|
||||||
|
video_result = _read_worker_result(
|
||||||
|
video_root,
|
||||||
|
candidate="ddrnet",
|
||||||
|
mode=_VIDEO_MODE,
|
||||||
|
)
|
||||||
|
route_video, route_video_archive = _validated_video_semantics(video_root, video_result)
|
||||||
|
m47 = read_m47_reference_graph_lab(m47_reference_graph_lab_root)
|
||||||
|
m47_source = _object(m47.report.get("source"), "M4.7 source")
|
||||||
|
m47_visual = _object(m47.report.get("visual_evidence"), "M4.7 visual evidence")
|
||||||
|
if (
|
||||||
|
m47_source.get("source_id") != "RAVNOVES00"
|
||||||
|
or m47_visual.get("linked_result_id") != route_video["base_m4_result_id"]
|
||||||
|
or m47_visual.get("timeline_frames") != _VIDEO_FRAME_COUNT
|
||||||
|
):
|
||||||
|
raise VegetationShadowLabError("M4.7 video binding differs from DDRNet source")
|
||||||
|
route_video["m47_reference_graph_result_id"] = m47.result_id
|
||||||
|
|
||||||
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-vegetation-", dir=output_root))
|
temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-vegetation-", dir=output_root))
|
||||||
@@ -315,6 +451,34 @@ def seal_vegetation_shadow_lab(
|
|||||||
"path": relative,
|
"path": relative,
|
||||||
"sha256": descriptor["sha256"],
|
"sha256": descriptor["sha256"],
|
||||||
}
|
}
|
||||||
|
if video_result is not None and route_video is not None and route_video_archive is not None:
|
||||||
|
video_root = ddrnet_ravnoves_video_root.resolve() # type: ignore[union-attr]
|
||||||
|
worker_descriptor = _copy_artifact(
|
||||||
|
video_root / "result.json",
|
||||||
|
temporary,
|
||||||
|
"worker/ddrnet-ravnoves-video.json",
|
||||||
|
artifacts,
|
||||||
|
role="worker-result",
|
||||||
|
media_type="application/json",
|
||||||
|
)
|
||||||
|
worker_proofs["ddrnet_ravnoves_video"] = {
|
||||||
|
"result_id": video_result.get("result_id"),
|
||||||
|
"path": worker_descriptor["path"],
|
||||||
|
"sha256": worker_descriptor["sha256"],
|
||||||
|
}
|
||||||
|
archive_descriptor = _copy_artifact(
|
||||||
|
route_video_archive,
|
||||||
|
temporary,
|
||||||
|
"video/ddrnet-semantic-masks.zip",
|
||||||
|
artifacts,
|
||||||
|
role="route-semantic-mask-archive",
|
||||||
|
media_type="application/zip",
|
||||||
|
)
|
||||||
|
route_video["mask_archive"] = {
|
||||||
|
"path": archive_descriptor["path"],
|
||||||
|
"sha256": archive_descriptor["sha256"],
|
||||||
|
"byte_length": archive_descriptor["byte_length"],
|
||||||
|
}
|
||||||
|
|
||||||
candidate_metrics: dict[str, object] = {}
|
candidate_metrics: dict[str, object] = {}
|
||||||
for candidate in _CANDIDATES:
|
for candidate in _CANDIDATES:
|
||||||
@@ -348,11 +512,13 @@ def seal_vegetation_shadow_lab(
|
|||||||
"shadow_session": "RAVNOVES00",
|
"shadow_session": "RAVNOVES00",
|
||||||
"shadow_camera": "sensor.camera.right",
|
"shadow_camera": "sensor.camera.right",
|
||||||
"shadow_frame_count": 12,
|
"shadow_frame_count": 12,
|
||||||
|
"video_shadow_frame_count": _VIDEO_FRAME_COUNT if route_video else 0,
|
||||||
},
|
},
|
||||||
"selected_candidate": selected,
|
"selected_candidate": selected,
|
||||||
"candidate_metrics": candidate_metrics,
|
"candidate_metrics": candidate_metrics,
|
||||||
"worker_proofs": worker_proofs,
|
"worker_proofs": worker_proofs,
|
||||||
"visual_catalog_sha256": hashlib.sha256(canonical_json(catalogs)).hexdigest(),
|
"visual_catalog_sha256": hashlib.sha256(canonical_json(catalogs)).hexdigest(),
|
||||||
|
"route_video": route_video,
|
||||||
"authority": authority,
|
"authority": authority,
|
||||||
}
|
}
|
||||||
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
|
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
|
||||||
@@ -366,6 +532,7 @@ def seal_vegetation_shadow_lab(
|
|||||||
"status": "visual-shadow-ready-policy-not-authorized",
|
"status": "visual-shadow-ready-policy-not-authorized",
|
||||||
"identity": identity,
|
"identity": identity,
|
||||||
"source": identity["source"],
|
"source": identity["source"],
|
||||||
|
"route_video": route_video,
|
||||||
"method": {
|
"method": {
|
||||||
"completeness": "complete",
|
"completeness": "complete",
|
||||||
"execution_class": "ai-inference",
|
"execution_class": "ai-inference",
|
||||||
@@ -375,13 +542,14 @@ def seal_vegetation_shadow_lab(
|
|||||||
"decision": {
|
"decision": {
|
||||||
"selected_candidate": selected,
|
"selected_candidate": selected,
|
||||||
"visual_shadow_ready": True,
|
"visual_shadow_ready": True,
|
||||||
|
"full_video_shadow_ready": route_video is not None,
|
||||||
"mission_policy_ready_for_configuration": True,
|
"mission_policy_ready_for_configuration": True,
|
||||||
"navigation_accepted": False,
|
"navigation_accepted": False,
|
||||||
"production_accepted": False,
|
"production_accepted": False,
|
||||||
},
|
},
|
||||||
"limitations": [
|
"limitations": [
|
||||||
"GOOSE validation is external-domain qualification, not RAVNOVES ground truth.",
|
"GOOSE validation is external-domain qualification, not RAVNOVES ground truth.",
|
||||||
"The RAVNOVES shadow remains in Worker proofs and is not catalogued as vegetation evidence because it has no independent labels.",
|
"The full RAVNOVES DDRNet playback is prediction-only and has no independent labels.",
|
||||||
"Vegetation semantics never clears rigid LiDAR/TGS occupancy.",
|
"Vegetation semantics never clears rigid LiDAR/TGS occupancy.",
|
||||||
"Undefined pixels outside the 600x600 center crop remain fail-closed.",
|
"Undefined pixels outside the 600x600 center crop remain fail-closed.",
|
||||||
],
|
],
|
||||||
@@ -407,6 +575,8 @@ def _parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--ddrnet-ravnoves-root", type=Path, required=True)
|
parser.add_argument("--ddrnet-ravnoves-root", type=Path, required=True)
|
||||||
parser.add_argument("--ppliteseg-ravnoves-root", type=Path, required=True)
|
parser.add_argument("--ppliteseg-ravnoves-root", type=Path, required=True)
|
||||||
parser.add_argument("--output-root", type=Path, required=True)
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--ddrnet-ravnoves-video-root", type=Path)
|
||||||
|
parser.add_argument("--m47-reference-graph-lab-root", type=Path)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -418,6 +588,8 @@ def main() -> None:
|
|||||||
ddrnet_ravnoves_root=args.ddrnet_ravnoves_root,
|
ddrnet_ravnoves_root=args.ddrnet_ravnoves_root,
|
||||||
ppliteseg_ravnoves_root=args.ppliteseg_ravnoves_root,
|
ppliteseg_ravnoves_root=args.ppliteseg_ravnoves_root,
|
||||||
output_root=args.output_root,
|
output_root=args.output_root,
|
||||||
|
ddrnet_ravnoves_video_root=args.ddrnet_ravnoves_video_root,
|
||||||
|
m47_reference_graph_lab_root=args.m47_reference_graph_lab_root,
|
||||||
)
|
)
|
||||||
print(destination)
|
print(destination)
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import zipfile
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse, Response
|
||||||
|
|
||||||
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
|
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
|
||||||
from k1link.laboratory.evidence_report import (
|
from k1link.laboratory.evidence_report import (
|
||||||
@@ -86,6 +88,48 @@ def build_vegetation_shadow_lab_router(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.get("/{result_id}/masks/{sequence}")
|
||||||
|
def get_video_mask(result_id: str, sequence: int) -> Response:
|
||||||
|
candidate = _resolve_candidate(root_provider, result_id)
|
||||||
|
manifest = _read_verified(candidate)
|
||||||
|
route_video = manifest.get("route_video")
|
||||||
|
if not isinstance(route_video, dict) or not 0 <= sequence < 4489:
|
||||||
|
raise HTTPException(status_code=404, detail="Vegetation video mask not found")
|
||||||
|
archive = route_video.get("mask_archive")
|
||||||
|
if not isinstance(archive, dict) or archive.get("path") != "video/ddrnet-semantic-masks.zip":
|
||||||
|
raise HTTPException(status_code=404, detail="Vegetation video mask not found")
|
||||||
|
archive_path = candidate / "video" / "ddrnet-semantic-masks.zip"
|
||||||
|
member = f"masks/frame-{sequence + 1:06d}.png"
|
||||||
|
try:
|
||||||
|
before = archive_path.stat()
|
||||||
|
with zipfile.ZipFile(archive_path) as frozen:
|
||||||
|
info = frozen.getinfo(member)
|
||||||
|
if info.is_dir() or info.file_size < 8 or info.file_size > 1024 * 1024:
|
||||||
|
raise ValueError("Vegetation video mask member is invalid")
|
||||||
|
payload = frozen.read(info)
|
||||||
|
after = archive_path.stat()
|
||||||
|
if (
|
||||||
|
before.st_size != after.st_size
|
||||||
|
or before.st_mtime_ns != after.st_mtime_ns
|
||||||
|
or len(payload) != info.file_size
|
||||||
|
):
|
||||||
|
raise ValueError("Vegetation video mask archive changed during read")
|
||||||
|
except (KeyError, OSError, ValueError, zipfile.BadZipFile):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Vegetation video mask failed verification",
|
||||||
|
) from None
|
||||||
|
digest = hashlib.sha256(payload).hexdigest()
|
||||||
|
return Response(
|
||||||
|
content=payload,
|
||||||
|
media_type="image/png",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "private, max-age=31536000, immutable",
|
||||||
|
"ETag": f'"{digest}"',
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from k1link.laboratory import LaboratoryEvidenceRegistry
|
from k1link.laboratory import LaboratoryEvidenceRegistry
|
||||||
|
import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module
|
||||||
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||||
from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab
|
from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab
|
||||||
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
|
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
|
||||||
@@ -87,19 +90,98 @@ def _worker_result(root: Path, *, candidate: str, mode: str, vegetation_iou: flo
|
|||||||
(root / "result.json").write_text(json.dumps(payload), encoding="utf-8")
|
(root / "result.json").write_text(json.dumps(payload), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path) -> None:
|
def _video_worker_result(root: Path) -> None:
|
||||||
|
root.mkdir(parents=True)
|
||||||
|
archive = root / "semantic-masks.zip"
|
||||||
|
mask = b"\x89PNG\r\n\x1a\n"
|
||||||
|
with zipfile.ZipFile(archive, "x", compression=zipfile.ZIP_STORED) as frozen:
|
||||||
|
for sequence in range(4489):
|
||||||
|
frozen.writestr(f"masks/frame-{sequence + 1:06d}.png", mask)
|
||||||
|
taxonomy = {
|
||||||
|
"schema_version": "missioncore.lab-v1-vegetation-taxonomy/v1",
|
||||||
|
"classes": [
|
||||||
|
{
|
||||||
|
"class_id": class_id,
|
||||||
|
"label": "undefined" if class_id == 0 else f"class-{class_id}",
|
||||||
|
"color_rgb": [class_id, class_id, class_id],
|
||||||
|
"disposition": "undefined" if class_id == 0 else "prediction",
|
||||||
|
}
|
||||||
|
for class_id in range(64)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"schema_version": "missioncore.lab-v1-goose-vegetation-run/v1",
|
||||||
|
"result_id": f"lab-v1-ravnoves-video-ddrnet-{'e' * 64}",
|
||||||
|
"mode": "ravnoves-video",
|
||||||
|
"candidate": {"candidate_key": "ddrnet"},
|
||||||
|
"source": {
|
||||||
|
"input_count": 4489,
|
||||||
|
"ground_truth_available": False,
|
||||||
|
},
|
||||||
|
"video_semantics": {
|
||||||
|
"base_m4_result_id": f"m4-threat-replay-{'f' * 64}",
|
||||||
|
"mask_archive": {
|
||||||
|
"path": "semantic-masks.zip",
|
||||||
|
"sha256": _sha256(archive),
|
||||||
|
"byte_length": archive.stat().st_size,
|
||||||
|
"frame_count": 4489,
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"encoding": "uint8-class-id-png",
|
||||||
|
"media_type": "application/zip",
|
||||||
|
"sequence_binding": "sequence-0-to-masks/frame-000001.png",
|
||||||
|
},
|
||||||
|
"taxonomy": taxonomy,
|
||||||
|
"aggregate_prediction_pixels": [4489 * 800 * 600, *([0] * 63)],
|
||||||
|
"center_crop_xyxy": [100, 0, 700, 600],
|
||||||
|
"outside_crop_state": "undefined",
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"navigation_accepted": False,
|
||||||
|
"safety_accepted": False,
|
||||||
|
"actuation_accepted": False,
|
||||||
|
"camera_semantics_can_clear_rigid_geometry": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(root / "result.json").write_text(json.dumps(payload), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch,
|
||||||
|
) -> None:
|
||||||
roots = {}
|
roots = {}
|
||||||
for candidate, vegetation_iou in (("ddrnet", 0.64), ("ppliteseg", 0.61)):
|
for candidate, vegetation_iou in (("ddrnet", 0.64), ("ppliteseg", 0.61)):
|
||||||
for mode in ("goose", "ravnoves"):
|
for mode in ("goose", "ravnoves"):
|
||||||
root = tmp_path / "worker" / f"{candidate}-{mode}"
|
root = tmp_path / "worker" / f"{candidate}-{mode}"
|
||||||
_worker_result(root, candidate=candidate, mode=mode, vegetation_iou=vegetation_iou)
|
_worker_result(root, candidate=candidate, mode=mode, vegetation_iou=vegetation_iou)
|
||||||
roots[(candidate, mode)] = root
|
roots[(candidate, mode)] = root
|
||||||
|
video_root = tmp_path / "worker" / "ddrnet-ravnoves-video"
|
||||||
|
_video_worker_result(video_root)
|
||||||
|
m47_root = tmp_path / f"m47-reference-graph-lab-{'a' * 64}"
|
||||||
|
m47_root.mkdir()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
vegetation_lab_module,
|
||||||
|
"read_m47_reference_graph_lab",
|
||||||
|
lambda _root: SimpleNamespace(
|
||||||
|
result_id=m47_root.name,
|
||||||
|
report={
|
||||||
|
"source": {"source_id": "RAVNOVES00"},
|
||||||
|
"visual_evidence": {
|
||||||
|
"linked_result_id": f"m4-threat-replay-{'f' * 64}",
|
||||||
|
"timeline_frames": 4489,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
result_root = seal_vegetation_shadow_lab(
|
result_root = seal_vegetation_shadow_lab(
|
||||||
ddrnet_goose_root=roots[("ddrnet", "goose")],
|
ddrnet_goose_root=roots[("ddrnet", "goose")],
|
||||||
ppliteseg_goose_root=roots[("ppliteseg", "goose")],
|
ppliteseg_goose_root=roots[("ppliteseg", "goose")],
|
||||||
ddrnet_ravnoves_root=roots[("ddrnet", "ravnoves")],
|
ddrnet_ravnoves_root=roots[("ddrnet", "ravnoves")],
|
||||||
ppliteseg_ravnoves_root=roots[("ppliteseg", "ravnoves")],
|
ppliteseg_ravnoves_root=roots[("ppliteseg", "ravnoves")],
|
||||||
output_root=tmp_path / "results",
|
output_root=tmp_path / "results",
|
||||||
|
ddrnet_ravnoves_video_root=video_root,
|
||||||
|
m47_reference_graph_lab_root=m47_root,
|
||||||
)
|
)
|
||||||
manifest = json.loads((result_root / "result.json").read_text("utf-8"))
|
manifest = json.loads((result_root / "result.json").read_text("utf-8"))
|
||||||
assert manifest["decision"]["selected_candidate"] == "ddrnet"
|
assert manifest["decision"]["selected_candidate"] == "ddrnet"
|
||||||
@@ -108,7 +190,9 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
|
|||||||
assert manifest["authority"]["navigation_or_safety_accepted"] is False
|
assert manifest["authority"]["navigation_or_safety_accepted"] is False
|
||||||
assert len(manifest["catalogs"]["ravnoves"]) == 0
|
assert len(manifest["catalogs"]["ravnoves"]) == 0
|
||||||
assert len(manifest["catalogs"]["goose"]) == 12
|
assert len(manifest["catalogs"]["goose"]) == 12
|
||||||
assert len(manifest["artifacts"]) == 76
|
assert len(manifest["artifacts"]) == 78
|
||||||
|
assert manifest["route_video"]["frame_count"] == 4489
|
||||||
|
assert manifest["route_video"]["outside_crop_state"] == "undefined"
|
||||||
assert manifest["catalogs"]["goose"][0]["focus"]["class_name"] == "high_grass"
|
assert manifest["catalogs"]["goose"][0]["focus"]["class_name"] == "high_grass"
|
||||||
assert "ddrnet_error" in manifest["catalogs"]["goose"][0]["assets"]
|
assert "ddrnet_error" in manifest["catalogs"]["goose"][0]["assets"]
|
||||||
assert "ppliteseg_error" in manifest["catalogs"]["goose"][0]["assets"]
|
assert "ppliteseg_error" in manifest["catalogs"]["goose"][0]["assets"]
|
||||||
@@ -121,7 +205,7 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
|
|||||||
)
|
)
|
||||||
proof = verify_laboratory_evidence_result(definition, result_root)
|
proof = verify_laboratory_evidence_result(definition, result_root)
|
||||||
assert proof["result_id"] == result_root.name
|
assert proof["result_id"] == result_root.name
|
||||||
assert proof["artifact_count"] == 76
|
assert proof["artifact_count"] == 78
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.include_router(build_vegetation_shadow_lab_router(root_provider=lambda: result_root.parent))
|
app.include_router(build_vegetation_shadow_lab_router(root_provider=lambda: result_root.parent))
|
||||||
@@ -135,6 +219,10 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
|
|||||||
)
|
)
|
||||||
assert asset.status_code == 200
|
assert asset.status_code == 200
|
||||||
assert asset.headers["cache-control"].endswith("immutable")
|
assert asset.headers["cache-control"].endswith("immutable")
|
||||||
|
mask = client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}/masks/0")
|
||||||
|
assert mask.status_code == 200
|
||||||
|
assert mask.content == b"\x89PNG\r\n\x1a\n"
|
||||||
|
assert mask.headers["cache-control"].endswith("immutable")
|
||||||
|
|
||||||
(result_root / asset_path).write_bytes(b"tampered")
|
(result_root / asset_path).write_bytes(b"tampered")
|
||||||
assert (
|
assert (
|
||||||
|
|||||||
Reference in New Issue
Block a user