|
|
|
@@ -1,6 +1,12 @@
|
|
|
|
|
import { getFrameCount, binsForBand } from "/packages/acoustics-core/stft.js";
|
|
|
|
|
|
|
|
|
|
const RESULT_METRIC_KEYS = ["peakToSidelobeDb", "energyInsideTruthRatio", "precision", "recall"];
|
|
|
|
|
const RESULT_METRIC_KEYS = [
|
|
|
|
|
"peakToSidelobeDb",
|
|
|
|
|
"energyInsideTruthRatio",
|
|
|
|
|
"precision",
|
|
|
|
|
"recall",
|
|
|
|
|
"mainLobeWidthM",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function sortForCanonicalJson(value) {
|
|
|
|
|
if (Array.isArray(value)) return value.map(sortForCanonicalJson);
|
|
|
|
@@ -60,6 +66,53 @@ export function shiftSceneDistance(scene, experiment, nextDistance) {
|
|
|
|
|
if (scene.image?.plane) scene.image.plane.z += delta;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function alignExperimentToScene(experiment, scene) {
|
|
|
|
|
const plane = scene.image?.plane;
|
|
|
|
|
if (plane) {
|
|
|
|
|
Object.assign(experiment.scan, {
|
|
|
|
|
z: plane.z,
|
|
|
|
|
xMin: plane.xMin,
|
|
|
|
|
xMax: plane.xMax,
|
|
|
|
|
yMin: plane.yMin,
|
|
|
|
|
yMax: plane.yMax,
|
|
|
|
|
});
|
|
|
|
|
return "image-plane";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const positions = (scene.sources ?? [])
|
|
|
|
|
.map((source) => source.position)
|
|
|
|
|
.filter((position) =>
|
|
|
|
|
Array.isArray(position) &&
|
|
|
|
|
position.length === 3 &&
|
|
|
|
|
position.every(Number.isFinite),
|
|
|
|
|
);
|
|
|
|
|
if (positions.length === 0) return "unchanged";
|
|
|
|
|
|
|
|
|
|
const paddedBounds = (values) => {
|
|
|
|
|
const minimum = Math.min(...values);
|
|
|
|
|
const maximum = Math.max(...values);
|
|
|
|
|
const padding = Math.max(0.25, (maximum - minimum) * 0.15);
|
|
|
|
|
return [minimum - padding, maximum + padding];
|
|
|
|
|
};
|
|
|
|
|
const median = (values) => {
|
|
|
|
|
const sorted = [...values].sort((left, right) => left - right);
|
|
|
|
|
const middle = Math.floor(sorted.length / 2);
|
|
|
|
|
return sorted.length % 2 === 0
|
|
|
|
|
? (sorted[middle - 1] + sorted[middle]) / 2
|
|
|
|
|
: sorted[middle];
|
|
|
|
|
};
|
|
|
|
|
const [xMin, xMax] = paddedBounds(positions.map((position) => position[0]));
|
|
|
|
|
const [yMin, yMax] = paddedBounds(positions.map((position) => position[1]));
|
|
|
|
|
Object.assign(experiment.scan, {
|
|
|
|
|
z: median(positions.map((position) => position[2])),
|
|
|
|
|
xMin,
|
|
|
|
|
xMax,
|
|
|
|
|
yMin,
|
|
|
|
|
yMax,
|
|
|
|
|
});
|
|
|
|
|
return "source-bounds";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function finiteMetricSubset(metrics = {}) {
|
|
|
|
|
return Object.fromEntries(
|
|
|
|
|
RESULT_METRIC_KEYS
|
|
|
|
@@ -75,6 +128,9 @@ function peakForResult(peak) {
|
|
|
|
|
};
|
|
|
|
|
if (peak.sourceId) result.sourceId = peak.sourceId;
|
|
|
|
|
if (Number.isFinite(peak.localizationErrorM)) result.localizationErrorM = peak.localizationErrorM;
|
|
|
|
|
if (Number.isFinite(peak.localizationErrorPixels)) {
|
|
|
|
|
result.localizationErrorPixels = peak.localizationErrorPixels;
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -85,6 +141,17 @@ function environmentForResult({ arrayDefinition, experiment, run }) {
|
|
|
|
|
microphoneCount: arrayDefinition.microphones.length,
|
|
|
|
|
gridPoints: experiment.scan.rows * experiment.scan.columns,
|
|
|
|
|
};
|
|
|
|
|
for (const [key, value] of Object.entries({
|
|
|
|
|
gridRows: experiment.scan.rows,
|
|
|
|
|
gridColumns: experiment.scan.columns,
|
|
|
|
|
scanXMin: experiment.scan.xMin,
|
|
|
|
|
scanXMax: experiment.scan.xMax,
|
|
|
|
|
scanYMin: experiment.scan.yMin,
|
|
|
|
|
scanYMax: experiment.scan.yMax,
|
|
|
|
|
scanZ: experiment.scan.z,
|
|
|
|
|
})) {
|
|
|
|
|
if (Number.isFinite(value)) environment[key] = value;
|
|
|
|
|
}
|
|
|
|
|
if (run.fallback?.code) environment.fallbackCode = run.fallback.code;
|
|
|
|
|
if (run.fallback?.message) environment.fallbackMessage = run.fallback.message;
|
|
|
|
|
const adapter = run.gpu?.adapterInfo;
|
|
|
|
@@ -127,7 +194,7 @@ export function createResultDocument(options) {
|
|
|
|
|
environment: environmentForResult({ arrayDefinition, experiment, run }),
|
|
|
|
|
bands: run.bands.map((band) => {
|
|
|
|
|
const evaluation = evaluations.get(band.id) ?? { peaks: [], metrics: {} };
|
|
|
|
|
return {
|
|
|
|
|
const bandResult = {
|
|
|
|
|
id: band.id,
|
|
|
|
|
color: band.color,
|
|
|
|
|
rawMap: {
|
|
|
|
@@ -145,6 +212,8 @@ export function createResultDocument(options) {
|
|
|
|
|
peaks: evaluation.peaks.map(peakForResult),
|
|
|
|
|
metrics: finiteMetricSubset(evaluation.metrics),
|
|
|
|
|
};
|
|
|
|
|
if (evaluation.evaluation) bandResult.evaluation = { ...evaluation.evaluation };
|
|
|
|
|
return bandResult;
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|