Snow-peak student. Code: src/measure.py, src/boxes.py, src/dbg_matL.py, src/palcheck.py, paint.eff_dist. Story: POST.md §9–11.
Once a photo's camera is fitted (how-to 1), every photo pixel has a known distance, height, surface normal, sun visibility and material (from the DEM, WorldCover and the snow model). The photo stops being a picture to eyeball and becomes a measuring instrument. You can ask it what L* lit snow has at 21 km, how much darker the forest is at 5 km than at 60, or how blue a rock wall goes behind 20 km of air, and get a number from hundreds of pixels instead of a guess from one.
This page is about three uses of that: measuring the world, measuring your own painting against the photo, and the finding that came out of it, which is that haze depends on height.
measure.py resizes the photo to the G-buffer's size and takes the median Lab over masks built from the G-buffer:
lab = rgb2lab(photo_resized / 255.)
hi = terr & (G["z"] > 1600)
thr = threshold_otsu(lab[..., 0][hi]) # photo snow: Otsu split of L* up high
psnow = hi & (lab[..., 0] > thr)
lit = (G["ndl"] > 0.15) & (G["sunlit"] > 0.5) # from the real sun at the photo's timestamp
shade = (G["ndl"] < 0.0) | (G["sunlit"] < 0.5)
cls = dict(snow_lit=psnow & lit, snow_shade=psnow & shade,
rock_lit=hi & ~psnow & lit & (G["trees"] < 0.3), rock_shade=hi & ~psnow & shade & (G["trees"] < 0.3),
forest=terr & (G["trees"] > 0.6) & (G["z"] < 1500),
sky_low=G["sky"] & np.roll(terr, -15, 0)) # the sky just above the silhouette
for k, m in cls.items():
out[k] = dict(L=median(L[m]), a=median(a[m]), b=median(b[m]), dist_km=median(G["dist"][m]) / 1000)
Each class comes with its median distance, so you know at what range you measured it. boxes.py does the same for hand-placed boxes, and reports the 10th and 90th percentiles of L* (a spread tells you when a box straddles two things).
What the fitted photos say:
| photo | range | lit snow | shaded snow | rock | forest | sky at the peak | sky high |
|---|---|---|---|---|---|---|---|
| Rainier from Silver Queen, clear July noon | 17–21 km | 91 (b* +5) | 59 | wall 36–54 (b* −11) | 21 at 5 km (b* −18) | 64 (b* −19) | 47 (b* −29) |
| Glacier Peak from the east, Aug 09:20 | 12 km | firn 73–90 | dacite 46 (b* −4) | 19.5 | 63 (b* −30) | ||
| Rainier from the Seattle tower, clear May evening | 94 km | 65 (b* +11) | 60 (b* +6) | foothills 45 | 58 (b* −2) | 57 | |
| Rainier from Elliott Bay, August dusk | 96 km | 73 | 69 | 59 at 69 km | 74 | 76 |

Rainier from the Seattle tower, 94 km. The measured statement: lit snow only +7 L* over its sky and warmer than it; shaded snow +2; the foothill forest band the darkest thing on the horizon, 12 under the sky.
Four things to paint from:
This was the most useful tool of the whole study: under each material I painted, print the photo's L* distribution next to mine (dbg_matL.py):
S, G, C, rgb, pal = render.run(shot, season, 320, season_over=dict(z_sl=z))
ph = render.photo_native(S, 320, H); lab = rgb2lab(ph / 255.); mine = rgb2lab(rgb / 255.)
for name in paint.MATS:
m = C["mat"] == paint.M[name]
print(f"{name:7s} n={m.sum():5d} photo L {np.percentile(lab[...,0][m], [10,50,90]).round(1)}"
f" b {np.median(lab[...,2][m]):.1f} | mine L {np.median(mine[...,0][m]):.1f}")
On Rainier v4 it said:
snow n= 5953 photo L [52.3 82.9 91.1] b 4.0 | mine L 93.6
rock n= 1922 photo L [25.2 47.6 65. ] b -9.6 | mine L 45.1
scree n= 1574 photo L [39.2 54.5 66.1] b -5.3 | mine L 63.8
meadow n= 1526 photo L [16.4 28.1 45.4] b -8.4 | mine L 47.0
forest n=26319 photo L [10.2 14.5 21.9] b -9.7 | mine L 25.1
Every albedo I had guessed became a measurement to correct. Forest went from 0.035 to 0.0055 (and then 0.0045 with a blue tint, because the photo's forest is b* −10), meadow from 0.16 to 0.055, scree from 0.32 to 0.25, and granite later from 0.30 to 0.15 (Stuart's lichen-darkened walls: photo L* 43, mine 63).
Three things make it work:
palcheck.py prints L*/b* of every palette entry for a light state. It found my sky zenith at L* 61 against the photo's 47 before I'd looked at a single painting.Fit a Koschmieder visual range to the clear 94 km photo (forest at 60–70 km is about half lost), and you get about 80 km. Now paint the Silver Queen view with that: the forest 5–8 km below the camera comes out at L* 22.6. The photo has 14.6, nearly its intrinsic ~12. That forest is almost unhazed at 5–8 km, while forest at 65 km is half gone. No single haze distance fits both photos.
The difference is height. The Silver Queen camera stands at 2,129 m, and most aerosol lives in the boundary layer: its density falls roughly as exp(−h/H_a), with H_a ≈ 1.5–2.5 km. What a ray crosses isn't its length but its length weighted by that density. For a straight ray from camera height h_c to target height h_t, the mean density has a closed form, and the sea-level-equivalent haze path is:
def eff_dist(dist, hc, ht, Ha=1500.0):
"""sea-level-equivalent haze path (m): aerosol density falls as exp(-h / Ha) (Ha ~ 1-2 km, the
boundary layer), so a ray from hc to ht crosses dist * mean(exp(-h/Ha)) of sea-level haze.
This is why summits stand clear above hazy foothills."""
dh = ht - hc
small = np.abs(dh) < 1.0
mean = np.where(small, np.exp(-hc / Ha),
Ha * (np.exp(-hc / Ha) - np.exp(-ht / Ha)) / np.where(small, 1.0, dh))
return dist * mean
In the renderer it's one line per pixel (render.add_haze_path, H_a = 2 km), and every haze decision downstream uses deff, not distance: the transmittance τ = exp(−deff / D_h), the band edges and the band collapse.
G["deff"] = np.where(G["sky"], np.inf, paint.eff_dist(G["dist"], S.C.z, G["z"], 2000.0))
That one change put the painted forest at the photo's value (22.6 → 14.7; photo 14.6), with no albedo touched.

Rainier v9, with haze by haze path. The forest under the camera is near-black, as in the photo. The valley behind it lifts toward blue; the mountain stands clear above.
It's also the physics behind the painters' rules. Imhof asks cartographers to "sharpen and increase the contrasts of light and shadow towards higher altitudes, but subdue them somewhat in the low lying regions", and Ferrari keeps his peaks crisp over milky valleys. Both are drawing this. A summit at 4,000 m seen from 90 km crosses about 0.4× the haze of a sea-level target at the same range. From a 2,000 m vantage, the same summit at 20 km crosses about a fifth of the haze of a sea-level path of the same length.