prepare op• Data kinds: depth → depth
• Call: import fullseye as fs; fs.ledger.surface_filter(z, dx, lambda_c=None, lambda_s=None, kind='gaussian', end_effect='reject', dy=None) (to call the implementation directly, import roughness; roughness.surface_filter(z, dx, lambda_c=None, lambda_s=None, kind='gaussian', end_effect='reject', dy=None); from the registry, opsroughness.get("surface_filter"))
• Return value through the ledger: fullseye.ledger.surface_filter(...) returns only the declared out type depth (the underlying function also returns auxiliary values). When you need what was dropped, use fullseye.ledger.surface_filter.raw(...) or call roughness.surface_filter directly.
• Underlying return: (roughness, waviness)
Separates a height field into roughness and waviness (Gaussian filter of ISO 16610-21).
Returns (roughness, waviness). roughness is the component shorter than λc, waviness the longer one, and unless end_effect="reject", roughness + waviness matches the input (the primary surface after applying λs) exactly.
Arguments
z height field z[i, j] (row = y, column = x).
dx, dy sampling intervals. dy=None means square pixels.
lambda_c long-wavelength cutoff (boundary between roughness / waviness). With None, no split
(waviness is an array of zeros).
lambda_s short-wavelength cutoff (S-filter that removes noise). Disabled with None.
kind only "gaussian". Anything else raises an exception instead of being silently substituted.
end_effect "reject" (default) / "mirror" / "wrap". See below.
dy sampling interval along rows (None = dx).
★ End handling (end_effect) — the reason this op was written
Convolution needs "data from where there is none" at the ends, over the kernel radius (GAUSS_TRUNCATE·λc = 0.75 λc, 4 σ).
The standard (ISO 16610-28) presumes that the evaluation area is trimmed at both ends.
• "reject" (default) — discards ceil(0.75 λc/pitch) samples at both ends. The output is a smaller array than the input. The discarded region is "not measured", so this is the only option that tells no lie.
• "mirror" — extends by mirroring and returns the same shape. Assumes the surface is an even function at the ends.
• "wrap" — circular convolution (same as FFT). Assumes that beyond the left end comes the right end.
Measured (★ the table below was measured when the truncation was ±λc/2, before 2026-10-18. After widening the kernel to ±0.75 λc
items 1.–3. read the same; only the end band grows from 40 to 60 samples. The numbers were not re-measured).
The ground truth is a 704x704 surface filtered first and then cropped to the central 512x512
(the answer computed with real data present outside the ends). dx=1, λc=80,
end band = outer 40 samples, where the ground-truth rms is 0.1932.
surface end_effect end-band rms error center rms error
tilted wrap 4.3883 (2272 %) 0.0e+00 (0.000 %)
tilted mirror 0.1765 ( 91 %) 0.0e+00 (0.000 %)
tilt removed wrap 0.0111 ( 5.7 %) 4.4e-15 (0.000 %)
tilt removed mirror 0.0115 ( 5.9 %) 4.4e-15 (0.000 %)
(any) reject — not returned — 0.0e+00 (0.000 %)
4 things to read off:
1. End handling only matters at the ends. The center matches the ground truth exactly in every case (natural, since the kernel does not reach the ends, but verified).
2. The worst combination is "wrap with the tilt left in" — the end error is 23 times the ground truth itself. Since it assumes that beyond the left end comes the right end, a step rises at the seam between 2 edges of different height.
3. Once the tilt is removed, wrap and mirror do not differ (5.7 % vs 5.9 %, wrap slightly better). Not "circular convolution is always bad", but "using it without removing the tilt is bad" is correct.
4. The price of reject is area. With 512x512 and λc=80 it discards 41.4 % (392x392 remains; 28.8 % when the truncation was ±λc/2). The discarded fraction grows as 1 - (1 - 1.5 λc/L)², so once λc exceeds 1/6 of the evaluation length, nearly half is lost.
★ Is the transmission as the standard specifies? (measured, λc=80, dx=1, sines of integer period measured by projection. Fixed on 2026-10-18)
λ waviness ISO value diff (waviness / diff before the fix)
16 0.000030 0.000000 +3.0e-05 0.000806 / +8.1e-04
32 0.013094 0.013139 -4.5e-05 0.017206 / +4.1e-03
40 0.062544 0.062500 +4.4e-05 0.058676 / -3.8e-03
64 0.338531 0.338564 -3.3e-05 0.342826 / +4.3e-03
80 0.500012 0.500000 +1.2e-05 0.509549 / +9.6e-03 ← λc
128 0.762894 0.762799 +9.5e-05 0.772150 / +9.4e-03
256 0.934601 0.934550 +5.1e-05 0.937944 / +3.4e-03
Before the fix it was 0.5095, not 0.5000, at the cutoff wavelength itself (relative +1.9 %). The cause was neither the constant α
(√(ln2/π)) nor the normalisation after sampling but the truncation ±λc/2 (= 2.67 σ) in the wording of the standard:
it drops 0.8 % of the tails, and renormalising the rest to 1 raises the low-pass share.
With the truncation at ±0.75 λc (4 σ) the value at λc is 0.500012 and the maximum error over all wavelengths falls to 9.5e-05
(the table above). The price is that the end band discarded by reject becomes 1.5 times wider. The sum of roughness and
waviness matches the input exactly either way.
fail-closed
• kind other than "gaussian" / end_effect other than the 3 known values.
• both lambda_c and lambda_s are None (a call that does nothing).
• a cutoff below the Nyquist wavelength 2*pitch, or beyond the evaluation length.
• lambda_s >= lambda_c (the band becomes empty).
• no array remains after trimming with "reject".
• surface_roughness family guide
• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).
• Operator provenance and references — the sources of the research/methods this op family came from.
• The canonical algorithm (author, year) and its uses are named in the family usage guide above.
• poc_robust_profile_filter — py -3.11 examples/poc_robust_profile_filter.py
• poc_surface_roughness — py -3.11 examples/poc_surface_roughness.py
depth as input)surface_form_remove · robust_gaussian_surface · surface_params · surface_psd
prepare)surface_form_remove · robust_gaussian_profile · robust_spline_profile · robust_gaussian_surface
*Provenance: roughness.py — ROUGHNESS operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.