A blind astrometry plate solver written in Rust, inspired by astrometry.net.
Given an astronomical image (or a list of detected source positions), zodiacal determines the sky coordinates — pointing, rotation, and plate scale — using only the pattern of stars in the image. No prior knowledge of the field is required.
Zodiacal implements the geometric hashing approach described in Lang et al. (2010):
- Source extraction — detect bright point sources in the image
- Quad formation — form 4-star asterisms from the brightest sources and compute scale-invariant hash codes
- Code matching — search prebuilt index files for matching asterisms using a kd-tree
- Hypothesis fitting — compute a TAN-WCS (tangent-plane) projection from matched star correspondences
- Bayesian verification — evaluate the alignment against known catalog positions using a log-odds decision framework
The solver uses multi-scale indexes with per-index scale filtering to efficiently search across different angular scales.
Tested on 1,000 simulated fields (9568x6380 px, plate scale 0.1296"/px, FOV ~20'x13'):
| Configuration | Solved | Rate | Median Time |
|---|---|---|---|
| Mag 16 catalog, 4 bands (3x factor) | 962/1000 | 96.2% | 0.62s |
| Mag 16 + verify catalog (82M stars) | 967/1000 | 96.7% | — |
| Mag 19 catalog, 4 bands (3x factor) | 972/1000 | 97.2% | — |
| Mag 19 catalog, 12 bands (sqrt(2) factor) | 985/1000 | 98.5% | 1.07s |
Best configuration solve time distribution (985 solved):
- 48% solved in under 1 second
- 83% solved in under 5 seconds
- 93% solved in under 10 seconds
- 99% solved in under 30 seconds
- 60-second timeout for all cases
The 15 remaining failures are quad-matching timeouts where the solver exhausts the search budget without finding the correct asterism match. Analysis shows these are a mix of sparse fields (few detected sources) and fields where the correct quad is buried among too many candidates.
For comparison, the reference astrometry.net system reports 99.9% on SDSS fields (2048x2048 px, 0.396"/px, wider FOV ~13.5').
Zodiacal requires prebuilt index files (.zdcl) containing star positions and quad hash codes. These are built from a starfield binary catalog.
Star catalogs are built using the gaia_filter tool from the starfield crate, which filters cached Gaia DR3 source files by magnitude:
# Build a magnitude-19 catalog (~565M stars, ~17 GB)
cd /path/to/starfield
cargo build --release --example gaia_filter
./target/release/examples/gaia_filter --magnitude 19.0 --output gaia_mag19.bin --threads 8 allThe all argument processes all cached Gaia source files. Brighter magnitude limits produce smaller, faster-to-load catalogs:
| Magnitude Limit | Stars | File Size |
|---|---|---|
| 16.0 | ~83M | ~2.5 GB |
| 19.0 | ~565M | ~17 GB |
The recommended approach builds a series of narrow-band indexes, each covering a different angular scale range:
zodiacal build-index-series \
-c gaia_mag19.bin \
-o my_index \
--scale-lower 10.0 \
--scale-upper 600.0 \
--max-stars-per-cell 30 \
--passes 16 \
--max-reuse 8 \
--max-depth 8This produces index files my_index_00.zdcl through my_index_11.zdcl, with 12 bands at a sqrt(2) scale factor. Each band covers a narrow range of quad angular sizes (e.g., 80-113", 113-160"), keeping the code trees small to reduce false-positive matches.
Key parameters:
--scale-lower/--scale-upper: angular scale range in arcseconds--scale-factor: ratio between adjacent bands (default sqrt(2) ~1.414; use 3.0 for fewer, wider bands)--max-stars-per-cell: brightest stars per HEALPix cell for uniformization (default 30)--max-depth: maximum HEALPix depth (default 8 = 786,432 cells; limits memory usage)--passes: number of quad-building passes per cell--max-reuse: maximum times a star can be reused across quads
For quick testing or narrow-scale applications:
zodiacal build-index \
-c gaia_mag16.bin \
-o test_index.zdcl \
--scale-lower 30.0 \
--scale-upper 600.0zodiacal solve image.png \
-i my_index_00.zdcl \
-i my_index_01.zdcl \
-i my_index_02.zdcl \
--timeout 60Process a directory of images or JSON source files:
zodiacal batch-solve images/ \
-i my_index_00.zdcl -i my_index_01.zdcl -i my_index_02.zdcl \
--pattern "*.json" \
--timeout 60 \
--failures-dir /tmp/failuresThe --failures-dir option writes diagnostic JSON for each unsolved case, useful for analyzing failure modes.
The solver accepts JSON source lists directly (skips extraction):
zodiacal solve sources.json \
-i my_index_00.zdcl -i my_index_01.zdclExtract sources from an image and write them as JSON for later solving:
zodiacal extract image.png -o sources.jsonZodiacal uses a JSON format for exchanging detected source lists between tools. The extract command produces this format, and the solver can consume it.
| Field | Type | Required | Description |
|---|---|---|---|
image_width |
number | yes | Image width in pixels |
image_height |
number | yes | Image height in pixels |
ra_deg |
number | no | Known RA of field center (degrees) |
dec_deg |
number | no | Known Dec of field center (degrees) |
plate_scale_arcsec |
number | no | Known plate scale (arcsec/pixel) |
sources |
array | yes | Detected sources |
sources[].x |
number | yes | Source x position (pixels) |
sources[].y |
number | yes | Source y position (pixels) |
sources[].flux |
number | yes | Source brightness (ADU) |
{
"image_width": 9568,
"image_height": 6380,
"ra_deg": 265.47,
"dec_deg": 44.31,
"plate_scale_arcsec": 0.1296,
"sources": [
{ "x": 4821.3, "y": 3190.7, "flux": 54210.0 },
{ "x": 1023.5, "y": 892.1, "flux": 38450.0 }
]
}The ra_deg, dec_deg, and plate_scale_arcsec fields are optional hints. When present they can speed up solving by constraining the search space. When absent they are omitted from the JSON entirely.
The solver pipeline:
Image → Source Extraction → Quad Formation → Code Matching → WCS Fitting → Verification
↑
Index Files (.zdcl)
↑
Star Catalog → HEALPix Uniformization → Quad Building
Key design decisions:
- HEALPix uniformization: stars are distributed across sky cells to ensure uniform index coverage regardless of stellar density
- Multi-scale indexes: narrow angular-scale bands keep kd-trees small, reducing false-positive matches
- Dual-parity matching: field codes are tried in both orientations (original + x/y swapped) to handle image flips
- Incremental quad loop: quads are generated in two phases (astrometry.net style) to avoid redundancy
- Per-index scale filtering: indexes whose band can't overlap with the field's backbone angular scale are skipped entirely
- starfield — star catalogs, coordinate systems, star finding
- ndarray — N-dimensional arrays
- clap — CLI argument parsing
- image — image loading
- rayon — parallelism
- serde / serde_json — serialization
See LICENSE for details.