Skip to content

12mm lens effective focal length measures 13.04mm, not the nominal 12.0 — self-heal cannot identify it #627

Open

Description

@brickbots

The 12mm lens entry in optics.py carries the nominal 12.0 mm as its effective focal length, flagged effective_focal_length_measured=False with a comment asking for an on-sky sweep. A rev4 imx462 unit with a 12 mm lens has now provided that measurement, and the nominal is wrong by 8.7%.

Measurement

Device: rev4, imx462, 12 mm lens, no camera_lens in config (lens assumed), running e499621a (#624). Solving normally on the assumed-lens gate.

Fitted FOV from /api/status, sampled over ~30 s:

12.436  12.436  12.437  12.439  12.434   (Matches 13-27)

Backing the effective focal length out of the 980 px @ 2.9 µm crop:

half_width_mm = 980 * 2.9 / 1000 / 2 = 1.421
EFL = 1.421 / tan(12.437 / 2)  =  13.04 mm     (+8.7% vs the 12.0 label)

The crop geometry is not in question here: the 16 mm's effective_focal_length_mm = 15.61 was fit against this same 980 px crop to reproduce the imx462's calibrated 10.38° field width. So the discrepancy sits in the lens, not the sensor profile. Note the sign is opposite to the 16 mm, which measured 2.4% short of its label.

Consequences today

Self-heal never promotes on these units. identify_lens_from_fitted_fov compares against the derived 13.507°:

lens derived FOV error vs fitted 12.437°
16mm 10.403° 19.6%
12mm 13.507° 7.9%

LENS_IDENTIFY_TOLERANCE is 0.05, so nothing is identified, the streak resets every frame, and camera_lens is never written. ADR 0029's "a fitted FOV matching no shipped lens writes nothing" branch is firing on a lens we actually shipped. The device keeps solving on the wide assumed gate forever, which is the safe outcome but not the intended one.

Every derived angle for 12 mm users is 8.6% wide — plate scale, the frustum, and the SQM zero point keyed to it.

The logger.info that explains the non-write is invisible in practice: the shipped logconf_default.json sets the root logger to ERROR, so no Integrator or Solver line reaches pifinder.log at all. Anyone debugging this on a stock unit sees silence.

Proposed fix

Set 12mm to the measured effective focal length (~13.04 mm) and flip effective_focal_length_measured=True. Self-heal then identifies at ~0.02% error and promotes on the next three solves.

Gate this on a second measurement before merging. The 16 mm's value earned its authority by reproducing two independently calibrated field widths on two different sensors — one unit is thinner evidence than that, and this constant moves every 12 mm user's SQM. A second 12 mm unit, ideally on an imx296 so the sensor half varies, would settle it the same way.

Knock-on changes once the value moves:

  • stated-12mm gate becomes [10.57, 14.30] instead of [11.48, 15.53]
  • the imx462 assumed union re-centres (currently 12.19 ± 3.35)
  • the imx296's 12 mm derived field moves too, and its SQM zero point with it

Rejected: widening LENS_IDENTIFY_TOLERANCE

Raising it to ~0.10 promotes today in one line, but it writes camera_lens=12mm while the device still derives 13.507°. The gate then tightens to [11.48, 15.53] around a field that is really 12.44° — 0.96° of headroom — and the 8.6% SQM error stays. It papers over a constant we know to be wrong.

Related

Activity

  1. brickbots commented on Aug 18, 2026

    @brickbots
    OwnerAuthor

    16 mm swap on the same unit confirms the crop geometry and the 15.61 mm value

    Same rev4 imx462 board, same night, same code (e499621a). Removed the 12 mm, fitted a 16 mm, restored camera_lens to absent so the lens was assumed again, and let self-heal run.

    Self-heal promoted in 9 seconds

    20:53:46 Solver:INFO:     Optical train: assumed 16mm lens on imx462, field of view 10.40 deg,
                              FOV gate [8.84, 15.53]
    20:53:54 Integrator:INFO: Lens self-heal: 3 solves fitted 10.41 deg, which is the 16mm
                              (10.40 deg derived). Stating it in config; the FOV gate narrows from
                              the assumed range to this lens on the next frame.
    20:53:55 Solver:INFO:     Optical train: stated 16mm lens on imx462, field of view 10.40 deg,
                              FOV gate [8.84, 11.96]
    

    "camera_lens": "16mm" landed in config, and the solver picked up the narrowed gate on the next frame. The full ADR 0029 path — assumed gate admits the solve, fitted FOV identifies the lens, promotion narrows the gate — works end to end.

    The measurement

    fitted FOV vs derived 10.403°
    10.4157° +0.12%
    10.4117° +0.08%
    10.3758° −0.26%
    mean 10.4011° −0.02%

    Backing the effective focal length out of the same 980 px @ 2.9 µm crop:

    EFL = 1.421 / tan(10.4011 / 2) = 15.613 mm      (registry: 15.61)
    

    Why this matters here

    The 16 mm reproduces its derived field to 0.02% through the same code path, on the same sensor, on the same night that the 12 mm missed by 7.9%. That rules out the competing explanation for the original report — that the imx462 crop_size or pixel_pitch_um might be wrong and the 12 mm merely the first lens to expose it. Both halves of the sensor profile are confirmed correct, so the 8.7% discrepancy sits entirely in the 12mm entry's unmeasured effective_focal_length_mm = 12.0.

    Side by side on one board:

    lens registry EFL measured? fitted vs derived
    16mm 15.61 mm yes −0.02%
    12mm 12.0 mm no (nominal placeholder) −7.9%

    This does not close the "second measurement before merging" caveat above — that asked for a second 12 mm sample to pin the new constant, ideally on an imx296 so the sensor half varies too. It does remove the main reason to doubt the first one.

    Incidental confirmation of the swap deadlock

    Worth recording since it happened in practice: once self-heal states 16mm, refitting the 12 mm gates [8.84, 11.96] against a real 12.44° field, so nothing solves and self-heal cannot recover it — it only ever writes into an absence. Recovery is the Lens menu or removing the config key by hand. This is ADR 0029's accepted consequence and #611's scope, noted here only because this issue's reproduction path walks straight into it.

  2. brickbots commented on Aug 18, 2026

    @brickbots
    OwnerAuthor

    12 mm refitted: promotes in three solves. Fixed in #628.

    Closing the loop on the original report. Same board, 12 mm back on, camera_lens cleared so the lens was assumed again, running the corrected registry value.

    21:47:14 Integrator:INFO: Lens self-heal: 3 solves fitted 12.43 deg, which is the 12mm
                              (12.44 deg derived). Stating it in config; the FOV gate narrows...
    21:47:16 Solver:INFO:     Optical train: stated 12mm lens on imx462, field of view 12.44 deg,
                              FOV gate [10.57, 14.30]
    
    before after
    derived 12 mm field 13.507° 12.4382°
    fitted 12.4294° (45 matches) same
    error vs derived −7.92% — outside the 5% tolerance, never identified −0.071%

    The measurement never changed; only the value it was being compared against did. That is the whole bug.

    The recomputed assumed gate also went live and is confirmed in the log:

    21:14:11 Optical train: assumed 16mm lens on imx462, field of view 10.40 deg,
                            FOV gate [8.84, 14.30]
    

    One thing that cost ten minutes, now fixed in the same PR

    The first 12 mm run after the swap produced 189 failed solves and zero successes. The failure log said:

    Solve FAILED - 2 centroids detected but pattern match failed (FOV est: 12.0°, max err: 4.0°)
    

    Those FOV numbers are hardcoded — the retired pre-0027 constants — not the gate in force, which was [8.84, 14.30]. Reading that line naturally suggests a gate problem. The real cause was in the other half of the same message: 1–3 centroids, i.e. the lens needed refocusing after the swap. tetra3 never reached the gate. Once refocused it solved with 45 matches and promoted immediately.

    Anyone landing here from a no-solve report should read the centroid count first and disregard the FOV figures on that line in 2.6.2 and earlier. #628 makes it report the real gate and whether the lens is stated or assumed.

    Still open

    The single-sample caveat from the issue body stands: 13.04 mm rests on one 12 mm. A second — ideally on an imx296, so the sensor half varies too — would put it on the same footing as the 16 mm's 15.61. Recorded in the ADR 0029 amendment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions