Skip to content

v0.3.0: track cleaning, pole-first search, per-camera optical centre - #1

Merged
rharnish merged 8 commits into
mainfrom
track-filters
Oct 1, 2026
Merged

rharnish merged 8 commits into
mainfrom
track-filters

Conversation

@rharnish

@rharnish rharnish commented Oct 1, 2026

Copy link
Copy Markdown
Owner

v0.3.0: track cleaning, pole-first search, per-camera optical centre

This release changes the solver in four ways. More blocks solve, the fits are tighter, and two nights of the same camera now land on the same pose to within 0.05°. The ledger gains the optical centre (cx, cy). That changes a stable record, so plume-triangulation has to adopt this release in a coordinated change.

What changes

Track cleaning (tracks.clean, 7ac45b8). The nearest-neighbour linker lets a track wander:

  • A track left open is inherited by the next star to cross its pixels.
  • A track hops between close stars, as in the Pleiades.
  • Cloud texture links into squiggles.

Each linked track is now split into the stars it contains, by fitting one smooth cubic path per star (RANSAC, 3 px). Squiggles, the camera's burned-in banner text and repeated tracks are dropped. The tracks pickle keeps both the linked and the cleaned tracks, each with the settings that made it. The linker's gap limit for colour cameras is 300 s.

The fold-back fix (7ac45b8). With k1 < 0 the fisheye radius kθ(1 + k1θ²) peaks near 118° off-axis and then shrinks. Stars behind the camera were therefore projected back into the frame, and the wide search scored up to 4–5 of these phantom stars per pose. project_fisheye now returns NaN past min(100°, the peak).

Pole-first search (7fbb62c). solve_wide starts from the celestial pole implied by the trails, which fixes two of the three pose angles, and scans the third. It adds two starts where the camera would be level. The grid around the published pose runs only when the pole search hasn't already solved with 12 or more stars. bi-s 2026-09-20 solves with 14 stars, about 5× faster.

Per-camera optical centre (6bbe217). The lens's optical axis sits a median 32 px from the frame's middle (up to 75 px, on bm-e). Solved as if centred, the pose tilted to make up for it. intrinsics.py fits one centre per camera from all its solved nights together, and records a new segment when the centre jumps. The result ships as hpwren/intrinsics.json, and every solve and ledger entry records the centre it used. calibrate intrinsics rebuilds the file.

Results

Every cached CDN block was re-solved at a10c451 (the version bump). All numbers below come from those solves.

v0.2.1 (baseline) v0.3.0
CDN blocks solved, of the baseline's 185 130 135 (plus 2 new nights of one camera)
median residual 1.26 px 0.78 px
median stars per solve 25 28
night-to-night pose disagreement, worst pair < 30 days apart 0.79° 0.05°
  • Poses move. They shift a median 0.76° and at most 3.1° (bm-e), in step with each camera's centre offset.
  • Part of the tighter agreement comes by construction. A camera's nights now share one fitted centre.

The shipped ledger (649fdba)

hpwren/pose_ledger.json has been regenerated with calibrate ledger. It now holds 137 solves on 76 cameras, over 18 nights from 2026-07-14 to 2026-09-27. It keeps every one of the 106 camera-nights of the v0.2.1 ledger and adds 31. Each entry's solver is 0.3.0+ga10c45148b. The READMEs and docs/records.md quote the new numbers.

Interface change: plume-triangulation

  • The ledger's new cx/cy fields are documented in docs/records.md. An entry with no centre is treated as centred: ledger.lookup reads a missing value as 0.
  • Plume's geom.py and corrected_cam must apply the centre. Otherwise v0.3.0 poses will be about as far off as the centre offset.
  • Adopting this release moves plume's results, so plume needs a coordinated re-run:
    • the FIgLib solves, republished with stars.publish
    • its ledger
    • its README numbers
    • a new results tag

Testing

  • pytest: 96 tests pass at every commit.
  • New tests cover track cleaning (test_tracks.py), the centre fit (test_intrinsics.py), the fold, solves through a known centre on a synthetic night, and pixel_to_cam with a centre.
  • Re-solving the full cache at a10c451 reproduced the development solves block for block: the same statuses, star counts and poses.

Frames and camera metadata: HPWREN, https://www.hpwren.ucsd.edu/.

🤖 Generated with Claude Code

rharnish and others added 8 commits September 28, 2026 10:43
… them

When a camera sends nothing, the CDN serves a 320x240 "No Images!" card under each
frame's name. nights.frame_files splits a block's frames into real ones and these
placeholders (read from the JPEG header only), and read_frames and frame_at skip the
placeholders. A block that is mostly placeholders fails with reason "no images: ...", and
its overlay is one of the placeholder cards. The gallery shows them as a fourth outcome,
noimg.
…er version

solve and solve_wide take an optional `trace` list and append one dict per step to it:
the attempt, the pole fit and its kept samples, the coarse scan, the starts, every rung of
every start's refinement, and the verdict. Nothing reads it back, so results don't change.

- animate.py and `calibrate animate [--kept] [--full] [--mark A-B,C]`: the trace played
  back as an H.264 video (through ffmpeg; OpenCV's mp4v without it).
- explore.py and `calibrate explore`: the trace as data, every star position projected in
  Python by the solver's model, for hpwren/explore.html, a replay page to scrub, zoom and
  inspect. It masks stars too far off the boresight, where the lens polynomial folds back
  and would put stars behind the camera in the frame.
- The gallery links a block's video and replay when they exist. overlay.draw(full=True)
  draws the whole frame.
- Results and ledger entries name the solver that made them (`solver`: the version, and
  the commit when run from a checkout, `.dirty` when it differed).
- docs/records.md describes every record the package ships or writes, and
  tests/test_records.py fails on a field it doesn't mention.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o the frame

Tracks (tracks.py, hpwren/calibrate.py):
- tracks.clean splits tracks the nearest-neighbour linker handed from one star to another
  (tracks.split: RANSAC a cubic path per star, cutting at gaps over 10 min), drops squiggly
  tracks (cloud texture, haze) unless one smooth part holds half their points, drops
  near-stationary tracks in HPWREN's clock banner, and drops duplicates.
- load_tracks closes a colour camera's track after 300 s unseen (it had no limit within a
  block), and the pickle records the link and clean recipes it was made with, so a changed
  recipe rebuilds it.

Fold-back (fisheye.py): project_fisheye returns NaN past 100 deg off the boresight or the
lens polynomial's fold, whichever comes first, so a star behind the camera no longer lands
in the frame. explore.py drops its own mask; the solver, overlay and video handle NaN.

On the cache's CDN blocks: 130 -> 133 solved (smer-tcs8 09-11 at d_az +39.5, bm-e 09-19,
rdd-e 09-20, wlfd-s 09-20 gained; bi-s 09-20 lost to the coarse scan), poses of blocks
solved both times moved 0.011 deg median, and the five solved blocks whose pole fit sat at
|p| 0.47-0.59 (banner tracks) are at 0.99-1.01. docs/records.md has the measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e grid only when needed

- Level-prior starts (solve._level_starts): of the pole's one-angle family of poses, the
  right one is almost always near where pitch and roll are both small. Near each of the two
  such places the best-scoring angle within 8 deg becomes an extra start.
- solve_wide runs the pole attempts first and the published-pose grid only if they haven't
  solved with 12+ stars (grid="fallback", sure_stars=12; grid="always" is the old order).
  The grid's old job, rescuing blocks with a low |p|, went with the banner tracks.

Over 180 CDN blocks: bi-s 09-20 solves (the coarse scan had no start for it), no pose moves,
and solving is about five times faster. On a thin solve the grid still runs, since it can
end in a tighter fit of the same stars (mp-n 09-11: 0.90 px against the pole's 1.32).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e through it

The solver assumed each lens's optical centre was the middle of the frame. On HPWREN's units
it sits a median 32 px off, stable night to night.

- A camera record may carry cx, cy (pixels right and down from the middle); fisheye,
  pole.pixel_to_cam/estimate/lens_scale and solve use them. Solve results record the centre
  they assumed, the overlay draws a result through its own centre, and ledger entries carry
  cx, cy (an interface change: a caller turning pixels into bearings with a ledger pose
  must apply its centre; entries without one mean (0, 0)).
- intrinsics.py fits one centre and lens per camera jointly across its solved nights, with a
  pose per night, as a series that starts a new segment where the centre jumps. A segment
  with fewer than 2 nights and 20 stars keeps the middle.
- `calibrate intrinsics` writes hpwren/intrinsics.json; hpwren.camera() gives the solver a
  camera's record with the centre for its frame size and date.

intrinsics.json is fitted from the cache's 137 solved CDN blocks: 47 of 76 cameras get a
centre, and a camera's single-night centres sit a median 2.5 px from its joint centre.
Re-solved through it: +bm-s 09-11 and +bi-s 09-20 over the track cleaning alone, median
residual 1.23 -> 0.78 px, median stars 25 -> 28, and two solves of one camera within 30 days
agree to 0.015 deg median and 0.05 deg worst (0.026 and 0.75 before). Poses move a median
0.76 deg and up to 3.1 deg (bm-e, centre (-63, +39) px), each by about its centre offset.
The shipped pose_ledger.json is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…centre

The ledger gains cx/cy (a stable-record change), so this is a minor bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerated with `calibrate ledger` from the cache re-solved at a10c451, so every entry's
solver is 0.3.0+ga10c45148b. It keeps all 106 camera-nights of the ledger it replaces
(assembled in plume-triangulation, tag results-2026-09-26) and adds 31. The READMEs and
records.md quote its numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ses are off

HPWREN's table describes each camera's view, which is what the network was built for;
triangulation is a use it wasn't designed around. Drop the count of off-azimuth cameras
and the camera names, and describe the lens, centre and dated poses as what this adds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rharnish
rharnish merged commit bf6cdb9 into main Oct 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant