feat(lidar): qualify Patchwork++ on GOOSE

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 15:22:28 +03:00
parent 951b40c870
commit 60ba64004b
15 changed files with 1082 additions and 256 deletions
@@ -90,25 +90,43 @@ export interface DatasetNativeScanPreview {
}>;
}
export interface DatasetGroundMetrics {
precision: number;
recall: number;
f1: number;
groundIou: number;
accuracy: number;
artificialGroundRecall: number;
naturalGroundRecall: number;
obstacleNonGroundRecall: number;
}
export interface DatasetGroundComparison {
sourceId: "goose-3d/v2025-08-22";
frameId: string;
pointCount: number;
currentGround: number[];
patchworkGround: number[];
patchworkAssigned: number[];
groundTruthGround: number[];
evaluated: number[];
disagreement: number[];
currentDisagreement: number[];
patchworkDisagreement: number[];
metrics: {
precision: number;
recall: number;
f1: number;
groundIou: number;
accuracy: number;
artificialGroundRecall: number;
naturalGroundRecall: number;
obstacleNonGroundRecall: number;
current: DatasetGroundMetrics;
patchworkpp: DatasetGroundMetrics;
};
latencyMs: {
current: number;
patchworkpp: number;
};
patchworkAssignedFraction: number;
inputProfile: {
frameId: "sensor/lidar/vls128_roof";
sensorHeightM: number;
heightEvidence: "published-dimensioned-schematic";
normalizedScanProduced: false;
};
latencyMs: number;
}
export class DatasetGatewayContractError extends Error {}
@@ -500,7 +518,7 @@ export function parseDatasetGroundComparison(
): DatasetGroundComparison {
const source = record(value, "Ground comparison");
if (
source.schema_version !== "missioncore.dataset-ground-comparison-preview/v1"
source.schema_version !== "missioncore.dataset-ground-comparison-preview/v2"
|| source.source_id !== "goose-3d/v2025-08-22"
|| source.sampling !== "deterministic-even-index"
) {
@@ -508,41 +526,135 @@ export function parseDatasetGroundComparison(
}
const pointCount = number(source.point_count, "point_count");
const currentGround = integers(source.current_ground, "current_ground", 1);
const patchworkGround = integers(source.patchwork_ground, "patchwork_ground", 1);
const patchworkAssigned = integers(
source.patchwork_assigned,
"patchwork_assigned",
1,
);
const groundTruthGround = integers(
source.ground_truth_ground,
"ground_truth_ground",
1,
);
const evaluated = integers(source.evaluated, "evaluated", 1);
const disagreement = integers(source.disagreement, "disagreement", 1);
const currentDisagreement = integers(
source.current_disagreement,
"current_disagreement",
1,
);
const patchworkDisagreement = integers(
source.patchwork_disagreement,
"patchwork_disagreement",
1,
);
if (
pointCount < 1
|| pointCount > 50_000
|| currentGround.length !== pointCount
|| patchworkGround.length !== pointCount
|| patchworkAssigned.length !== pointCount
|| groundTruthGround.length !== pointCount
|| evaluated.length !== pointCount
|| disagreement.length !== pointCount
|| currentDisagreement.length !== pointCount
|| patchworkDisagreement.length !== pointCount
) {
throw new DatasetGatewayContractError("Ground comparison arrays не выровнены");
}
const metrics = record(source.metrics, "metrics");
const fraction = (key: string): number => {
const value = number(metrics[key], `metrics.${key}`);
if (value > 1) {
throw new DatasetGatewayContractError(`metrics.${key}: ожидалась доля`);
}
return value;
const parseMetrics = (
value: unknown,
label: string,
): DatasetGroundMetrics => {
const providerMetrics = record(value, label);
const fraction = (key: string): number => {
const result = number(providerMetrics[key], `${label}.${key}`);
if (result > 1) {
throw new DatasetGatewayContractError(`${label}.${key}: ожидалась доля`);
}
return result;
};
return {
precision: fraction("precision"),
recall: fraction("recall"),
f1: fraction("f1"),
groundIou: fraction("ground_iou"),
accuracy: fraction("accuracy"),
artificialGroundRecall: fraction("artificial_ground_recall"),
naturalGroundRecall: fraction("natural_ground_recall"),
obstacleNonGroundRecall: fraction("obstacle_non_ground_recall"),
};
};
const provider = record(source.provider, "provider");
const currentMetrics = parseMetrics(metrics.current, "metrics.current");
const patchworkMetrics = parseMetrics(
metrics.patchworkpp,
"metrics.patchworkpp",
);
const providers = record(source.providers, "providers");
const currentProvider = record(providers.current, "providers.current");
const patchworkProvider = record(providers.patchworkpp, "providers.patchworkpp");
if (
provider.provider_id !== "missioncore-local-percentile-ground/v1"
|| provider.ground_truth !== false
currentProvider.provider_id !== "missioncore-local-percentile-ground/v1"
|| currentProvider.ground_truth !== false
|| !/^[a-f0-9]{64}$/.test(
string(provider.implementation_sha256, "provider.implementation_sha256"),
string(
currentProvider.implementation_sha256,
"providers.current.implementation_sha256",
),
)
|| patchworkProvider.provider_id !== "patchworkpp/v1.4.1"
|| patchworkProvider.source_tag !== "v1.4.1"
|| patchworkProvider.source_commit
!== "3e6903a1d5537a4cc2ace897b0bbb98a92d6014c"
|| patchworkProvider.ground_truth !== false
|| !/^[a-f0-9]{64}$/.test(
string(
patchworkProvider.binary_sha256,
"providers.patchworkpp.binary_sha256",
),
)
) {
throw new DatasetGatewayContractError("Ground comparison provider несовместим");
}
const latency = record(source.latency_ms, "latency_ms");
const inputProfile = record(source.input_profile, "input_profile");
const sensorFrame = record(inputProfile.sensor_frame, "input_profile.sensor_frame");
const height = record(inputProfile.height, "input_profile.height");
const scope = record(inputProfile.scope, "input_profile.scope");
const sensorHeightM = number(
height.sensor_above_ground_m,
"height.sensor_above_ground_m",
);
if (
inputProfile.schema_version !== "missioncore.goose-patchwork-profile/v1"
|| inputProfile.source_id !== "goose-3d/v2025-08-22"
|| inputProfile.representation !== "native-scan"
|| sensorFrame.frame_id !== "sensor/lidar/vls128_roof"
|| sensorFrame.handedness !== "right"
|| sensorFrame.x !== "forward"
|| sensorFrame.y !== "left"
|| sensorFrame.z !== "up"
|| sensorFrame.one_revolution !== true
|| height.base_link_above_ground_m !== 0.64
|| height.lidar_above_base_link_m !== 1.6
|| sensorHeightM !== 2.24
|| height.evidence !== "published-dimensioned-schematic"
|| scope.patchworkpp_eligible !== true
|| scope.normalized_scan_produced !== false
|| scope.complete_vehicle_transform_known !== false
|| scope.deskew_claimed !== false
) {
throw new DatasetGatewayContractError("GOOSE Patchwork++ profile несовместим");
}
const patchworkAssignedFraction = number(
source.patchwork_assigned_fraction,
"patchwork_assigned_fraction",
);
if (patchworkAssignedFraction > 1) {
throw new DatasetGatewayContractError(
"patchwork_assigned_fraction: ожидалась доля",
);
}
const safety = record(source.safety, "safety");
if (
safety.qualification_only !== true
@@ -555,20 +667,27 @@ export function parseDatasetGroundComparison(
frameId: string(source.frame_id, "frame_id", true),
pointCount,
currentGround,
patchworkGround,
patchworkAssigned,
groundTruthGround,
evaluated,
disagreement,
currentDisagreement,
patchworkDisagreement,
metrics: {
precision: fraction("precision"),
recall: fraction("recall"),
f1: fraction("f1"),
groundIou: fraction("ground_iou"),
accuracy: fraction("accuracy"),
artificialGroundRecall: fraction("artificial_ground_recall"),
naturalGroundRecall: fraction("natural_ground_recall"),
obstacleNonGroundRecall: fraction("obstacle_non_ground_recall"),
current: currentMetrics,
patchworkpp: patchworkMetrics,
},
latencyMs: {
current: number(latency.current, "latency_ms.current"),
patchworkpp: number(latency.patchworkpp, "latency_ms.patchworkpp"),
},
patchworkAssignedFraction,
inputProfile: {
frameId: "sensor/lidar/vls128_roof",
sensorHeightM,
heightEvidence: "published-dimensioned-schematic",
normalizedScanProduced: false,
},
latencyMs: number(source.latency_ms, "latency_ms"),
};
}
@@ -138,9 +138,11 @@ export function DatasetGatewayWorkspace() {
masks: {
currentGround: comparisonAligned?.currentGround ?? emptyMask,
currentAssigned: comparisonAligned?.evaluated ?? emptyMask,
candidateGround: comparisonAligned?.groundTruthGround ?? emptyMask,
candidateAssigned: comparisonAligned?.evaluated ?? emptyMask,
disagreement: comparisonAligned?.disagreement ?? emptyMask,
candidateGround: comparisonAligned?.patchworkGround ?? emptyMask,
candidateAssigned: comparisonAligned?.patchworkAssigned ?? emptyMask,
disagreement: comparisonAligned?.currentDisagreement ?? emptyMask,
candidateDisagreement:
comparisonAligned?.patchworkDisagreement ?? emptyMask,
groundTruthGround: preview.groundTruthGround,
},
};
@@ -301,7 +303,9 @@ export function DatasetGatewayWorkspace() {
...(comparison
? ([
["current", "Current"],
["disagreement", "Ошибки"],
["candidate", "Patchwork++"],
["disagreement", "Ошибки Current"],
["candidate-disagreement", "Ошибки PW++"],
] as const)
: []),
["intensity", "Remission"],
@@ -320,20 +324,42 @@ export function DatasetGatewayWorkspace() {
{comparison ? (
<dl className="dataset-preview__metrics">
<div>
<dt>Ground IoU</dt>
<dd>{(comparison.metrics.groundIou * 100).toFixed(1)}%</dd>
<dt>Current · IoU</dt>
<dd>{(comparison.metrics.current.groundIou * 100).toFixed(1)}%</dd>
</div>
<div>
<dt>Precision</dt>
<dd>{(comparison.metrics.precision * 100).toFixed(1)}%</dd>
<dt>Current · Recall</dt>
<dd>{(comparison.metrics.current.recall * 100).toFixed(1)}%</dd>
</div>
<div>
<dt>Recall</dt>
<dd>{(comparison.metrics.recall * 100).toFixed(1)}%</dd>
<dt>Current · Natural</dt>
<dd>
{(comparison.metrics.current.naturalGroundRecall * 100).toFixed(1)}%
</dd>
</div>
<div>
<dt>Latency</dt>
<dd>{comparison.latencyMs.toFixed(0)} ms</dd>
<dt>Current · Latency</dt>
<dd>{comparison.latencyMs.current.toFixed(0)} ms</dd>
</div>
<div>
<dt>Patchwork++ · IoU</dt>
<dd>
{(comparison.metrics.patchworkpp.groundIou * 100).toFixed(1)}%
</dd>
</div>
<div>
<dt>Patchwork++ · Recall</dt>
<dd>{(comparison.metrics.patchworkpp.recall * 100).toFixed(1)}%</dd>
</div>
<div>
<dt>Patchwork++ · Natural</dt>
<dd>
{(comparison.metrics.patchworkpp.naturalGroundRecall * 100).toFixed(1)}%
</dd>
</div>
<div>
<dt>Patchwork++ · Latency</dt>
<dd>{comparison.latencyMs.patchworkpp.toFixed(1)} ms</dd>
</div>
</dl>
) : comparisonError ? (
@@ -362,9 +388,19 @@ export function DatasetGatewayWorkspace() {
<span><i className="dataset-legend-ground" />current ground</span>
<span><i className="dataset-legend-other" />current non-ground</span>
</>
) : previewMode === "candidate" ? (
<>
<span><i className="dataset-legend-ground" />Patchwork++ ground</span>
<span><i className="dataset-legend-other" />Patchwork++ non-ground</span>
</>
) : previewMode === "disagreement" ? (
<>
<span><i className="dataset-legend-error" />ошибка относительно labels</span>
<span><i className="dataset-legend-error" />ошибка Current</span>
<span><i className="dataset-legend-other" />совпадение с labels</span>
</>
) : previewMode === "candidate-disagreement" ? (
<>
<span><i className="dataset-legend-error" />ошибка Patchwork++</span>
<span><i className="dataset-legend-other" />совпадение</span>
</>
) : (
@@ -380,6 +416,14 @@ export function DatasetGatewayWorkspace() {
<p>{previewError ?? "Проверяем point alignment и разметку."}</p>
</div>
)}
{comparison ? (
<p className="dataset-preview__comparison-note">
Patchwork++ использует опубликованную высоту VLS-128{" "}
{comparison.inputProfile.sensorHeightM.toFixed(2)} м; это
algorithm-specific admission, а не полный vehicle TF и не
normalized scan.
</p>
) : null}
</section>
) : null}
@@ -7,6 +7,7 @@ export type LidarGroundViewMode =
| "current"
| "candidate"
| "disagreement"
| "candidate-disagreement"
| "semantic"
| "ground-truth";
@@ -21,6 +22,7 @@ export interface LidarGroundPointCloudFrame {
candidateGround: number[];
candidateAssigned: number[];
disagreement: number[];
candidateDisagreement?: number[];
groundTruthGround?: number[];
};
}
@@ -119,6 +121,15 @@ function frameColors(
disagreement ? 0.31 : 0.29,
disagreement ? 0.22 : 0.35,
);
} else if (mode === "candidate-disagreement") {
const disagreement = frame.masks.candidateDisagreement?.[index] === 1;
setRgb(
colors,
offset,
disagreement ? 1 : 0.24,
disagreement ? 0.31 : 0.29,
disagreement ? 0.22 : 0.35,
);
} else {
setRgb(colors, offset, candidate ? 0.24 : 0.29, candidate ? 0.84 : 0.36, 0.43);
}
@@ -194,36 +194,97 @@ test("decodes one bounded point-aligned native-scan preview", async () => {
);
});
test("decodes a point-aligned current-vs-ground-truth comparison", async () => {
test("decodes a point-aligned current-vs-Patchwork++ comparison", async () => {
const payload = {
schema_version: "missioncore.dataset-ground-comparison-preview/v1",
schema_version: "missioncore.dataset-ground-comparison-preview/v2",
source_id: "goose-3d/v2025-08-22",
frame_id: "frame-1",
sampling: "deterministic-even-index",
point_count: 2,
current_ground: [1, 1],
patchwork_ground: [1, 0],
patchwork_assigned: [1, 1],
ground_truth_ground: [1, 0],
evaluated: [1, 1],
disagreement: [0, 1],
current_disagreement: [0, 1],
patchwork_disagreement: [0, 0],
metrics: {
true_positive: 1,
false_positive: 1,
false_negative: 0,
true_negative: 0,
precision: 0.5,
recall: 1,
f1: 2 / 3,
ground_iou: 0.5,
accuracy: 0.5,
artificial_ground_recall: 1,
natural_ground_recall: 0,
obstacle_non_ground_recall: 1,
current: {
true_positive: 1,
false_positive: 1,
false_negative: 0,
true_negative: 0,
precision: 0.5,
recall: 1,
f1: 2 / 3,
ground_iou: 0.5,
accuracy: 0.5,
artificial_ground_recall: 1,
natural_ground_recall: 0,
obstacle_non_ground_recall: 1,
},
patchworkpp: {
true_positive: 1,
false_positive: 0,
false_negative: 0,
true_negative: 1,
precision: 1,
recall: 1,
f1: 1,
ground_iou: 1,
accuracy: 1,
artificial_ground_recall: 1,
natural_ground_recall: 1,
obstacle_non_ground_recall: 1,
},
},
latency_ms: 12.5,
provider: {
provider_id: "missioncore-local-percentile-ground/v1",
implementation_sha256: "a".repeat(64),
ground_truth: false,
latency_ms: {
current: 12.5,
patchworkpp: 0.4,
},
patchwork_assigned_fraction: 1,
providers: {
current: {
provider_id: "missioncore-local-percentile-ground/v1",
implementation_sha256: "a".repeat(64),
ground_truth: false,
},
patchworkpp: {
provider_id: "patchworkpp/v1.4.1",
source_url: "https://github.com/url-kaist/patchwork-plusplus",
source_tag: "v1.4.1",
source_commit: "3e6903a1d5537a4cc2ace897b0bbb98a92d6014c",
binding_version: "0.0.1",
binary_sha256: "b".repeat(64),
platform: "linux",
machine: "x86_64",
ground_truth: false,
},
},
input_profile: {
schema_version: "missioncore.goose-patchwork-profile/v1",
source_id: "goose-3d/v2025-08-22",
representation: "native-scan",
sensor_frame: {
frame_id: "sensor/lidar/vls128_roof",
handedness: "right",
x: "forward",
y: "left",
z: "up",
one_revolution: true,
},
height: {
base_link_above_ground_m: 0.64,
lidar_above_base_link_m: 1.6,
sensor_above_ground_m: 2.24,
evidence: "published-dimensioned-schematic",
},
scope: {
patchworkpp_eligible: true,
normalized_scan_produced: false,
complete_vehicle_transform_known: false,
deskew_claimed: false,
},
},
safety: {
qualification_only: true,
@@ -231,15 +292,23 @@ test("decodes a point-aligned current-vs-ground-truth comparison", async () => {
},
};
const parsed = parseDatasetGroundComparison(payload);
assert.equal(parsed.metrics.groundIou, 0.5);
assert.deepEqual(parsed.disagreement, [0, 1]);
assert.equal(parsed.metrics.current.groundIou, 0.5);
assert.equal(parsed.metrics.patchworkpp.groundIou, 1);
assert.deepEqual(parsed.patchworkDisagreement, [0, 0]);
assert.equal(parsed.inputProfile.sensorHeightM, 2.24);
const fetched = await fetchDatasetGroundComparison({
fetcher: async () => new Response(JSON.stringify(payload), { status: 200 }),
});
assert.equal(fetched.latencyMs, 12.5);
assert.equal(fetched.latencyMs.patchworkpp, 0.4);
payload.disagreement.pop();
payload.patchwork_disagreement.pop();
assert.throws(
() => parseDatasetGroundComparison(payload),
DatasetGatewayContractError,
);
payload.patchwork_disagreement.push(0);
payload.input_profile.height.sensor_above_ground_m = 2.25;
assert.throws(
() => parseDatasetGroundComparison(payload),
DatasetGatewayContractError,