sfp_disambiguate — OPTICS shape_from_polarization op

• Data kinds: table → normalmap

• Call: import fullseye as fs; fs.ledger.sfp_disambiguate(candidates, prior='convex', mask=None, center=None, prior_normals=None, coarse_depth=None, brewster_tol=0.02, invalid='raise') (to call the implementation directly, import sfpolar; sfpolar.sfp_disambiguate(candidates, prior='convex', mask=None, center=None, prior_normals=None, coarse_depth=None, brewster_tol=0.02, invalid='raise'); from the registry, opsoptics.get("sfp_disambiguate"))

Usage

Pick one normal per pixel from the candidate set of sfp_normals, using a stated prior — convexity with Brewster-ridge segmentation, or a coarse depth / normal map.

prior="convex" (an object bulging toward the camera, such as a dome, a lens or a cap): the azimuth is the candidate pointing away from *center* (default: the centroid of *mask*). For the specular model the two zenith branches are separated by the Brewster ridge — the pixels whose DoLP is within *brewster_tol* of 1: the connected region of the mask that contains *center* (minus the ridge) takes the branch below Brewster's angle, the rest of the mask the branch above. Inside the ridge band the branch is read from the radial slope of the DoLP (a convex surface tilts more toward its rim, so the DoLP rises outward below Brewster's angle and falls outward above it). If the object never reaches Brewster's angle there is no ridge and every pixel stays on the low branch, which is right for a shallow dome. If noise breaks the ridge, the outside leaks in and takes the low branch — measured in the tests, not hidden.

prior="normals" / prior="depth": per pixel, the candidate closest in angle to *prior_normals* (H, W, 3) — or to the normals of *coarse_depth* (H, W) (height toward the camera, pixel units, photometric.surface_normals). This resolves both ambiguities at once, and a coarse depth needs to be right only to within the angular distance between candidates.

Pixels outside *mask* (default candidates["valid"]) are set to (0, 0, 1) (flat background, the convention of photometric.integrate_normals). Invalid pixels inside the mask (valid is False: dark, physically impossible, or out of the model's

reach): invalid="raise" refuses (fail-closed), "exclude" drops them to the background.

Returns an (H, W, 3) float64 unit normal map (the normalmap sort).

Raises ValueError: *candidates* not an sfp_normals result; unknown *prior* / *invalid*; a prior's array missing or of the wrong shape; an empty mask; invalid pixels inside the mask with invalid="raise".

Family-wide input contract (fail-closed)

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.

Detailed usage guide

• optics_imaging family guide

References (sample data, literature)

• 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.

Runnable examples (verified samples that actually call this op)

• poc_shape_from_polarization — py -3.11 examples/poc_shape_from_polarization.py

Ops the type connects to (they accept normalmap as input)

grating_rgb · thin_film_rgb · ward_anisotropic · micro_normals · blast_normals · finish_shade · oren_nayar · clearcoat_shade

Same category (shape_from_polarization)

sfp_dolp_model · dolp_to_zenith · aolp_to_azimuth · sfp_render · sfp_normals · sfp_estimate_index · sfp_height


*Provenance: sfpolar.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.