feat(perception): add DDRNet full-video vegetation replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 01:13:18 +03:00
parent 67e6ae98e2
commit 30080c51aa
10 changed files with 729 additions and 64 deletions
@@ -51,6 +51,26 @@ export interface VegetationVisualCase {
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 {
resultId: string;
createdAtUtc: string;
@@ -59,6 +79,7 @@ export interface VegetationShadowResult {
candidates: readonly VegetationCandidateMetrics[];
routeCases: readonly VegetationVisualCase[];
validationCases: readonly VegetationVisualCase[];
routeVideo: VegetationRouteVideo | null;
limitations: readonly string[];
visualShadowReady: 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 {
const payload = objectValue(value, "Vegetation LAB");
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)),
routeCases,
validationCases,
routeVideo: routeVideoValue(payload.route_video),
limitations: arrayValue(payload.limitations, "vegetation.limitations")
.map((item, index) => textValue(item, `vegetation.limitations[${index}]`)),
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(
resultId: string,
{