Files
he11lib/docs/superpowers/specs/2026-07-03-camera-geometry-redesign.md
T
Martino Ferrari 18d6276ab9 Add design spec for full camera geometry & measurement uncertainty
Generalizes camera modeling from a single pixel-scale/viewing-angle pair
to a full pinhole camera (3D orientation + position + intrinsics),
projected via true perspective (not just uniform cosine compression),
plus 2D beam pointing and per-plane z uncertainty. Every nominal
geometry value is now paired with an explicit tolerance that determines
whether it's held fixed or refined jointly with the mode fit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 09:00:22 +02:00

223 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# he11lib — Full Camera Geometry & Measurement Uncertainty Redesign
**Date:** 2026-07-03
**Status:** Approved for planning
**Supersedes:** parts of `docs/superpowers/specs/2026-07-02-gyrotron-mode-purity-design.md`
relating to `MeasurementPlane.pixel_scale`/`viewing_angle_deg`, `GeometryCalibration`,
and the single-scalar beam pointing angle. All other decisions in the original spec
(mode basis, noise, deconvolution, phase-retrieval fallback, package layout) are
unchanged and still apply.
## Purpose
The original design modeled camera geometry as a single, isotropic pixel scale plus
one viewing-angle tilt (a uniform, affine correction across the whole frame), and
treated each `MeasurementPlane`'s `z` distance as exact. Real measurement setups are
more demanding:
- The camera's orientation relative to the beam axis has a full 3 rotational degrees
of freedom (yaw, pitch, roll), not one.
- The camera is close/wide-angle enough that true perspective projection (keystoning
that varies across the frame) matters, not just a uniform compression factor.
- A physical calibration step (e.g. fiducial markers) gives nominal values for camera
position, orientation, and intrinsics — but mechanical vibration means none of these
can be trusted as exact; all must be refined jointly with everything else.
- The beam's pointing/tilt is two-dimensional (independent horizontal and vertical
tilt), not a single scalar angle.
- Each plane's `z` distance (from a translation stage or tape measure) is also only
known to a nominal precision and should be refined, not trusted exactly.
## Scope of this change
- Replace the single pixel-scale/viewing-angle model with a full pinhole camera model
shared across all planes in one reconstruction.
- Generalize beam pointing from one angle to two (horizontal, vertical).
- Model each plane's `z` as a nominal value with its own refinable uncertainty.
- Unify "trusted/fixed" vs. "uncertain/refined" behind a single tolerance mechanism
(see below), replacing the old `None`-means-unknown convention for geometry.
- Out of scope: lens distortion (radial/tangential), rolling-shutter effects, and
multi-camera setups. These are not part of the current measurement setup and would
need a separate design if they become relevant.
## Architecture
### Data model changes (`data.py`)
- `MeasurementPlane` drops `pixel_scale` and `viewing_angle_deg` (per-plane geometry
no longer makes sense once the camera pose is a single shared physical setup for
the whole reconstruction). It gains `z_tolerance: float`, the ± bound (in meters)
around the nominal `z` within which the true distance is refined. `z_tolerance` must
be `>= 0`; `0` means `z` is trusted exactly and held fixed.
- `ReconstructionResult.pointing_angle_deg: float` becomes two fields:
`pointing_angle_horizontal_deg: float` and `pointing_angle_vertical_deg: float`.
- `ReconstructionResult.geometry` gains entries for the fitted `CameraModel` fields
(see below) alongside the existing per-plane fitted `z` values.
### `CameraModel` and `CameraModelTolerance` (`geometry.py`)
```python
@dataclass
class CameraModel:
focal_length_px: float
position: tuple[float, float, float] # (x, y, z) in the beam-axis frame,
# z=0 at the output window
orientation_deg: tuple[float, float, float] # (yaw, pitch, roll); all-zero =
# boresight normal to the target
# plane, no in-plane rotation
principal_point: tuple[float, float] = (0.0, 0.0) # (px, px) offset from frame center
@dataclass
class CameraModelTolerance:
focal_length_px: float
position: tuple[float, float, float]
orientation_deg: tuple[float, float, float]
principal_point: tuple[float, float] = (0.0, 0.0) # (px, px)
```
`CameraModel` always represents a nominal point estimate (from calibration, or an
assumed default), never a value trusted as exact by itself — trust/uncertainty is
expressed entirely through the paired `CameraModelTolerance` (see "Tolerance
mechanism" below).
`GeometryCalibration` is rewritten around these two types. Given a `CameraModel` and a
target plane's `z`, it provides:
- **Forward projection** — physical `(X, Y)` at that `z` to pixel `(row, col)`, via
standard pinhole projection: rotate/translate the world point into the camera
frame using `orientation_deg`/`position`, then perspective-divide using
`focal_length_px`/`principal_point`.
- **Inverse projection** — pixel `(row, col)` to physical `(X, Y)`, via rayplane
intersection: cast a ray from the camera through each pixel and intersect it with
the known `z =` const target plane. This is what produces genuine keystoning (the
correction varies across the frame), replacing the old uniform
`x = col * scale / cos(angle)` formula.
- Raises `ValueError` for degenerate poses where the target plane is edge-on to or
behind the camera (no valid intersection).
### Tolerance mechanism (applies to `CameraModel`/`CameraModelTolerance` and
`MeasurementPlane.z`/`z_tolerance`)
Every nominal geometry value is paired with a tolerance that determines how
`ModalFitter` treats it:
- **`tolerance == 0`**: held fixed at the nominal value. Excluded entirely from the
optimizer's parameter vector and substituted as a constant into the model function.
This recovers the old "fully known, don't fit it" behavior.
- **`tolerance > 0`**: included in the fit, bounded to
`[nominal - tolerance, nominal + tolerance]` via `scipy.optimize.least_squares`'
`bounds=`.
There is no "fully unbounded" mode for these parameters — if a value is genuinely
unconstrained, its tolerance should be set generously wide rather than infinite,
since an unbounded 7-9 parameter homography fit from 3-10 planes has little chance of
converging usefully without some bound.
### `ModalFitter` changes (`fitting.py`)
The optimizer's parameter vector is now built dynamically per reconstruction:
- Always free (unchanged in kind, generalized in the pointing case): complex LG mode
coefficients, per-plane beam transverse center `(x, y)`, and the two beam pointing
angles (`pointing_angle_horizontal_deg`, `pointing_angle_vertical_deg`).
- Conditionally free (new): any `CameraModel` field whose paired
`CameraModelTolerance` entry is nonzero, and any plane's `z` whose `z_tolerance` is
nonzero — each bounded as described above.
`BeamReconstructor.__init__` gains required `camera: CameraModel` and
`camera_tolerance: CameraModelTolerance` parameters (alongside existing `w0`, `z0`,
`wavelength`), replacing the old optional per-plane pixel-scale/viewing-angle
handling.
### `SyntheticBeamGenerator` changes (`synthetic.py`)
Takes an exact ground-truth `CameraModel` (position/orientation/intrinsics) and the
two beam pointing angles, and generates each plane at its own exact/ground-truth `z`
— which may deliberately differ from the nominal `z` given to the resulting
`MeasurementPlane`, so tests can verify the fit recovers the true `z` despite a
deliberately offset nominal input.
## Data flow (updated)
1. Build a list of `MeasurementPlane` (flux array + nominal `z` + `z_tolerance` +
label), plus a nominal `CameraModel` + `CameraModelTolerance` for the whole
reconstruction.
2. `GeometryCalibration` resolves the pixel↔physical mapping per plane from the
(possibly-still-being-refined) `CameraModel` and that plane's (possibly-being-
refined) `z`.
3. `NoiseEstimator` computes per-plane noise weights (unchanged).
4. `DiffusionDeconvolver` optionally deblurs each plane (unchanged; still assumes an
isotropic pixel-space kernel — noted as an existing approximation, not addressed
by this redesign).
5. `ModalFitter` runs the joint noise-weighted nonlinear least-squares fit over LG
coefficients + per-plane center + 2 pointing angles + any nonzero-tolerance camera
and `z` parameters, growing the mode set automatically as before.
6. Phase-retrieval fallback and `ReconstructionResult` assembly proceed as in the
original design, with `pointing_angle_deg` replaced by the horizontal/vertical
pair and `geometry` extended to include the fitted `CameraModel` fields and
per-plane fitted `z`.
## Testing strategy
In addition to the original design's synthetic-ground-truth-recovery approach:
- **`CameraModel` projection round-trip**: for several poses (on-axis, tilted,
off-center) and several `z` values, physical→pixel→physical recovers the original
point.
- **Keystone regression**: projecting a symmetric grid through a tilted/off-axis
`CameraModel` produces non-uniform spacing across the frame (distinguishing it from
the old uniform cosine-compression model).
- **Degenerate pose**: a pose placing the target plane edge-on to or behind the
camera raises `ValueError`.
- **Tolerance semantics**: a `tolerance=0` field stays exactly at its (deliberately
wrong) nominal value rather than being corrected; a `tolerance>0` field recovers a
ground truth offset from nominal but within its band; a ground truth placed outside
a deliberately too-tight band is clipped to the bound rather than escaping it.
- **End-to-end**: the full pipeline recovers mode purity, both pointing angles, the
camera pose, and per-plane `z`, when `SyntheticBeamGenerator`'s ground truth is
offset from the nominal inputs (within their tolerances) — simulating realistic
calibration/measurement error rather than assuming perfect nominal values.
## Error handling
- `CameraModelTolerance` fields and `MeasurementPlane.z_tolerance` must be `>= 0`;
`ValueError` otherwise. Validated at construction, consistent with the existing
"validate only at boundaries" approach.
- Degenerate camera geometry (target plane edge-on to or behind the camera) raises
`ValueError` from the projection code rather than producing NaNs.
- **New documented pitfall**: with only 3-10 planes, adding ~7-9 shared camera
unknowns plus one `z` correction per plane can be practically underdetermined even
though each plane contributes many pixels of data, because those unknowns are
*global* and only weakly constrained by subtle keystone differences between planes.
`fit_auto`/`BeamReconstructor` emit a `UserWarning` (not an error, consistent with
existing warn-don't-raise style) when the free-parameter count is large relative to
the number of planes, prompting the user toward tighter tolerances rather than
silently returning an ill-conditioned fit.
## Migration impact
This is a breaking change to the public API, acceptable pre-1.0 with no external
users yet:
- `MeasurementPlane(pixel_scale=..., viewing_angle_deg=...)`
`MeasurementPlane(z_tolerance=...)` + a reconstruction-level `CameraModel`/
`CameraModelTolerance`.
- `BeamReconstructor(...)` requires new `camera`/`camera_tolerance` arguments.
- `SyntheticBeamGenerator.generate(...)` requires a `CameraModel` and two pointing
angles instead of scalar `viewing_angle_deg`/`pixel_scale`/`pointing_angle_deg`.
- `ReconstructionResult.pointing_angle_deg``pointing_angle_horizontal_deg` +
`pointing_angle_vertical_deg`.
- `docs/api.md` and `examples/full_pipeline_example.py` need updating to match.
- The existing pitfalls documented in `CLAUDE.md` (Rayleigh-range clipping,
mode-growth overfitting) still apply unchanged; the new degeneracy pitfall above is
additive.
## Deliverables
- Updated `geometry.py` (`CameraModel`, `CameraModelTolerance`, rewritten
`GeometryCalibration`), `data.py`, `fitting.py`, `synthetic.py`, `reconstruct.py`.
- Updated tests covering projection round-trip, keystone behavior, degenerate poses,
tolerance semantics, and end-to-end recovery under offset nominal inputs.
- Updated `docs/api.md` and `examples/full_pipeline_example.py` reflecting the new
public interface.
- Updated `CLAUDE.md` with the new degeneracy pitfall.