polarization_separate — SPECULAR polarization op

• Data kinds: polsweep → image2d

• Call: import fullseye as fs; fs.ledger.polarization_separate(images, angles_deg=(0.0, 45.0, 90.0, 135.0), max_violation_frac=0.0) (implementation को सीधे call करना हो तो import specularity; specularity.polarization_separate(images, angles_deg=(0.0, 45.0, 90.0, 135.0), max_violation_frac=0.0), registry से लेना हो तो opsspecular.get("polarization_separate"))

• Ledger के ज़रिए return value: fullseye.ledger.polarization_separate(...) केवल घोषित out type image2d का value लौटाता है (मूल function सहायक जानकारी भी लौटाता है)। जो हिस्सा छोड़ दिया गया है उसकी ज़रूरत हो तो fullseye.ledger.polarization_separate.raw(...) का उपयोग करें, या specularity.polarization_separate को सीधे call करें।

• GPU: नहीं (केवल CPU reference implementation)

उपयोग

polariser sweep को उसकी unpolarised और linearly polarised radiance में बाँटता है। → (diffuse, specular), दोनों (H, W)।

> नीचे का विस्तृत विवरण मूल text में है — सारांश और headings का अनुवाद हो चुका है।

Fitting I(t) = 0.5 (S0 + S1 cos 2t + S2 sin 2t) per pixel gives I_min and I_max in closed form, and the classical separation (Wolff & Boult 1991; Nayar, Fang & Boult 1997) reads

diffuse = 2 * I_min (the unpolarised radiance)

specular = I_max - I_min (the linearly polarised radiance)

with diffuse + specular = I_min + I_max = the total scene radiance, so nothing is lost or invented. Round-tripping polarization_render through this operator returns the inputs with a maximum absolute error of 3.9e-16 for the four angles of a division-of-focal-plane sensor and 4.4e-16 for a bare three-angle sweep (measured in tests/test_specularity.py).

Read the names as shorthand. What is recovered exactly is the unpolarised and polarised parts. Calling them diffuse and specular assumes diffuse reflection is unpolarised and specular reflection is fully linearly polarised — true near Brewster's angle for a dielectric, false at normal incidence, where the specular reflection is unpolarised and this operator returns all of it as "diffuse", and unreliable for metals. The polarisation route is complementary to the colour route (specular_diffuse_split), not a replacement: it needs no illuminant colour and works on textured, multi-material surfaces, but it needs a favourable geometry.

*max_violation_frac* is the fraction of pixels allowed to fit a negative minimum radiance before the call fails. The default 0 is fail-closed: a negative fitted minimum means the modulation exceeded the mean, which no analyser can produce, and it usually means the frames and *angles_deg* are out of order. Raise it to clamp sensor-noise-level violations to zero as a deliberate, recorded choice.

What value to raise it to (measured 2026-09-15). Violations come from pixels whose *unpolarised* radiance sits at the noise floor, not from the noise level as such: uniform scenes with unpolarised fraction ≥ 0.2 show 0.0000 violations at 0.5–5 % noise, and only a scene that is almost fully polarised (unpolarised fraction 0.05) fails (2 % noise → 0.0018, 5 % → 0.13). On a gradient scene (unpolarised radiance 0 → 0.6, absolute noise σ, four angles) the violation fraction tracks the share of pixels with D < 2σ: σ=0.005 → 0.0076 (2σ share 0.023), σ=0.01 → 0.015 (0.039), σ=0.02 → 0.025 (0.070), σ=0.05 → 0.058 (0.172). So estimate the share of pixels darker than 2–3 σ and set *max_violation_frac* to that; the full table is in docs/ops/specular/guides/specular_photometric.md. The frame-order claim above is weaker than it reads: with equally spaced angles (0/45/90/135, 0/60/120, and also 0/30/90/135 and 0/20/45/90/160) every permutation of a noise-free sweep still fits a non-negative minimum (all permutations measured, violation 0.0000) — a wrong order looks like a correct sweep at another azimuth, so the order is metadata this check cannot recover.

Negative frames (2026-10-24). A pixel with a frame value below -POLAR_NEGATIVE_FLOOR * max|I| (2 % of the stack's largest magnitude) is a violation too, in the same *max_violation_frac* budget: a negative radiance is not a scene, and polarization_render refuses it in the forward direction. The floor leaves room for the read noise of dark-frame-subtracted data; the reasoning is at POLAR_NEGATIVE_FLOOR.

Raises ValueError: *images* is not an (N, H, W) stack of at least 3 frames, has no pixels, or exceeds MAX_LIGHTS / MAX_STACK_ELEMENTS; *angles_deg* does not match the frame count, leaves the fit rank-deficient (two angles equal modulo 180) or ill-conditioned (condition number over POLAR_DESIGN_MAX_COND), or has every angle at most 2*pi in magnitude (radians passed as degrees); more than *max_violation_frac* of the pixels fit a negative minimum or have a frame below the negative floor.

उपयोग की विस्तृत guide

• specular_photometric family guide

संदर्भ (sample data, साहित्य)

• Sample data catalog (DL URL / license) — 2-D के लिए skimage.data (BSD/public) + synthetic, 3-D के लिए real data sources (Stanford/PDS आदि) के DL URL।

• Operators का स्रोत और संदर्भ — इस op family के मूल research/methods के स्रोत।

• Algorithm का मानक स्रोत (लेखक, वर्ष) और उपयोग ऊपर की family usage guide में दिए गए हैं।

चलाए जा सकने वाले उदाहरण (verified samples जो सच में यह op call करते हैं)

• perf_polarization_analyze — py -3.11 examples/perf_polarization_analyze.py

• poc_polarization_specular — py -3.11 examples/poc_polarization_specular.py

• specular_photometric — py -3.11 examples/specular_photometric.py

वे अगले ops जिनसे type जुड़ता है (image2d को input के रूप में लेते हैं)

polarization_render · deflectometry_render · deflectometry_phase_to_slope

इसी category के (polarization)

polarization_render · polarization_dolp_map · polarization_stokes · polarization_stokes_map


*Provenance: specularity.py — SPECULAR operator registry. यह per-op note tools/opdocs.py md से अपने-आप generate होता है (हाथ से edit न करें)।*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.