Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sar-focus

Synthetic aperture radar image formation from raw phase history: LFM pulse compression, time-domain backprojection, phase gradient autofocus, and impulse response analysis checked against the analytic resolution and sidelobe figures.

A SAR image is a coherent sum over an aperture the platform flew rather than one the antenna has. Everything interesting follows from that: range resolution comes from bandwidth and has nothing to do with pulse length, cross-range resolution comes from how much the viewing angle changed and has nothing to do with the antenna, and both are wrong the moment you don't know where the antenna was to a few millimetres.

pip install -r requirements-dev.txt
pytest                              # 27 tests
python bench/benchmark.py           # every table below
python bench/benchmark.py --quick   # a faster sanity check

The claim this repo makes

Not "it produces plausible images" — plausible is easy and unfalsifiable. Every number below is checked against a value that was known before the code existed:

quantity analytic value
range resolution (−3 dB IRW) 0.886 · c / 2B
azimuth resolution (−3 dB IRW) 0.886 · λR / 2L
unweighted peak sidelobe ratio −13.26 dB
Hamming PSLR / broadening ≈ −42.8 dB / ≈ 1.47x

The 0.886 matters. c/2B is the Rayleigh figure; the measured −3 dB width of an unweighted sinc is 11% narrower. Checking against the wrong one turns an 11% error into an apparent success, which is a mistake I made before the tests caught it.

Resolution against theory

Sweeping bandwidth and aperture independently. Range should track 1/B and be indifferent to aperture; azimuth should track 1/L and be indifferent to bandwidth. That double dissociation is the real test — a single matching number could be a coincidence.

B MHz aperture m range IRW theory err azim IRW theory err
100 100 1.6609 1.6601 +0.05% 0.6631 0.6663 −0.47%
100 200 1.6620 1.6601 +0.11% 0.3315 0.3326 −0.33%
200 200 0.8302 0.8301 +0.02% 0.3315 0.3326 −0.33%
400 200 0.4149 0.4150 −0.02% 0.3315 0.3326 −0.34%
200 400 0.8299 0.8301 −0.02% 0.1658 0.1661 −0.22%

Everything lands inside 0.5%, and the two axes move only with the parameter they should. Doubling bandwidth halves the range cell with azimuth unchanged; doubling the aperture halves the azimuth cell with range unchanged.

Sidelobes, and a bug they caught

Backprojection samples the compressed history at arbitrary non-integer delays, so it has to interpolate. Linear interpolation seemed fine — until the sidelobes came out better than physically possible:

range oversampling azim PSLR range PSLR azim IRW err
1 (linear interp on raw samples) −15.55 −16.23 +2.29%
2 −13.62 −14.26 −0.03%
4 −13.31 −13.44 −0.29%
8 −13.27 −13.31 −0.33%
16 −13.26 −13.27 −0.34%
theory −13.26 −13.26

An unweighted aperture cannot do better than −13.26 dB. Measuring −16.23 dB meant something was filtering the data, and that something was the interpolator: a triangular interpolation kernel is a sinc² taper in the frequency domain, so linear interpolation was applying an amplitude weighting by accident — broadening the main lobe ~2% while suppressing the sidelobes.

Band-limited oversampling before backprojection removes it, and the sidelobes rise to meet theory from below. A result that looks too good is a bug report; this one would have been invisible without a number to check against.

Weighting

The classic trade, measured on both sides at once:

window range IRW m broadening PSLR dB ISLR dB
none 0.8302 1.000x −13.31 −10.72
Hann 1.3493 1.626x −31.55 −33.01
Hamming 1.2208 1.471x −42.74 −40.31
Taylor (n̄=4, −35 dB) 1.1092 1.336x −35.07 −30.99

Hamming's −42.74 dB against a textbook −42.8 dB, at 1.471x broadening against a textbook ~1.47x. Taylor does what it is designed to do: hit a specified sidelobe level (−35 dB requested, −35.07 measured) for the least broadening that buys.

Autofocus

Phase gradient autofocus estimates the aperture phase error from the data. It works because a bright scatterer is a known signal — its response should be a clean impulse, so any phase structure on it is error. Doing that on the brightest scatterer in every range bin and averaging is what makes it robust.

phase error IRW before IRW after vs limit recovery entropy before after
none 0.3312 0.3312 −0.4% 1.00x 7.826 7.826
quadratic 2 rad 0.3687 0.3331 +0.2% 1.11x 8.388 7.871
quadratic 4 rad 1.1587 0.3317 −0.3% 3.49x 8.931 8.184
quadratic 8 rad 0.8627 0.3360 +1.0% 2.57x 9.440 7.956
quadratic 12 rad 0.9376 0.3581 +7.7% 2.62x 9.739 8.615
random 1.5 rad rms 0.3847 0.3713 +11.7% 1.04x 9.875 9.582
random 3.0 rad rms 0.4663 0.4082 +22.8% 1.14x 10.096 9.867
random 5.0 rad rms 0.3096 0.3461 +4.1% 0.89x 10.103 9.917

On low-order defocus it recovers the diffraction limit almost exactly — a 3.5x blur removed to within 0.3% of the theoretical width.

Where it loses

Two honest failures in that table.

High-order random error barely improves. Recovery is 1.04x–1.14x, and at 5 rad rms the IRW worsens. PGA estimates a phase gradient and integrates it; that is well conditioned for a smooth low-order error and poorly conditioned for one with structure across the aperture. Entropy still falls in every row, so it is finding something — just not enough, and not the right thing.

The IRW metric itself stops being meaningful there. Look at the "before" column: 12 rad quadratic reads 0.9376 but 4 rad reads 1.1587, and random 5 rad reads 0.3096, which is below the diffraction limit. That is not a sharper image. Once a response breaks into multiple lobes, the width measured at the brightest pixel describes whichever fragment happened to win, not the target. Entropy is monotone across the whole sweep and IRW is not, which is the reason both are reported.

A bug worth keeping

PGA run for a fixed iteration count degrades an image that is already focused. On clean data, six iterations raised entropy 7.826 → 8.55 and drove the apparent IRW below the diffraction limit. With few bright scatterers most range bins hold sidelobe ridges rather than targets, and the estimator faithfully fits those.

Two fixes, both in sar/autofocus.py: rank range bins by peak-to-mean and use only the strongest, and return the best-cost iterate rather than the last one. The none row above is the regression test — PGA on focused data is now exactly a no-op.

The sign convention was also wrong initially, and wrong in the worst way: the correction is exp(+jφ), and with exp(−jφ) every iteration made the image measurably worse while the code looked entirely reasonable. Image entropy moving the wrong way is what caught it.

Cost

Backprojection is O(pulses × pixels) and makes no apology for it:

image pixels pulses seconds Mpix-pulse/s
81×81 6,561 600 0.40 9.9
161×161 25,921 600 1.01 15.4
321×321 103,041 600 3.30 18.8
641×641 410,881 600 18.94 13.0

Linear in the product, as promised. This is the algorithm's whole trade: it assumes nothing about the geometry — not a straight track, not a flat scene, not separable range migration — and pays for that generality per pixel. The frequency-domain methods buy back orders of magnitude by assuming exactly the things backprojection refuses to.

Layout

file what
sar/waveform.py LFM chirp, matched filter, windows
sar/geometry.py Platform track, range histories, resolution formulas
sar/simulate.py Exact phase-history generation, phase-error models
sar/focus.py Backprojection, band-limited range oversampling
sar/autofocus.py PGA: centre, window, LUMV estimator, integrate
sar/metrics.py IRW / PSLR / ISLR, entropy, contrast

What this doesn't do

  • The data is simulated. This is the real limitation. The simulator evaluates exact two-way delays with no approximations, so range migration appears because the geometry produces it — but image formation is still being checked against physics I also wrote. It pins down resolution, sidelobes and autofocus behaviour, which have analytic answers. It says nothing about calibration, real motion measurement error, antenna patterns, or clutter statistics. Running this against Sandia or Capella collects is the obvious next step and is not done.
  • No frequency-domain image formation. Range-Doppler and ω-k are what you actually field; backprojection is the reference they get checked against. Only the reference is here.
  • Stripmap, broadside, straight and level. No spotlight, no squint, no motion compensation for a track that wanders.
  • Point scatterers only. No distributed targets, no speckle, so nothing here exercises the statistics that dominate a real scene.
  • No polarimetry, no interferometry.

MIT licensed.

About

SAR image formation from raw phase history: pulse compression, backprojection, phase gradient autofocus — validated against analytic resolution and sidelobe theory to 0.5%

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages