feat: add comparable localization algorithms
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user