Skip to content

About

Measures a fixed camera's azimuth, pitch, roll and lens from the star trails it records at night, with no site visit. Applied to HPWREN's wildfire cameras.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

37 Commits

Folders and files

Repository files navigation

star-calibration

Measure a fixed outdoor camera's azimuth, pitch, roll and lens from the stars it already records at night. No site visit, no surveyed landmark, no calibration target.

From star trails to camera pose: tracks, the celestial pole, the scan about it, the fitted stars

One 90-minute block from HPWREN's Big Black Mountain South camera. 1: moving point sources, linked into tracks. 2: the trails' flow field gives the celestial pole in closed form, with no star named. 3: the pole fixes two angles; a 1-D scan finds the third. 4: stars assigned to whole tracks and refit — 23 stars at a median 0.78 px — with the published pose in orange for reference.

Built for HPWREN's wildfire cameras. HPWREN's camera table gives each camera's site precisely, and its heading and field of view as the direction it watches and the lens it carries: what a network built to show people the landscape needs. Locating smoke by triangulating between cameras asks for more, bearings to a fraction of a degree and a lens model, and this measures both from frames the cameras already record. The same method applies to any fixed camera that sees a patch of night sky.

What it found on HPWREN

From 43 nights between 2026-07-03 and 2026-10-08, on frames from HPWREN's public CDN (hpwren/pose_ledger.json, v0.4.0):

  • 267 solves on 89 cameras, from 316 night blocks tried, at a median residual of 0.75 px and 25 stars per solve.
  • Nights agree to hundredths of a degree. Two nights of the same camera under 30 days apart land on the same pose to 0.013° (median), and 97% of them within 0.05°: the acceptance test that matters more than any star count (cross_night.py).
  • A lens model for every camera. The 90° units are equidistant fisheyes at 0.880–0.891 of the nameplate scale, spanning about ±55°. Big Black Mountain's cameras are a second lens group at 0.772–0.776. Near the frame edge, modelling the lens is worth up to 8° of bearing.
  • Each camera's optical centre, fitted from all its nights at once (intrinsics.py): a median 28 px from the frame's middle on the 86 cameras where it is fitted, and steady from night to night. Centres and solves are iterated together until no centre moves.
  • Dated poses. Cameras get serviced and re-aimed, so a pose is a measurement on a date. The ledger says when one applies to another date, and refuses to bridge an apparent re-aim (ledger.py).

Downstream, in plume-triangulation, these poses are scored in kilometres of wildfire-location error. They are also checked against terrain: the skyline predicted from a DEM through the star-solved pose lands on the image edge to a third of a pixel.

How a solve works

  1. Tracks (tracks.py). Detect point sources in every dark frame and link them frame to frame. On HPWREN this runs twice: once with big blobs allowed, in the top third of the frame, so the brightest stars find a first pose; then with the usual small blobs over all the sky above the camera's skyline, which a terrain model (terrain.py) places through that pose. The second pass's tracks are the ones solved. Keep what persists and moves: hot pixels and lens artefacts stay put, stars drift at the sidereal rate. The monochrome (NIR) units get a noise-scaled detector and a constant-velocity linker. Then clean takes out what isn't one star: it splits tracks the linker handed from one star to another, and drops cloud texture that wanders instead of drifting and the burned-in banner's clock digits.
  2. Pole (pole.py). For a star at direction d, ḋ = ω (p × d), which is linear in the celestial pole p. One least-squares solve over all trails gives the pole in camera coordinates, with no catalog and no search. Its length measures the lens scale.
  3. Scan (solve.py). The pole fixes two angles. Scan the turn about it at 0.1°, scoring how many bright catalog stars land on any track. The best few angles start refinement, plus the angles near where the camera comes out level, since most are mounted close to it. A grid around the published pose runs too, unless the pole search has already solved with 12 or more stars.
  4. Assign and refit. Match catalog stars to whole tracks (Hungarian assignment on median track distance) and refit pose — then pose and lens — with a robust loss, tightening the match radius as it converges. A solve needs ≥ 8 stars under 3 px median, and the lens inside a plausible band. On HPWREN, a fit with 5–7 stars that passes the rest is accepted when another night of the same camera, solved on its own, lands within 0.1° of it.

Star positions carry proper motion, IAU 1976 precession and refraction. Skipping precession alone leaves about 0.3° of azimuth in every 2026 solve (catalog.py documents the chain). Every solve records the sky model it used, and a ledger refuses to mix models.

Use

pip install "star-calibration[opencv] @ git+https://github.com/rharnish/star-calibration"

Leave out [opencv] if your environment already has an OpenCV build.

As a library, with frames from anywhere:

from star_calibration.tracks import clean, collect
from star_calibration.solve import Night, solve_wide

t0 = 1789112704  # the epoch track offsets count from
frames = [...]   # (epoch, epoch - t0, jpeg bytes), in time order
tracks, decoded = collect(frames, lat=33.1, lon=-116.8, max_gap_s=300)
tracks, counts = clean(tracks, banner_px=0)   # rows of burned-in text at the top, if any
cam = {"lat": 33.1, "lon": -116.8, "elev": 1600, "az": 180, "fov": 90}  # published pose
r = solve_wide(Night(camera="my-cam", cam=cam, t0=t0, tracks=tracks))
r["status"], r["pose"]   # 'solved', {'d_az', 'd_pitch', 'd_roll', 'k_ratio', 'k1'}

examples/hpwren_worked_example.ipynb does this on a real camera and night, start to finish, and then checks the pose against a second night and against landmarks in a daytime frame. examples/hpwren_track_cleaning.ipynb is a hard night: most of the linked tracks are lit cloud, and the solve only succeeds once tracks.clean takes them out.

On HPWREN, see src/star_calibration/hpwren/:

python -m star_calibration.hpwren.calibrate fetch 20260911 hp-s-mobo-c vo-n-mobo-c
python -m star_calibration.hpwren.calibrate solve
python -m star_calibration.hpwren.calibrate agree

Layout

module
catalog bright-star catalog (HYG, mag ≤ 4) and apparent alt/az for any epoch and site
fisheye the lens model, pixel ↔ direction, and the shared HPWREN lens
tracks point-source detection, linking, and cleaning
pole the pole from trails, and the pose family it implies
solve Night, solve, solve_wide
cross_night night-to-night agreement
overlay a solve drawn on its own frame: tracks, fitted stars, and the published pose's error
animate a solve played back as video from its trace: pole, coarse search, refinement, verdict
explore the same trace as data for a browser replay, every star already projected
intrinsics each camera's optical centre, fitted jointly from all its nights
terrain a camera's skyline from the Copernicus DEM, and the part of a frame above it
ledger per-camera, per-date poses, and when one applies
sun, moon dark-frame selection; moonlit nights solve as well as dark ones
hpwren/ HPWREN's camera table, CDN nights, a command-line calibrator, the solved ledger

Every record the package and its HPWREN pipeline produce, from the camera table and pose ledger to the cache's tracks, solves and weather, is described field by field, with units and rules, in docs/records.md.

pytest runs in ~15 s with no data. It includes an end-to-end solve of a synthetic night: catalog stars through a known, off-nameplate camera, back to that camera to 0.03°.

History

This began as the camera-calibration stage of plume-triangulation and was split out with its git history. That repository's NOTES.md is the lab notebook for how each piece was built and tested, including what failed; docstrings here cite it by date.

Attribution

  • HPWREN — camera frames and the camera table (hpwren/sites.js, hpwren/cams.json). Use of HPWREN data requires a credit reference to https://www.hpwren.ucsd.edu/.
  • Copernicus DEM GLO-30 — the skyline (terrain.py), read from its public bucket on AWS: © DLR e.V. 2010–2014 and © Airbus Defence and Space GmbH 2014–2018, provided under COPERNICUS by the European Union and ESA.
  • HYG Database — astronexus/HYG-Database, CC BY-SA 4.0. data/bright_stars.json is a filtered derivative (mag ≤ 4) under the same license.
  • Prior work — R. Quimby, Using the Stars for Altitude-Azimuth Calibration of HPWREN Cameras, HPWREN, 20 September 2024.

License

Code is MIT (LICENSE). src/star_calibration/data/bright_stars.json is CC BY-SA 4.0, as derived from HYG.

About

Measures a fixed camera's azimuth, pitch, roll and lens from the star trails it records at night, with no site visit. Applied to HPWREN's wildfire cameras.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages