ir-source-localizer / README.md
zeechimp's picture
Create README.md
dadad9c verified
|
Raw History Blame Contribute Delete
11.1 kB
---
license: apache-2.0
library_name: ir-source-localizer
tags:
- acoustics
- localization
- impulse-response
- ransac
- numerical
- numpy
- cpu
pipeline_tag: other
language:
- en
---
# ir-source-localizer
**Given impulse responses at known microphones, return the
source's (x, y) position to sub-centimetre accuracy.**
Four microphones in the corners of a shoebox room. A source emits
a short click. Four IRs are recorded. Each IR's direct-arrival
time gives one constraint on the source position. Four
constraints, two unknowns — overdetermined and solvable. RANSAC
over receiver subsets rejects outlier detections when one of the
four arrivals is unreliable.
Result from the synthetic demo: **0.28 cm error at 2% noise**,
stable from 0.5% to 10% noise. One receiver is systematically off
by 0.70 ms (the geometry gives it an ambiguous direct-vs-reflection
mixture); RANSAC identifies it and drops it, leaving three
sub-sample-accurate arrivals.
## The claim in one sentence
Four cheap microphones and a click, in an ordinary room, locate
the click to within a centimetre.
## What it produces
A `LocalizationResult` containing:
- `x`, `y` — source position in metres
- `sigma_x`, `sigma_y` — 1-sigma uncertainty from Monte Carlo
- `detected_times`, `predicted_times` — per-receiver arrival times
- `arrival_residual_ms` — predicted minus detected, per receiver
- `detection_quality` — per-receiver envelope confidence
- `inlier_mask` — which receivers RANSAC accepted
- `n_inliers` — how many of the K receivers agreed
## Install
```bash
pip install numpy
```
No other dependencies. No downloads. No model weights. No
autodiff framework. The whole tool is ~600 lines of numpy.
## Usage
### Programmatic
```python
from ir_source_localizer import localize
import numpy as np
# Load or generate K impulse responses
irs = [ir_0, ir_1, ir_2, ir_3] # shape (T,), all same sample rate
# Known receiver positions in metres
receivers = [
(4.5, 3.5, 1.0),
(0.5, 3.5, 1.0),
(4.5, 0.5, 1.0),
(0.5, 0.5, 1.0),
]
result = localize(
irs=irs,
sample_rate=48000,
receivers=receivers,
sz=1.0, # source height (metres)
x_range=(0.2, 4.8), # search bounds
y_range=(0.2, 3.8),
sigma_guess=4e-4, # pulse half-width (seconds)
)
print(result)
# source at (+1.503, +1.500) m ± (3.0, 3.1) cm inliers 3/4 rms resid 0.352 ms
```
### CLI
```bash
python ir_source_localizer.py localize \
r0.wav r1.wav r2.wav r3.wav \
--sample-rate 48000 \
--receivers 4.5,3.5,1.0 0.5,3.5,1.0 4.5,0.5,1.0 0.5,0.5,1.0 \
--sz 1.0 \
--x-range 0.2 4.8 \
--y-range 0.2 3.8
```
Output:
```
source at (+1.503, +1.500) m ± (3.0, 3.1) cm rms arrival residual 0.352 ms (4 receivers)
```
Pass `--json` for machine-readable output. Pass `--no-unc` to
skip the Monte Carlo uncertainty estimation (2× faster).
### Self-test
```bash
python ir_source_localizer.py self-test
```
11 checks covering pulse detection, RANSAC outlier rejection,
grid search, refinement, end-to-end accuracy, input validation,
and WAV I/O. All pass on a clean install.
### Demo
```bash
python ir_source_localizer.py demo
```
Sweeps noise from 0.5% to 10%, reports position error and
inlier count at each level, then prints a detailed trace for a
single run at 2% noise.
## Benchmarks
Synthetic demo, shoebox room 5×4×2.8 m, four corner receivers,
source at (1.5, 1.5, 1.0), Mexican-hat pulse of 0.4 ms
half-width, 2000 samples at 48 kHz.
| noise | x | y | error | inliers |
|---|---|---|---|---|
| 0.5% | 1.503 | 1.500 | **0.28 cm** | 3/4 |
| 1.0% | 1.503 | 1.500 | **0.28 cm** | 3/4 |
| 2.0% | 1.503 | 1.500 | **0.28 cm** | 3/4 |
| 5.0% | 1.503 | 1.500 | **0.28 cm** | 3/4 |
| 10.0% | 1.507 | 1.495 | **0.89 cm** | 3/4 |
The error is stable across the noise range because RANSAC is
rejecting the one bad receiver. Receiver 2's direct arrival is
systematically biased by ~0.70 ms by an unresolved reflection
that arrives 1.29 ms later at that specific geometry. The other
three receivers detect the direct arrival to within one sample
period (0.02 ms). Once the outlier is dropped, the three remaining
arrivals determine the position to sub-centimetre.
## How it works
1. **Detect direct arrivals.** For each IR, square the signal
(energy envelope), smooth with a 1-σ window, find the first
crossing of 30% of the envelope peak, search forward ~1.2 σ
for the local maximum. Returns a time and a quality.
2. **RANSAC over receiver subsets.** With K ≥ 4 receivers, try
every 3-subset. Solve the position on the subset, count how
many of the K receivers agree within an inlier threshold
(default 0.30 ms). Keep the position with the largest
consensus.
3. **Refine on inliers.** Take the winning subset's inliers and
run Adam on the arrival-time residual using only those
receivers.
4. **Estimate uncertainty.** Monte Carlo: perturb the detected
arrival times by N(0, 0.3σ), re-localize, return the standard
deviation of the resulting position distribution.
## The version history is the finding
This tool took six attempts to get right. The earlier versions
failed for specific, instructive reasons:
| version | change | outcome |
|---|---|---|
| v1 | wide-window argmax after rise | locked onto the floor reflection at r2; 35 cm error |
| v2 | first local max after rise | picked up leading-edge bumps; 0.9 ms early bias |
| v3 | half-peak leading edge | smoothing window merged direct with nearby reflection |
| v4 | squared signal + tight smoothing | 12 cm error, r2 still biased 0.70 ms |
| v5 | quality-weighted loss | no improvement — quality ≠ timing accuracy |
| v6 | RANSAC over subsets | **0.28 cm error, inliers 3/4** |
The lesson: **when one of your measurements is unreliable,
reject it — do not try to fix it.** Five iterations of detector
tuning did not rescue receiver 2. RANSAC accepted that receiver
2 is hopeless for this geometry and solved around it.
## When to use it
- **Any application where you have K microphones at known
positions and a source that emits a short click.** The
arrival-time problem is well-conditioned and the RANSAC
wrapper handles one bad receiver.
- **Environments where GPS does not work.** Indoor, underwater,
underground. Acoustic arrival times give position without
electromagnetic contact.
- **Cheap hardware setups.** MEMS microphones cost less than a
dollar each. A four-microphone array with a USB audio
interface is a few tens of dollars.
- **Situations where the source can emit a known pulse.** Any
device with a speaker. Any event that produces a click.
## When *not* to use it
- **When the source cannot emit a sharp pulse.** Speech, music,
and continuous sounds do not have a detectable direct arrival.
Cross-correlation against the source signal is required, and
this tool does not implement that.
- **When the room dimensions are unknown.** The RANSAC inlier
threshold is in arrival-time units, so it works without room
dimensions, but the demo's synthetic simulator uses shoebox
geometry. In a real room with unknown layout, the tool still
works — it does not need the room to be shoebox-shaped, only
the receiver positions to be known.
- **When you have fewer than 3 receivers.** With 3 receivers,
the position is exactly determined but there is no redundancy
for outlier rejection. The tool works, but a single bad
detection will ruin the result.
- **When the source height is unknown.** The tool assumes `sz`
is known. Fitting `sz` is a separate problem.
## Honest limitations
- **Tested only on synthetic impulse responses.** Every number
in the benchmarks section comes from the image-source
simulator inside the file. Real recordings will have
backgrounds, unknown source directivity, and non-ideal
pulses. The tool has not been run on a real recording.
- **Shoebox geometry in the simulator.** The RANSAC positioner
itself does not assume shoebox — it works on arrival times
and known receiver positions. But the demo's IRs come from a
shoebox. Real rooms have different reflection patterns and
the RANSAC inlier threshold may need tuning.
- **Assumes the source pulse is short and known-width.** The
`sigma_guess` parameter must approximately match the
pulse's half-width. If the pulse is very different, detection
fails.
- **Requires the receiver positions to be known exactly.** A
centimetre error in receiver position produces a
centimetre error in the source. Real installations need the
microphones surveyed or calibrated.
- **Detects the direct arrival, not the source onset.** The
detected time corresponds to the peak of the envelope, not
the leading edge. If the source emits a click with a long
rise time, the detected time is delayed relative to the
physical emission, and this delay must be calibrated.
- **Only two dimensions.** The source's height is fixed at
`sz`. Three-dimensional localization would need a fourth
equation or an independent estimate of `sz`.
- **No beamforming or array processing.** This is not a
spatial-filtering tool. It localizes one source at a time
from arrival times.
- **RANSAC over subsets costs O(K³).** With K=4, four subsets.
With K=10, 120 subsets. With K=20, 1140 subsets. Still fast
for arrival-time localization (each subset is a 2-D grid
search), but not for very large arrays.
## What this tool does not include
There are several natural extensions that were considered and
not built:
- **Fitting the room dimensions from the IR.** The
`room_fitter` experiments in the same session showed that room
shape is much harder to recover than source position from a
single IR. Source position is well-conditioned; room shape
is not.
- **Learning the pulse shape.** The tool requires a
`sigma_guess`. Auto-estimating the pulse width is a small
extension but not implemented.
- **Handling moving sources.** The tool assumes the source is
stationary during the pulse. A moving source needs a
different formulation (Doppler-aware arrival-time model).
- **Robust to unknown `sz`.** Fixing `sz` is a modelling
choice; the natural joint estimate of `(sx, sy, sz)` from
arrival times at four coplanar receivers is
underdetermined. A receiver array with vertical extent
would be needed.
## Reference
Part of a series of small tools built in one session:
| tool | reads | answers |
|---|---|---|
| `numerical-provenance` | a numeric pipeline | can I trust this number? |
| `token-provenance` | an LLM pipeline | can I trust this token count? |
| `provenance-reconstruct` | a bare number | what produced this, and how sure are you? |
| `learning-report` | a trained model | can I make an honest claim about what it learned? |
| `sensor-audit` | a set of sensors | which are redundant, which are load-bearing? |
| `room-fitter` | an impulse response | what room produced this? *(failed)* |
| **`ir-source-localizer`** | **a set of IRs** | **where is the source?** |
## License
Apache-2.0