Method
Nothing here is faked.
The canopy above is rendered from a known leaf area Λ, then measured as if it were a photograph. The measurement is a step-for-step port of hemispheR-py’s pipeline — down to numpy’s rounding — so the two give the same answers on the same pixels.
Import
Mask the fisheye circle, take the blue channel (sky is bright in blue, leaves are dark), gamma-correct and scale to 0–255.
Binarize
Otsu’s method picks the threshold that best splits sky from canopy — once for the whole frame, or per compass quadrant when the light is uneven.
Gap fraction
Map each pixel to its view-zenith angle through the lens model, bin into 7 rings × 8 sectors and take the fraction of sky in each.
Canopy attributes
Invert Beer–Lambert over the rings to get leaf area index, then compare with the per-sector estimate to get the clumping index.
Round trip
Render a canopy of known leaf area, measure the picture, compare. Over 24 canopies per setting, random canopies read about 6% low: pixels that blend leaf edge and sky tip toward “sky”. The error is steady from Λ = 1 to 4.
Clumping
Gather the same leaves into clusters and more light gets through, so Le falls by about 10–15% — while the corrected L typically stays within about 5% of the truth. That is the clumping index doing its job. Try the slider.
Parity with the Python tool
The same 12 canopies, run through the real hemispheR-py, give identical thresholds and identical Le, L, LX and DIFN — across lenses, channels, global and zonal thresholds, and ring counts. Your browser repeats the comparison on every load; CI blocks deploys if it ever fails.
What this does not prove. A synthetic canopy tests the arithmetic and the port, not your camera. Real photos add exposure, lens, mask and sky-condition error — the sky/canopy threshold is the biggest — which is why the Python tool exposes mask, channel, gamma, lens and thresholds. Shoot level, under even sky, and treat any single number as an estimate.