polarization oppolsweep → image2dimport fullseye as fs; fs.ledger.polarization_separate(images, angles_deg=(0.0, 45.0, 90.0, 135.0), max_violation_frac=0.0) (実装を直接呼ぶなら import specularity; specularity.polarization_separate(images, angles_deg=(0.0, 45.0, 90.0, 135.0), max_violation_frac=0.0)、台帳から引くなら opsspecular.get("polarization_separate"))fullseye.ledger.polarization_separate(...) は宣言 out 型 image2d の値だけを返す(本体は補助情報も返す)。捨てられた側が要るときは fullseye.ledger.polarization_separate.raw(...)、または specularity.polarization_separate を直接呼ぶ。Split a polariser sweep into its unpolarised and linearly polarised radiance. → (diffuse, specular), both (H, W).
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
:func: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
(:func: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 :func: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
:data:POLAR_NEGATIVE_FLOOR.
Raises ValueError: images is not an (N, H, W) stack of at
least 3 frames, has no pixels, or exceeds :data:MAX_LIGHTS /
:data: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 :data:POLAR_DESIGN_MAX_COND), or
has every angle at most 2pi 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.
py -3.11 examples/perf_polarization_analyze.pypy -3.11 examples/poc_polarization_specular.pypy -3.11 examples/specular_photometric.pyimage2d を入力に取れる)polarization_render · deflectometry_render · deflectometry_phase_to_slope
polarization)polarization_render · polarization_dolp_map · polarization_stokes · polarization_stokes_map
Provenance: specularity.py — SPECULAR operator registry. この per-op ノートは tools/opdocs.py md が自動生成(手編集しない)。
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.