These are the scripts I measured Ferrari's tarn with on 25 September, copied unchanged from my study folder. Today (26 September) I ran every one of them from a clean copy, and they reproduce the numbers in the tutorial. The one file here that wasn't an analysis script is pixelgeo.py, the clean-slope snapper my painter used. It's included because the crest measurement is what it came from.
Requirements: Python 3, numpy, scipy, scikit-image, scikit-learn, pillow, matplotlib.
Run everything from this toolkit/ folder. The scripts write figures to img/ and arrays to out/.
mkdir -p img out
a[::k, ::k] as img/00_native.png. Every script reads that file through util.load_ref().python
d = np.any(a[:, 1:] != a[:, :-1], axis=2)
print(np.bincount(np.concatenate([np.diff(np.flatnonzero(r)) for r in d[::7]]))[:12])
Trace the planes. planes.py holds my hand-traced polygons for the tarn, in native coordinates, plus a couple of colour tests. For another picture, rewrite the polygons: python src/grid.py draws a coordinate grid over the picture to trace from, and python src/crop.py x y w h scale name.png magnifies any region. Keep planes_names.py in step with your plane list. This step is by hand on purpose: see section 1 of the tutorial.
Run the instruments, in any order after planes.py:
| script | what it draws or prints | writes |
|---|---|---|
values.py |
greyscale and squint; notan in 2, 3 and 5 values; value range per plane; the flat value plan | 03_* |
lightmap.py |
five light classes from value and warmth, with magnified crops | 05_* |
light_probe.py |
the sky gradient by row and column. (Its lit/dark neighbour count is the dead end described in the tutorial.) | printout |
reflection.py |
fits the mirror axis and scale, and reports value and colour shifts by source value | 06_* |
form.py, profiles.py |
isophotes with gradient arrows per mass; value cross-sections | 07_* |
edges.py |
hard, middle and soft edges by measured rise width | 08_edges.png |
clusters.py |
same-colour cluster sizes, shapes and run lengths per plane | out/ref_cluster_stats.json |
dither.py |
2×2 checker, flat and stripe shares per plane; a map of every checker | 11_checker_map.png |
atlas.py |
32×32 windows at 7×, with each window's three commonest colours isolated (edit the window list) | 09_cluster_atlas.png |
palette.py |
colours by the material that uses them; a*b* and value-chroma charts; colours needed per plane | 10_* |
depth.py |
value, contrast, chroma, hue and detail per plane, near to far (edit ORDER) |
12_depth.png |
contours.py |
how a silhouette's top edge breaks into runs and steps (edit the column ranges) | printout |
seg_try.py |
k-means "planes": kept as the dead end it was | tmp_kmeans.png |
check_sheets.py and compare_plates.py run the same functions on a painter's output. They expect cells in sheets/cells/<key>.png, each with a <key>_planes.npy plane map written by the painter, and map my painter's planes onto Ferrari's. Change MY and CORR at the top for yours. The notan check alone needs no plane map:
from values import analyse # or, minimally:
Lb = gaussian_filter(lum(rgb), 2.5 * rgb.shape[1] / 634) # the same squint, scaled to picture width
dark, mid, light = [(np.digitize(Lb, (28.9, 55.6)) == i).mean() for i in range(3)] # Ferrari's cuts