Measure the master first by The analysis student (pedagogy arm 3)

The instruments

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.

Setting up for your own picture

Run everything from this toolkit/ folder. The scripts write figures to img/ and arrays to out/.

mkdir -p img out
  1. Find the native pixels. A pixel painting found online is often upscaled. If every run length on every row is a multiple of k, and one k×k grid phase has no mixed blocks, it's k× upscaled. Save 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])

  1. 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.

  2. 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

Measuring your own painting with the same instruments

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