feat: qualify K1 local surface over time
This commit is contained in:
@@ -48,6 +48,28 @@ export interface LidarLocalSurfaceModel {
|
||||
confidence: LidarLocalSurfaceDistribution;
|
||||
poseBindingAgeMs: LidarLocalSurfaceDistribution;
|
||||
surfaceMaxAgeMs: LidarLocalSurfaceDistribution;
|
||||
temporalQualification: {
|
||||
prediction: {
|
||||
currentFrameExcluded: true;
|
||||
sampleCount: number;
|
||||
residualP50M: LidarLocalSurfaceDistribution;
|
||||
residualP95M: LidarLocalSurfaceDistribution;
|
||||
inlierFraction: LidarLocalSurfaceDistribution;
|
||||
};
|
||||
stability: {
|
||||
sampleCount: number;
|
||||
heightDeltaM: LidarLocalSurfaceDistribution;
|
||||
slopeDeltaDeg: LidarLocalSurfaceDistribution;
|
||||
roughnessDeltaM: LidarLocalSurfaceDistribution;
|
||||
jumpCount: number;
|
||||
};
|
||||
stepCandidates: {
|
||||
isGroundTruth: false;
|
||||
framesWithCandidates: number;
|
||||
cellCount: LidarLocalSurfaceDistribution;
|
||||
pointCount: LidarLocalSurfaceDistribution;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
anchors: LidarLocalSurfaceAnchor[];
|
||||
occupancyPolicy: {
|
||||
@@ -88,6 +110,7 @@ export interface LidarLocalSurfaceFrame {
|
||||
pointsXyzM: Array<[number, number, number]>;
|
||||
pointClass: number[];
|
||||
pointHeightM: number[];
|
||||
pointStepCandidate: number[];
|
||||
pose: {
|
||||
positionXyzM: [number, number, number];
|
||||
orientationXyzw: [number, number, number, number];
|
||||
@@ -108,12 +131,56 @@ export interface LidarLocalSurfaceFrame {
|
||||
surface: number;
|
||||
occupied: number;
|
||||
belowSurface: number;
|
||||
stepCandidate: number;
|
||||
};
|
||||
prediction: {
|
||||
available: boolean;
|
||||
currentFrameExcluded: true;
|
||||
cellCount: number;
|
||||
residualP50M: number;
|
||||
residualP95M: number;
|
||||
inlierFraction: number;
|
||||
};
|
||||
temporal: {
|
||||
compared: boolean;
|
||||
heightDeltaM: number;
|
||||
slopeDeltaDeg: number;
|
||||
roughnessDeltaM: number;
|
||||
jump: boolean;
|
||||
stepCandidateCellCount: number;
|
||||
};
|
||||
occupancyPolicy: LidarLocalSurfaceModel["occupancyPolicy"];
|
||||
groundTruth: false;
|
||||
authority: LidarLocalSurfaceModel["authority"];
|
||||
}
|
||||
|
||||
export interface LidarLocalSurfaceTimeline {
|
||||
modelId: string;
|
||||
sourcePackId: string;
|
||||
sessionId: string;
|
||||
frameCount: number;
|
||||
sourceFrameIndex: number[];
|
||||
sessionSeconds: number[];
|
||||
sourceAvailable: number[];
|
||||
valid: number[];
|
||||
predictionAvailable: number[];
|
||||
predictionResidualP50M: number[];
|
||||
predictionResidualP95M: number[];
|
||||
predictionInlierFraction: number[];
|
||||
sensorHeightM: number[];
|
||||
slopeDeg: number[];
|
||||
roughnessM: number[];
|
||||
confidence: number[];
|
||||
temporalCompared: number[];
|
||||
heightDeltaM: number[];
|
||||
slopeDeltaDeg: number[];
|
||||
roughnessDeltaM: number[];
|
||||
temporalJump: number[];
|
||||
stepCandidatePointCount: number[];
|
||||
groundTruth: false;
|
||||
authority: LidarLocalSurfaceModel["authority"];
|
||||
}
|
||||
|
||||
export class LidarLocalSurfaceContractError extends Error {}
|
||||
|
||||
export class LidarLocalSurfaceApiError extends Error {
|
||||
@@ -199,6 +266,39 @@ function tuple(
|
||||
return values;
|
||||
}
|
||||
|
||||
function finiteVector(
|
||||
value: unknown,
|
||||
length: number,
|
||||
label: string,
|
||||
): number[] {
|
||||
const values = array(value, label).map((item, index) =>
|
||||
finite(item, `${label}[${index}]`)
|
||||
);
|
||||
if (values.length !== length) {
|
||||
throw new LidarLocalSurfaceContractError(`${label}: неверная длина`);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function integerVector(
|
||||
value: unknown,
|
||||
length: number,
|
||||
label: string,
|
||||
maximum?: number,
|
||||
): number[] {
|
||||
const values = array(value, label).map((item, index) => {
|
||||
const result = integer(item, `${label}[${index}]`);
|
||||
if (maximum !== undefined && result > maximum) {
|
||||
throw new LidarLocalSurfaceContractError(`${label}: значение вне диапазона`);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
if (values.length !== length) {
|
||||
throw new LidarLocalSurfaceContractError(`${label}: неверная длина`);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function distribution(
|
||||
value: unknown,
|
||||
label: string,
|
||||
@@ -288,6 +388,90 @@ function anchor(value: unknown): LidarLocalSurfaceAnchor {
|
||||
};
|
||||
}
|
||||
|
||||
function temporalQualification(
|
||||
value: unknown,
|
||||
): LidarLocalSurfaceModel["metrics"]["temporalQualification"] {
|
||||
if (value === undefined || value === null) return null;
|
||||
const source = record(value, "temporal_qualification");
|
||||
const prediction = record(source.prediction, "temporal_qualification.prediction");
|
||||
const stability = record(source.stability, "temporal_qualification.stability");
|
||||
const steps = record(
|
||||
source.step_candidates,
|
||||
"temporal_qualification.step_candidates",
|
||||
);
|
||||
if (
|
||||
prediction.current_frame_excluded !== true
|
||||
|| steps.is_ground_truth !== false
|
||||
) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"LiDAR temporal qualification завышает evidence",
|
||||
);
|
||||
}
|
||||
const predictionSampleCount = integer(
|
||||
prediction.sample_count,
|
||||
"prediction.sample_count",
|
||||
);
|
||||
const stabilitySampleCount = integer(
|
||||
stability.sample_count,
|
||||
"stability.sample_count",
|
||||
);
|
||||
const jumpCount = integer(stability.jump_count, "stability.jump_count");
|
||||
if (jumpCount > stabilitySampleCount) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"LiDAR temporal jump count несовместим",
|
||||
);
|
||||
}
|
||||
return {
|
||||
prediction: {
|
||||
currentFrameExcluded: true,
|
||||
sampleCount: predictionSampleCount,
|
||||
residualP50M: distribution(
|
||||
prediction.residual_p50_m,
|
||||
"prediction.residual_p50_m",
|
||||
),
|
||||
residualP95M: distribution(
|
||||
prediction.residual_p95_m,
|
||||
"prediction.residual_p95_m",
|
||||
),
|
||||
inlierFraction: distribution(
|
||||
prediction.inlier_fraction,
|
||||
"prediction.inlier_fraction",
|
||||
),
|
||||
},
|
||||
stability: {
|
||||
sampleCount: stabilitySampleCount,
|
||||
heightDeltaM: distribution(
|
||||
stability.height_delta_m,
|
||||
"stability.height_delta_m",
|
||||
),
|
||||
slopeDeltaDeg: distribution(
|
||||
stability.slope_delta_deg,
|
||||
"stability.slope_delta_deg",
|
||||
),
|
||||
roughnessDeltaM: distribution(
|
||||
stability.roughness_delta_m,
|
||||
"stability.roughness_delta_m",
|
||||
),
|
||||
jumpCount,
|
||||
},
|
||||
stepCandidates: {
|
||||
isGroundTruth: false,
|
||||
framesWithCandidates: integer(
|
||||
steps.frames_with_candidates,
|
||||
"step_candidates.frames_with_candidates",
|
||||
),
|
||||
cellCount: distribution(
|
||||
steps.cell_count,
|
||||
"step_candidates.cell_count",
|
||||
),
|
||||
pointCount: distribution(
|
||||
steps.point_count,
|
||||
"step_candidates.point_count",
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function model(value: unknown): LidarLocalSurfaceModel {
|
||||
const source = record(value, "LiDAR local-surface model");
|
||||
const sourceEvidence = record(source.source, "source");
|
||||
@@ -373,6 +557,9 @@ function model(value: unknown): LidarLocalSurfaceModel {
|
||||
metrics.surface_max_age_ms,
|
||||
"surface_max_age_ms",
|
||||
),
|
||||
temporalQualification: temporalQualification(
|
||||
metrics.temporal_qualification,
|
||||
),
|
||||
},
|
||||
anchors,
|
||||
occupancyPolicy: occupancyPolicy(source.occupancy_policy),
|
||||
@@ -446,10 +633,23 @@ export function parseLidarLocalSurfaceFrame(
|
||||
const pointHeightM = array(source.point_height_m, "point_height_m").map(
|
||||
(item, index) => finite(item, `point_height_m[${index}]`),
|
||||
);
|
||||
const pointStepCandidate = array(
|
||||
source.point_step_candidate,
|
||||
"point_step_candidate",
|
||||
).map((item, index) => {
|
||||
const value = integer(item, `point_step_candidate[${index}]`);
|
||||
if (value > 1) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"Некорректная step-candidate mask",
|
||||
);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
if (
|
||||
pointsXyzM.length !== pointCount
|
||||
|| pointClass.length !== pointCount
|
||||
|| pointHeightM.length !== pointCount
|
||||
|| pointStepCandidate.length !== pointCount
|
||||
) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"LiDAR local-surface point arrays расходятся",
|
||||
@@ -457,12 +657,15 @@ export function parseLidarLocalSurfaceFrame(
|
||||
}
|
||||
const pose = record(source.pose, "pose");
|
||||
const surface = record(source.surface, "surface");
|
||||
const prediction = record(source.prediction, "prediction");
|
||||
const temporal = record(source.temporal, "temporal");
|
||||
const counts = record(source.counts, "counts");
|
||||
const parsedCounts = {
|
||||
classified: integer(counts.classified, "counts.classified"),
|
||||
surface: integer(counts.surface, "counts.surface"),
|
||||
occupied: integer(counts.occupied, "counts.occupied"),
|
||||
belowSurface: integer(counts.below_surface, "counts.below_surface"),
|
||||
stepCandidate: integer(counts.step_candidate, "counts.step_candidate"),
|
||||
};
|
||||
if (
|
||||
parsedCounts.classified !== pointClass.filter((item) => item !== 0).length
|
||||
@@ -470,6 +673,8 @@ export function parseLidarLocalSurfaceFrame(
|
||||
|| parsedCounts.occupied !== pointClass.filter((item) => item === 2).length
|
||||
|| parsedCounts.belowSurface
|
||||
!== pointClass.filter((item) => item === 3).length
|
||||
|| parsedCounts.stepCandidate
|
||||
!== pointStepCandidate.filter((item) => item === 1).length
|
||||
) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"LiDAR local-surface counts расходятся",
|
||||
@@ -486,6 +691,20 @@ export function parseLidarLocalSurfaceFrame(
|
||||
4,
|
||||
"surface.plane_coefficients_map",
|
||||
);
|
||||
if (prediction.current_frame_excluded !== true) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"Текущий кадр попал в prediction input",
|
||||
);
|
||||
}
|
||||
const predictionInlierFraction = finite(
|
||||
prediction.inlier_fraction,
|
||||
"prediction.inlier_fraction",
|
||||
);
|
||||
if (predictionInlierFraction < 0 || predictionInlierFraction > 1) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"Prediction inlier fraction несовместим",
|
||||
);
|
||||
}
|
||||
return {
|
||||
modelId: text(source.model_id, "model_id", SAFE_MODEL_ID),
|
||||
sourcePackId: text(source.source_pack_id, "source_pack_id", SAFE_PACK_ID),
|
||||
@@ -503,6 +722,7 @@ export function parseLidarLocalSurfaceFrame(
|
||||
pointsXyzM,
|
||||
pointClass,
|
||||
pointHeightM,
|
||||
pointStepCandidate,
|
||||
pose: {
|
||||
positionXyzM: [position[0], position[1], position[2]],
|
||||
orientationXyzw: [
|
||||
@@ -530,12 +750,181 @@ export function parseLidarLocalSurfaceFrame(
|
||||
),
|
||||
},
|
||||
counts: parsedCounts,
|
||||
prediction: {
|
||||
available: boolean(prediction.available, "prediction.available"),
|
||||
currentFrameExcluded: true,
|
||||
cellCount: integer(prediction.cell_count, "prediction.cell_count"),
|
||||
residualP50M: finite(
|
||||
prediction.residual_p50_m,
|
||||
"prediction.residual_p50_m",
|
||||
),
|
||||
residualP95M: finite(
|
||||
prediction.residual_p95_m,
|
||||
"prediction.residual_p95_m",
|
||||
),
|
||||
inlierFraction: predictionInlierFraction,
|
||||
},
|
||||
temporal: {
|
||||
compared: boolean(temporal.compared, "temporal.compared"),
|
||||
heightDeltaM: finite(
|
||||
temporal.height_delta_m,
|
||||
"temporal.height_delta_m",
|
||||
),
|
||||
slopeDeltaDeg: finite(
|
||||
temporal.slope_delta_deg,
|
||||
"temporal.slope_delta_deg",
|
||||
),
|
||||
roughnessDeltaM: finite(
|
||||
temporal.roughness_delta_m,
|
||||
"temporal.roughness_delta_m",
|
||||
),
|
||||
jump: boolean(temporal.jump, "temporal.jump"),
|
||||
stepCandidateCellCount: integer(
|
||||
temporal.step_candidate_cell_count,
|
||||
"temporal.step_candidate_cell_count",
|
||||
),
|
||||
},
|
||||
occupancyPolicy: occupancyPolicy(source.occupancy_policy),
|
||||
groundTruth: false,
|
||||
authority: authority(source.authority),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLidarLocalSurfaceTimeline(
|
||||
value: unknown,
|
||||
): LidarLocalSurfaceTimeline {
|
||||
const source = record(value, "LiDAR local-surface timeline");
|
||||
if (
|
||||
source.schema_version !== `${LOCAL_SURFACE_SCHEMA_PREFIX}-timeline/v1`
|
||||
|| source.access !== "read-only"
|
||||
|| source.ground_truth !== false
|
||||
) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"LiDAR local-surface timeline несовместим",
|
||||
);
|
||||
}
|
||||
const frameCount = integer(source.frame_count, "frame_count");
|
||||
if (frameCount < 1 || frameCount > 100_000) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"LiDAR local-surface timeline слишком большой",
|
||||
);
|
||||
}
|
||||
const sourceFrameIndex = integerVector(
|
||||
source.source_frame_index,
|
||||
frameCount,
|
||||
"source_frame_index",
|
||||
);
|
||||
const sessionSeconds = finiteVector(
|
||||
source.session_seconds,
|
||||
frameCount,
|
||||
"session_seconds",
|
||||
);
|
||||
const sourceAvailable = integerVector(
|
||||
source.source_available,
|
||||
frameCount,
|
||||
"source_available",
|
||||
1,
|
||||
);
|
||||
const valid = integerVector(source.valid, frameCount, "valid", 1);
|
||||
const predictionAvailable = integerVector(
|
||||
source.prediction_available,
|
||||
frameCount,
|
||||
"prediction_available",
|
||||
1,
|
||||
);
|
||||
const predictionResidualP50M = finiteVector(
|
||||
source.prediction_residual_p50_m,
|
||||
frameCount,
|
||||
"prediction_residual_p50_m",
|
||||
);
|
||||
const predictionResidualP95M = finiteVector(
|
||||
source.prediction_residual_p95_m,
|
||||
frameCount,
|
||||
"prediction_residual_p95_m",
|
||||
);
|
||||
const predictionInlierFraction = finiteVector(
|
||||
source.prediction_inlier_fraction,
|
||||
frameCount,
|
||||
"prediction_inlier_fraction",
|
||||
);
|
||||
const confidence = finiteVector(source.confidence, frameCount, "confidence");
|
||||
const temporalCompared = integerVector(
|
||||
source.temporal_compared,
|
||||
frameCount,
|
||||
"temporal_compared",
|
||||
1,
|
||||
);
|
||||
const temporalJump = integerVector(
|
||||
source.temporal_jump,
|
||||
frameCount,
|
||||
"temporal_jump",
|
||||
1,
|
||||
);
|
||||
if (
|
||||
sourceFrameIndex.some(
|
||||
(item, index) => index > 0 && item <= sourceFrameIndex[index - 1],
|
||||
)
|
||||
|| sessionSeconds.some(
|
||||
(item, index) => index > 0 && item <= sessionSeconds[index - 1],
|
||||
)
|
||||
|| predictionInlierFraction.some((item) => item < 0 || item > 1)
|
||||
|| confidence.some((item) => item < 0 || item > 1)
|
||||
|| temporalJump.some(
|
||||
(item, index) => item === 1 && temporalCompared[index] !== 1,
|
||||
)
|
||||
) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"LiDAR local-surface timeline content несовместим",
|
||||
);
|
||||
}
|
||||
return {
|
||||
modelId: text(source.model_id, "model_id", SAFE_MODEL_ID),
|
||||
sourcePackId: text(source.source_pack_id, "source_pack_id", SAFE_PACK_ID),
|
||||
sessionId: text(source.session_id, "session_id", SAFE_ID),
|
||||
frameCount,
|
||||
sourceFrameIndex,
|
||||
sessionSeconds,
|
||||
sourceAvailable,
|
||||
valid,
|
||||
predictionAvailable,
|
||||
predictionResidualP50M,
|
||||
predictionResidualP95M,
|
||||
predictionInlierFraction,
|
||||
sensorHeightM: finiteVector(
|
||||
source.sensor_height_m,
|
||||
frameCount,
|
||||
"sensor_height_m",
|
||||
),
|
||||
slopeDeg: finiteVector(source.slope_deg, frameCount, "slope_deg"),
|
||||
roughnessM: finiteVector(source.roughness_m, frameCount, "roughness_m"),
|
||||
confidence,
|
||||
temporalCompared,
|
||||
heightDeltaM: finiteVector(
|
||||
source.height_delta_m,
|
||||
frameCount,
|
||||
"height_delta_m",
|
||||
),
|
||||
slopeDeltaDeg: finiteVector(
|
||||
source.slope_delta_deg,
|
||||
frameCount,
|
||||
"slope_delta_deg",
|
||||
),
|
||||
roughnessDeltaM: finiteVector(
|
||||
source.roughness_delta_m,
|
||||
frameCount,
|
||||
"roughness_delta_m",
|
||||
),
|
||||
temporalJump,
|
||||
stepCandidatePointCount: integerVector(
|
||||
source.step_candidate_point_count,
|
||||
frameCount,
|
||||
"step_candidate_point_count",
|
||||
),
|
||||
groundTruth: false,
|
||||
authority: authority(source.authority),
|
||||
};
|
||||
}
|
||||
|
||||
async function responseJson(
|
||||
response: Response,
|
||||
fallback: string,
|
||||
@@ -600,3 +989,29 @@ export async function fetchLidarLocalSurfaceFrame(
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchLidarLocalSurfaceTimeline(
|
||||
modelId: string,
|
||||
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
|
||||
): Promise<LidarLocalSurfaceTimeline> {
|
||||
if (!SAFE_MODEL_ID.test(modelId)) {
|
||||
throw new LidarLocalSurfaceContractError(
|
||||
"Некорректный LiDAR local-surface timeline",
|
||||
);
|
||||
}
|
||||
const fetcher = options.fetcher ?? fetch;
|
||||
const response = await fetcher(
|
||||
`/api/v1/lidar/local-surfaces/${modelId}/timeline`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
signal: options.signal,
|
||||
},
|
||||
);
|
||||
return parseLidarLocalSurfaceTimeline(
|
||||
await responseJson(
|
||||
response,
|
||||
"Не удалось получить LiDAR local-surface timeline.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user