polarization op• Data kinds: image2d → polsweep
• Call: import fullseye as fs; fs.ledger.polarization_demosaic(raw, layout=((90.0, 45.0), (135.0, 0.0))) (to call the implementation directly, import optics; optics.polarization_demosaic(raw, layout=((90.0, 45.0), (135.0, 0.0))); from the registry, opsoptics.get("polarization_demosaic"))
Split a polarisation-sensor mosaic into the four analyser images.
A polarisation camera puts four micro-polarisers on each 2x2 block of
pixels; the raw frame is one (H, W) array in which neighbouring pixels saw
the scene through different analysers. This returns a `(4, H, W)` array
of images `[I_0, I_45, I_90, I_135] (the polsweep` sort, in
:data:POLARIZATION_SWEEP_ANGLES order, so the result feeds
:func:specularity.polarization_stokes and `polarization_dolp_map`
directly), each interpolated to full resolution.
Interpolation is bilinear, the same estimate a Bayer demosaic makes for
a colour plane that occupies one pixel in four: a missing pixel is the mean
of its measured 4-neighbours (edge-adjacent) or 4-neighbours (diagonal),
which is exactly the `[[1, 2, 1], [2, 4, 2], [1, 2, 1]] / 4` kernel applied
to the masked plane. Ground truth: on a plane that is linear in x and y
the interpolation is exact (a bilinear estimate of an affine field is the
field), so the test plants four affine fields, mosaics them, and demands
every returned image equal its field to 1e-12 away from the border. The
border is handled by mirroring the mosaic two pixels outward before
interpolating (an even, non-duplicating reflection keeps the 2x2 phase), so
a uniform field is exact up to the edge and a gradient is mirrored there —
a symmetric bias on the outermost row and column, and said so. Against
Polanalyser's OpenCV bilinear path the interior agrees to the 16-bit
quantisation (1.8e-5) and only the outer two pixels differ (2026-09-18).
*layout* is the angle at each 2x2 position, `((a00, a01), (a10, a11))` in
degrees; the default is the Sony IMX250MZR block. Every angle in
:data:POLARIZATION_SWEEP_ANGLES must appear exactly once.
Raises `ValueError`: *raw* is not a 2-D array, has an odd height or
width (the block would be cut), contains non-finite values, or *layout* is
not a permutation of the four sweep angles.
Provenance: the layout convention and the "four Bayer planes" reading of
the mosaic follow Polanalyser (Maeda, MIT); the interpolation is the
classic bilinear Bayer demosaic. This is a re-implementation from that
description, not copied code, and it does not depend on OpenCV. Colour
polarisation sensors (IMX250MYR, a 4x4 block) are handled by
:func:polarization_demosaic_color.
Every optics op validates its input before computing (nothing slips through silently):
• Units are baked into the argument name — _mm / _um / _deg / _mrad. Confusing mm with µm does not crash; it yields a plausible-looking wrong answer, so the name prevents it. Nothing here guesses the unit from the magnitude.
• **Strings raise ValueError** — float('50') succeeds, so an unparsed configuration value would slip through as a length (measured: thin_lens('50', '200') returned a plausible 66.667 mm). bool is refused too, as the implicit promotion True == 1.
• **complex / masked arrays raise ValueError (real-valued slots only; silently dropping the imaginary part or peeling off the mask is refused). NaN/Inf raises ValueError on every input.**
• Division by zero and its relatives are refused by name: focal length 0, radius of curvature 0, refractive index <= 0, a fully opaque aperture (all zeros, so the normalisation is 0/0), a PSF whose sum is <= 0, a Stokes vector with S0 = 0, and an object sitting at the front focal point (the image is at infinity).
• Only two ops return a non-finite value, and both state it as a contract: depth_of_field returns far_mm = inf beyond the hyperfocal distance (that is what the hyperfocal distance means), and gaussian_beam returns wavefront_radius_mm = inf at the waist (the radius of curvature of a plane wavefront). Both also return a finite companion (far_is_infinite / curvature_per_mm). **Any other silent NaN/Inf is detected internally and raises ValueError** — "float64 overflowed" and "the answer is infinite" are different claims, so the first is never returned wearing the face of the second.
• Size caps: generated grids are capped by optics.MAX_GRID (4096); supplied fields/PSFs/apertures by optics.MAX_FIELD_ELEMENTS (2^24); ABCD element chains by optics.MAX_SYSTEM_ELEMENTS (1024); Zernike by MAX_ZERNIKE_TERMS (512) / MAX_ZERNIKE_ORDER (40) / MAX_ZERNIKE_BASIS (2^25). This closes, fail-closed, the paths where a small argument triggers a huge internal allocation (measured: n_max=40 × 4096² needs 108 GB).
• Physically impossible states are refused too: a Stokes vector with degree of polarisation > 1, negative transmittance, negative intensity, and invalid Zernike indices such as n-|m| odd.
• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).
• Operator provenance and references — the sources of the research/methods this op family came from.
• The canonical algorithm (author, year) and its uses are named in the family usage guide above.
• polarization_camera_pipeline — py -3.11 examples/polarization_camera_pipeline.py
polsweep as input)—
polarization)jones_element · jones_apply · stokes_from_jones · mueller_element · mueller_apply · stokes_analyze · polarization_demosaic_color · mueller_from_intensities
*Provenance: optics.py — OPTICS operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.