BookNotesSlidesP-sets Syllabusdraft · v0.2.49.1
◀▲⌂▶
💬Comments welcome. To leave a note, select any text and click the note / highlight button that pops up — or open the panel with the tab at the top-right (‹). Notes are visible only inside our private review group.

45.23 Reading a Lytro light field⧉

A Lytro .lfr is not a picture, and it is not yet a light field either. It is a single sensor frame, a lenslet image (a honeycomb of thousands of tiny circular sub-images, one behind each microlens; see Chapter 31.2, Light field cameras), plus a bundle of metadata describing the camera that made it. The 4-D light field $L(u,v,s,t)$ is in there, but sampled on a lattice that does not align with a rectangular pixel grid: the microlenses sit on a hexagonal grid, rotated by a fraction of a degree relative to the sensor rows, with a pitch of ~14 pixels that is not an integer and does not align with the Bayer mosaic underneath. "Decode it" hides two different jobs:

  1. The container job: get the bits out: parse the .lfr wrapper, unpack the raw sensor frame, and read the metadata (the microlens geometry, the color tags, the black/white levels). This is container parsing, and a library does it for us.
  2. The geometry job: put the rays on a uniform grid: locate every microlens center to sub-pixel accuracy, then resample each sub-image onto a regular $(u,v)$ angular block and each microlens onto a regular $(s,t)$ spatial lattice, converting the rotated hex sampling into the rectangular 4-D array $L(u,v,s,t)$ that every downstream algorithm wants. This is the hard part, and it is what this appendix is really about.
💡 The one rule

The microlens lattice is the whole problem. Estimate it to sub-pixel accuracy (ideally from a flat white image, not from the metadata alone) before you resample. The uniform grid you are building is an interpolation of the real lenslet centers; if your lattice is off by even a fraction of a pixel, or you forget that the rows are hexagonally staggered, the regular grid beats against the true one and you get banding and cross-talk that no later step removes. Get the geometry right first; everything else (color, exposure) is a normal-imaging problem layered on top.

45.23.1 The container: the LFP/LFR format⧉

A .lfr (and the older .lfp) is a simple chunked container. It opens with an 8-byte magic header (\x89LFP\r\n\x1a\n), then a sequence of length-prefixed sections each tagged by a magic word (LFM for a metadata chunk in UTF-8 JSON, LFC for a component of binary data) and identified by an embedded sha1-… string. One JSON metadata chunk is the table of contents: it names, by sha1, the chunks holding the raw image, the per-frame metadata, and a JPEG thumbnail. The raw sensor data itself is 10-bit, packed four pixels into five bytes (the Illum sensor is $7728\times5368$); the F01 predecessor packed 12-bit into a $3280\times3280$ frame.

We do not hand-roll any of this: imageio's built-in Lytro plugin (LYTRO-LFR, LYTRO-ILLUM-RAW) parses the container, unpacks the 10-bit data, and returns the raw lenslet image normalized to $[0,1]$ together with the per-frame metadata dict. Everything from that raw image onward is ours (figures/src/lytro_illum.py). The metadata keys we need: devices.mla (microlens lensPitch, rotation, scaleFactor, sensorOffset), devices.sensor.pixelPitch, the Bayer mosaic description, the black/white levels, and the color-correction matrix ccm.

45.23.2 The pipeline, step by step⧉

Inputs: the raw lenslet image and per-frame metadata (from imageio), and, strongly recommended, a white image for the same camera at a matching zoom/focus (below).

1. Black/white level correction. Subtract the per-channel black level and rescale to the white level (both in the metadata, in 10-bit counts). The sensor's black floor (~65/1023 on our Illum) is small but real; skipping it tints the shadows.

2. Demosaic. The lenslet image is a Bayer mosaic like any other sensor frame, so demosaic it to RGB (we reuse the book's green-based demosaic, Part 15, Demosaicking and joint methods). The Illum's pattern is GRBG (we confirmed this directly from the raw mosaic: over an orange subject the red sites are brightest, the blue sites dimmest). Demosaic before the angular resampling, while the data still has its full sensor resolution.

3. Build the microlens grid model. From the metadata, the nominal lattice pitch in pixels is lensPitch / pixelPitch, adjusted by the metadata scaleFactor in each axis (≈ $20\,\mu\text{m}/1.4\,\mu\text{m} = 14.3$ px). For a hexagonal "row-major" array the vertical row spacing is $\tfrac{\sqrt3}{2}$ of the pitch; the array rotation and the sensorOffset (which places the lattice center relative to the sensor center) finish the model. This is a serviceable grid on its own.

4. Refine the grid from a white image (sub-pixel). The metadata grid is good to a fraction of a pixel, but for clean output we fit the lattice to the data: blur the white image, find the bright center of every microlens disc near its predicted location, and least-squares-fit a full affine lattice (spacing, rotation, shear, center) to those centers. This affine fit improves the metadata grid (following the calibration approach of Dansereau, Pizarro & Williams 2013); it is not a full calibration of each ray through the main lens.

5. Resample to a uniform grid. For each microlens $(s,t)$ and each angular sample $(u,v)$, the corresponding sensor position is the microlens center plus a rotated angular offset; read it with bilinear interpolation (Chapter 6.5, Resampling and upsampling). This single gather turns the rotated hex sampling into an array whose Python axis order is (t, s, v, u, channel). Two subtleties decide the quality, both in From hexagonal lenslets to a uniform grid below: the angular offsets must span one lenslet, no more, and the hexagonal row stagger must be rectified.

6. Devignette (photometric flat-field). Each microlens images the main-lens aperture unevenly, so corner angular samples are darker and color-shifted: a per-$(u,v)$ vignetting. Decode the white image through the same grid into its own 4-D field and divide; because the white target is uniform, this flattens both the spatial and angular falloff. We divide by the white field's luminance (achromatic) so devignetting corrects brightness without re-tinting; white balance is a separate step. Achromatic division does not correct spatially varying color shading, and samples near the pupil boundary need particular care because dividing by a weak calibration signal amplifies noise. The implementation clamps the normalized flat field to 0.1 before division.

7. Color. White-balance, then apply the metadata color-correction matrix ccm (camera RGB → linear sRGB; its rows sum to 1, so it preserves neutrals). Do not use the metadata whiteBalanceGain directly (see Easy mistakes); it is in a Lytro-internal convention and does not neutralize the demosaicked RGB. The implementation derives gains from the white image or, lacking one, gray-world. A factory white image measures the calibration illuminant, however, not necessarily the scene illuminant. These gains do not guarantee neutral scene colors; use a neutral target under the scene lighting when color accuracy matters.

8. The result. Steps 1–7 produce a scene-linear 4-D light field. As in the DNG appendix, show it through a display tone-map (auto-exposure + gamma) only at the end: never bake the display curve into the array you will refocus or merge on.

Reference implementation (steps 1–7, pseudocode)
raw, meta = imageio.imread(lfr)              # container parse + 10-bit unpack
lin   = (raw - black)/(white - black)        # 1: level correction (per Bayer channel)
rgb   = demosaic(lin, pattern="GRBG")        # 2: Bayer → RGB (full sensor res)
grid  = grid_from_metadata(meta)             # 3: hex lattice: pitch, rotation, offset
grid  = refine_from_white(white_raw, grid)   # 4: affine lattice fit to lenslet peaks
L     = build_lightfield(rgb, grid)          # 5: bilinear resample → L[s,t,u,v,c]
Lw    = build_lightfield(demosaic(white), grid)
L     = L / luminance(Lw).clip(eps)          # 6: achromatic devignetting
wb    = neutral_from(white) or gray_world(L) # 7: white balance (NOT metadata gains)
L     = clip((L * wb) @ ccm.T, 0)            #    colour-correction matrix → linear sRGB
# L is the scene-linear 4-D light field. Tone-map (gamma) is display-only.

Array layout note: our code stores L[t, s, v, u, c] (spatial rows/cols, then angular rows/cols, then color), the book's $L(u,v,s,t)$ in a fixed index order. A sub-aperture view is L[:, :, v, u]; the central view is the all-but-pinhole photo at $(u,v)$ centered.

45.23.3 From hexagonal lenslets to a uniform grid⧉

This step has two common failure modes.

Angular span. Each microlens images the main-lens aperture onto a disc ~14 px across. The $(u,v)$ samples for that microlens must be read from within that disc. Span wider and you read into the dark gaps between lenslets and into the neighbors' discs: angular cross-talk that shows up as a halo or a color fringe at the edge of every sub-aperture view. We sample an odd $N\times N$ block (typically $13\times13$) with sample spacing equal to the pitch divided by $N$, rotated to match the array. The two extreme sample centers are therefore separated by $(N-1)/N$ of a pitch. The middle sample lands on the fitted lenslet center. A square block still has corners outside a circular pupil; those angular views need a pupil mask or confidence weights before quantitative use.

Hexagonal rectification. In a hex "row-major" array, alternate rows are offset by half a pitch. If you simply stack one sample per microlens into a rectangular array, the odd rows are shifted half a lenslet sideways from the even rows: a half-pixel comb that reads as fine horizontal banding on any smooth gradient. The fix is to resample the staggered rows onto a common column grid (interpolate the odd rows back by half a sample). This rectification approximates a rectangular spatial lattice. The implementation leaves the first column of staggered rows unchanged because it lacks a left neighbor; crop that boundary for methods that require uniform sampling. (This banding is geometric, not photometric: it is identical in a mono decode, which is how you tell it apart from vignetting.)

fig-lenslet-to-grid
Figure 45.23.1. From the raw lenslet image to a uniform 4-D grid. Left: a magnified crop of the raw Lytro sensor frame, a honeycomb of circular sub-images, one per microlens, on a hexagonal lattice rotated slightly off the pixel rows. Middle: the fitted lattice, with one microlens highlighted; its center is the spatial coordinate $(s,t)$ and the pixels inside its disc are the angular coordinate $(u,v)$. Right: after resampling and hexagonal rectification, the regular 4-D array $L(u,v,s,t)$, drawn as the grid of sub-aperture views. The rotated, staggered hex sampling on the left has become the rectangular grid on the right.

45.23.4 Why a white image⧉

A white image is a frame of a uniform, out-of-focus white surface: the target supplies approximately uniform radiance, while the variation within and across microlens discs reveals the lattice and measures vignetting. Its color response is a reference for the calibration illumination. The Lytro Illum ships a set of them (the dataset we use bundles 34, taken at assorted zoom and focus settings); the camera's caldata archive stores them as .RAW+.TXT pairs. The right one to use is the one whose zoom/focus matches the capture; our find_white_image picks it automatically by reading the zoom/focus steps from each white image's metadata. With it you get a sub-pixel grid (step 4), clean devignetting (step 6), and a color reference for the calibration illuminant (step 7); without it you fall back to the metadata grid and gray-world color, whose geometric and color errors depend on the capture.

fig-white-image-calibration
Figure 45.23.2. What the white image buys you. (a) A white-image crop: the evenly-lit microlens discs whose centers define the lattice. (b) The fitted lattice overlaid on the discs. (c)–(d) A decoded sub-aperture grid before and after devignetting by the white field: the corner views go from dark and color-cast to matching the center. Without a white image the decode still works (metadata grid + gray-world), but the corners stay uneven.
fig-lytro-decode
Figure 45.23.3. A real Lytro Illum capture decoded to a 4-D light field (INRIA dataset). Top: the central sub-aperture view (the ordinary photo the main lens would have taken) next to the dataset's reference render: they match in framing and color. Bottom: a $3\times3$ selection of sub-aperture views; watch the background shift against the foreground from corner to corner: that parallax is the angular dimension $(u,v)$, the whole point of capturing the field. From here, Chapter 31.4, Refocusing and synthetic aperture, is a shift-and-add over these views.

45.23.5 Open questions and where we approximate⧉

Lytro is defunct, so unlike the DNG appendix we cannot ask the vendor; these are the places our decode is an informed approximation rather than a verified match.

  1. The color "look." We reproduce demosaic → white balance → color matrix faithfully, but Lytro's app applied an additional proprietary tone-and-hue "look" (as Lightroom does for DNGs, Chapter 45.15, Rendering a raw DNG — from sensor numbers to a faithful image). Our central views match the dataset's reference renders in geometry and overall color; exact hue/saturation may differ.
  2. whiteBalanceGain convention. The metadata gains do not neutralize the demosaicked RGB when applied as plain multipliers (they leave a strong cast: in our tests greens and magentas swap). We instead derive gains from the white image, with the scene-illuminant limitation described above. We do not know the exact stage/space those metadata gains are meant for.
  3. Metadata grid vs white-image grid. For our camera the metadata pitch was accurate and only the center/rotation needed fitting; in general we trust the white-image affine fit. Whether the metadata grid is ever canonical we leave open.

45.23.6 Easy mistakes (most of which we made)⧉

A wrong light-field decode looks almost right. In rough order of how badly each bit us:

fig-lytro-mistakes
Figure 45.23.4. Plausible-looking ways to get the decode wrong, versus the correct pipeline, the same Lytro capture throughout. Metadata WB (a strong color cast, greens and magentas swap); no hexagonal rectification (fine horizontal banding on smooth areas); no devignetting (dark, color-cast corner views); and the correct decode next to the dataset's reference render. Each failure looks superficially fine, which is why these bugs survive review.