Decimate (block reduce)
Reduce a w×h grid by an integer factor: every f×f block of input cells becomes one
output cell holding the block’s mean, min or max. The output is
ceil(w/f) × ceil(h/f). This is the first engine op that changes grid size, so it is also the
first that must derive a placement rather than pass one through.
What it means
Section titled “What it means”- Block mean is coverage fraction. Block-averaging a 0/1 canopy mask by
fgives the canopy-cover fraction per coarse cell — the psychogeoblock_meanstep that turns a 1 m foliage mask into a coarser percentage layer. - Block min/max are coarse surfaces. Min of a last-return surface is a cheap ground floor; max of a first-return surface is a coarse canopy top. Both are the non-overlapping cousins of the morphology erode/dilate.
- Edge blocks reduce over the cells that exist. When
worhis not a multiple offthe right/bottom blocks are partial; their mean is the mean of the present cells, not of a zero-padded full block. Factor 1 is accepted by the kernel as the identity; the graph node restrictsfactorto 2..64.
Placement
Section titled “Placement”Array space here is corner-indexed: cell (i, j) occupies [i, i+1) × [j, j+1), so a
cell’s world centre is origin + (i+½)·axes[0] + (j+½)·axes[1]. Output cell (I, J) covers
array-space [I·f, (I+1)·f), hence
worldFromArray_out = worldFromArray_in · scale(f, f, 1)— origin unchanged, in-plane axes multiplied by f. The output cell’s centre is exactly the
centroid of its block’s input-cell centres (asserted in the test). A partial edge block keeps
the nominal full-block footprint.
GPU approach
Section titled “GPU approach”One "use gpu" thread per output cell loops over its block — no atomics, no workgroup
memory, fixed summation order — so min/max are bit-exact against the CPU golden and the mean
agrees to f32 rounding. Tier-2 resident as a graph node: the output buffer is leased at the
smaller size and the next op reads it in place.
import { decimateGpu } from 'intraspatial/gpu/spatial/decimate';
const cover = await decimateGpu(canopyMask, w, h, 8, 'mean'); // ceil(w/8) × ceil(h/8)As a graph node: g.op1("decimate", { grid }, { factor: 8, mode: "mean" }) — the output
handle’s placement is already the scaled one.
Composes with
Section titled “Composes with”- Morphology before it — open the mask, then block-mean it: canopy-cover fraction per coarse cell.
thresholdafter it — cover ≥ 0.5 → “dense woodland” at the coarse resolution.
Status: ✅ implemented · src/gpu/spatial/decimate.ts, graph op
src/gpu/graph/ops/decimate.ts. Not yet nodata-aware (terrain gap 3).