feat: add comparable localization algorithms

This commit is contained in:
2026-08-28 20:20:57 +09:00
parent 7fe846fbf9
commit d58f673402
20 changed files with 2104 additions and 68 deletions
+44 -9
View File
@@ -50,8 +50,12 @@ function normalizeRawMap(rawMap, scan) {
if (!Array.isArray(values) && !ArrayBuffer.isView(values)) {
throw new TypeError("rawMap must be an Array, a typed array, or a map descriptor with values.");
}
if (descriptor?.scale !== undefined && descriptor.scale !== "power") {
throw new RangeError("rawMap scale must be power.");
if (
descriptor?.scale !== undefined &&
descriptor.scale !== "power" &&
descriptor.scale !== "score"
) {
throw new RangeError("rawMap scale must be power or score.");
}
if (descriptor?.rows !== undefined && descriptor.rows !== scan.rows) {
throw new RangeError("rawMap rows do not match scan.rows.");
@@ -69,6 +73,13 @@ function normalizeRawMap(rawMap, scan) {
return values;
}
function rawMapScale(rawMap) {
if (rawMap && !Array.isArray(rawMap) && !ArrayBuffer.isView(rawMap)) {
return rawMap.scale ?? "power";
}
return "power";
}
function normalizeSources(sources) {
if (!Array.isArray(sources)) throw new TypeError("sources must be an array.");
const ids = new Set();
@@ -420,12 +431,14 @@ function collectHalfPowerIndices(values, scan, peakIndex, halfPowerRatio) {
return { indices: queue.subarray(0, queueEnd), thresholdValue };
}
function measureBeamSpreadFromValues(values, scan, peakIndex, halfPowerRatio) {
function measureBeamSpreadFromValues(values, scan, peakIndex, halfPowerRatio, mapScale = "power") {
const component = collectHalfPowerIndices(values, scan, peakIndex, halfPowerRatio);
const peakPosition = scanIndexToPosition(peakIndex, scan);
const base = {
calibrated: false,
method: "connected-half-power-power-weighted-covariance",
method: mapScale === "score"
? "connected-half-maximum-score-weighted-covariance"
: "connected-half-power-power-weighted-covariance",
halfPowerRatio,
thresholdValue: component.thresholdValue,
pixelCount: component.indices.length,
@@ -506,18 +519,28 @@ export function measureBeamSpread(rawMap, scan, peakIndex, halfPowerRatio = 0.5)
throw new RangeError("peakIndex is outside rawMap.");
}
requireRatio("halfPowerRatio", halfPowerRatio, { positive: true });
return measureBeamSpreadFromValues(values, scan, peakIndex, halfPowerRatio);
return measureBeamSpreadFromValues(
values,
scan,
peakIndex,
halfPowerRatio,
rawMapScale(rawMap),
);
}
/**
* Evaluate one linear-power DAS band. Source z coordinates are projected onto
* scan.z, matching the image-plane ground-truth convention.
* Evaluate one nonnegative spatial power or score map. Source z coordinates
* are projected onto scan.z, matching the image-plane ground-truth convention.
*/
export function evaluateDasBand(options) {
export function evaluateLocalizationBand(options) {
if (!options || typeof options !== "object") throw new TypeError("evaluation options are required.");
const { rawMap, scan } = options;
validateScan(scan);
const values = normalizeRawMap(rawMap, scan);
const mapScale = options.mapScale ?? rawMapScale(rawMap);
if (mapScale !== "power" && mapScale !== "score") {
throw new RangeError("mapScale must be power or score.");
}
const sources = normalizeSources(options.sources ?? options.scene?.sources ?? []);
const truths = sources.map((source) => ({
sourceId: source.id,
@@ -558,7 +581,13 @@ export function evaluateDasBand(options) {
const detections = selectDetections(localPeaks, thresholdValue, minimumPeakDistanceM, maximumDetections).map(
(detection) => ({
...detection,
beamSpread: measureBeamSpreadFromValues(values, scan, detection.index, halfPowerRatio),
beamSpread: measureBeamSpreadFromValues(
values,
scan,
detection.index,
halfPowerRatio,
mapScale,
),
}),
);
const { sourceMatches, truePositiveCount } = matchTruths(truths, detections, matchToleranceM);
@@ -604,6 +633,7 @@ export function evaluateDasBand(options) {
mainLobeWidthM: mainLobe?.equivalentDiameterM ?? 0,
},
diagnostics: {
mapScale,
mainLobe,
mainLobeWidthM: mainLobe?.equivalentDiameterM ?? 0,
gridResolutionM: resolution,
@@ -611,6 +641,11 @@ export function evaluateDasBand(options) {
};
}
/** Backwards-compatible name for callers that evaluate conventional DAS maps. */
export function evaluateDasBand(options) {
return evaluateLocalizationBand(options);
}
/** Select only metric names accepted by result.schema.json. */
export function selectResultMetrics(evaluationOrMetrics, metricNames = RESULT_METRIC_KEYS) {
const metrics = evaluationOrMetrics?.metrics ?? evaluationOrMetrics;