photometric op• Data kinds: images → normalmap
• Call: import fullseye as fs; fs.ledger.photometric_stereo(images, lights, mask=None, normalize=True, *, lit_only=False, lit_thresh=0.001) (to call the implementation directly, import photometric; photometric.photometric_stereo(images, lights, mask=None, normalize=True, *, lit_only=False, lit_thresh=0.001); from the registry, ops3d.get("photometric_stereo"))
• Return value through the ledger: fullseye.ledger.photometric_stereo(...) returns only the declared out type normalmap (the underlying function also returns auxiliary values). When you need what was dropped, use fullseye.ledger.photometric_stereo.raw(...) or call photometric.photometric_stereo directly.
Lambertian photometric stereo: recover normals and albedo from N images taken under known light directions. → (normals HxWx3, albedo HxW).
I_n = albedo * max(N·L_n, 0). At each pixel g = albedo*N is solved by least squares g = pinv(L) @ I, albedo=|g|, normal=g/|g|. N>=3 is required. Pixels with albedo~0 or outside the mask get normal=(0,0,1). normalize=True normalises the light vectors to unit directions (same convention as render_lambertian = the absolute albedo comes out correctly). If you use intensity-weighted light sources, set normalize=False and use raw vectors on the synthesis side as well.
lit_only (added 2026-09-04): solves with attached shadows excluded. The model's max(·, 0) is non-linear, so feeding observations with N·L < 0 (true value 0) straight into linear least squares biases the solution — measured: on a sphere with neither shadows nor AO the median is 9°; solving with only the lit light sources gives 0.000°. Default is False (the previous behaviour is unchanged). With True, each pixel is re-solved using only the light sources with I > lit_thresh. Pixels with the same lighting pattern are grouped and processed with 1 pseudo-inverse, so the extra cost grows only roughly with the number of patterns over the light count (at most 64 groups for 6 lights). Pixels with fewer than 3 lit sources fall back to the all-lights solution (fail-open:
a value, even a biased one, breaks downstream integration less than setting unsolvable pixels to NaN).
lit_thresh: lower bound of the luminance regarded as "lit". Place it above the imaging noise.
• 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.
• photometric_stereo — py -3.11 examples_3d/photometric_stereo.py
normalmap as input)fuse_to_voxel · render_shaded · phong_shade · matcap_shade · brdf_lommel_seeliger · brdf_hapke · bump_normals_fbm · integrate_normals
photometric)surface_normals · integrate_normals · render_lambertian
*Provenance: photometric.py — 3D 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.