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>>;
|
||||
}
|
||||
|
||||
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,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user