tb_lf_epi_slope — 2D typed op

• Data kinds: lightfield → image

• Call: fullseye.apply(img, "tb_lf_epi_slope", a=0.5, b=0.5) (the 2-D model is one image plus two scalar knobs a,b∈[0,1])

• GPU: yes (mechanical GPU path from the allow-list, CuPy). device="auto" chooses the GPU from the measured size (589,824 pixels) up

tb_lf_epi_slope: input → output

*The figure is the actual output on a synthetic 128×128 input. Left: input, right: output. Point clouds are drawn as a top-down scatter (brightness = z), 1-D series as a line plot, volumes as the maximum-intensity projection along z, videos as the middle frame, complex images as magnitude; return values that are not pictures are shown as the values themselves.*

*Knob a does not change the output (measured: identical at 0.1 / 0.5 / 0.9).*

*Knob b does not change the output (measured: identical at 0.1 / 0.5 / 0.9).*

Stages (the ops that come before → this op, left to right):

▸ tb_lf_epi_slope: stages (docs site)

On other images (synthetic scene / photo / coins. Top row: inputs, bottom row: their outputs. Knobs at default):

▸ tb_lf_epi_slope: other inputs (docs site)

Usage

Per-pixel slope from the EPI line orientation (structure tensor, refined by shearing).

A scene point traces a straight line in the epipolar-plane image (lf_epi), so along that line the intensity is constant:

E_u + s * E_x = 0. Accumulating that constraint over the whole angular grid and a window x window spatial neighbourhood gives the closed-form least-squares slope s = -(J_ux + J_vy) / (J_xx + J_yy) with J_ab = sum(E_a * E_b) — both the horizontal and vertical EPI directions pooled.

Why it is iterated (fixed 2026-11-03). A finite difference across views measures a displacement of s px per view, while a finite difference across pixels measures one of 1 px; for a texture component of angular frequency w the ratio is sin(w s) / sin(w) (the pre-fix central differences), equal to s only at |s| in {0, 1}. That is the classic statement that the structure tensor is accurate only for disparities in a small range around zero (Wanner & Goldluecke, CVPR 2012 and TPAMI 36(3) 2014, who handle larger ranges by shearing the light field). So the estimator shears the field by an integer slope k (integer shifts: no resampling blur), measures the residual r = s - k, sets k <- round(k + r) per pixel and repeats until no pixel changes (at most EPI_MAX_ROUNDS rounds, at most EPI_MAX_SHEARS distinct shears). The residual is measured with a matched discretisation (adjacent-view difference against the mean of the two views' Gaussian derivatives, inner scale EPI_INNER_SIGMA), whose error is O(r^3 w^2) with |r| <= 1/2.

Measured 2026-11-03, median over the interior of single-layer lf_synthesize fields (64x64, edge="wrap"), s in [-3, 3] in steps of 0.25: error <= 0.006 px for 5x5 and 9x9 views, <= 0.007 px for 4x4 (even count) and <= 0.012 px for 3x3 and for a single row of 6 views, at texture sigma 1.0 / 1.5 / 2.0 / 5.0 px, for windows 5 / 9 / 15 (the pre-fix estimator read -1.388 for -1.5 at sigma 2 and 1.46 for 2.0 at sigma 1.5). Integer slopes are exact to 1e-9 on a wrapped field. Valid range: the shear ladder starts at k = 0, so the first pass must see the right sign and a non-vanishing magnitude: that holds for |s| <= 3 at any texture correlation length >= 1 px, and (measured) up to |s| = 8 at sigma >= 2; at sigma 1 it fails beyond |s| = 5 (s = 6 read as 0.25 — the texture is aliased: it moves more than its correlation length several times per view). Near an occlusion boundary the window mixes two slopes (no estimator of a single orientation per window can avoid that). With edge="nearest" input the outer |k|*(N-1)/2 px of the frame see clamped border pixels.

Returns (slope_map, energy): the (H, W) slope map (centre-view coordinates) and the (H, W) gradient energy J_xx + J_yy of the unsheared field (the first pass's denominator). Pixels whose energy is below *min_energy* have no measurable parallax (a flat patch of sky); their slope is set to 0 and their energy reported as-is, so you threshold on energy instead of being handed a plausible-looking number divided by ~0.

Raises ValueError: *lf* not a valid light field, an angular/spatial shape where *neither* EPI direction carries information (the horizontal EPI needs U >= 2 and W >= 2, the vertical needs V >= 2 and H >= 2), an even or non-positive *window*, a non-positive *min_energy*.

Typed bridge of the lightfield op lf_epi_slope into the 2-D evolution registry: the same implementation, called under the op(v, a, b) convention. This op has no tunable parameter; a and b are unused.

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.

Try it in Studio

The program below has been verified to run (same input as the figure). In Studio's help this block becomes buttons that load and run it on the spot.

img_to_lightfield 0.50 0.50
tb_lf_epi_slope 0.50 0.50

▸ Load this pipeline  ·  Load & run

Runnable examples (verified samples that actually call this op)

The examples below call the underlying ledger op lf_epi_slope. This bridge op is the same implementation adapted to the fn(v, a, b) convention, so the behaviour carries over unchanged (only the call form differs).

• lightfield_depth — py -3.11 examples/lightfield_depth.py

• poc_lightfield_depth — py -3.11 examples/poc_lightfield_depth.py

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

identity · gaussian · mean_box · mean_box_wh · bilateral · kuwahara · unsharp · median

Same category (typed)

tb_points_to_voxel · tb_estimate_point_normals · tb_iss_keypoints · tb_project_points · tb_render_point_depth · tb_points_render · tb_statistical_outlier_removal · tb_radius_outlier_removal


*Provenance: ops.py — 2D 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.