surface_filter — ROUGHNESS 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)

Usage

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 λc/2.

The standard (ISO 16610-28) presumes that the evaluation area is trimmed by λc/2 at both ends.

• "reject" (default) — discards ceil(λc/2/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 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 28.8 % (432x432 remains). The discarded fraction grows as 1 - (1 - λc/L)², so once λc exceeds 1/4 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)

λ waviness ISO value diff

16 0.000806 0.000000 +8.1e-04

32 0.017206 0.013139 +4.1e-03

40 0.058676 0.062500 -3.8e-03

64 0.342826 0.338564 +4.3e-03

80 0.509549 0.500000 +9.6e-03 ← max

128 0.772150 0.762799 +9.4e-03

256 0.937944 0.934550 +3.4e-03

At the cutoff wavelength itself it is 0.5095, not 0.5000 (relative +1.9 %). The cause is

the truncation ±λc/2 (= 2.67 σ) that the standard prescribes, which drops 0.8 % of the tails.

Widening the truncation to 4 σ (±0.75 λc) lowers the maximum error to 9e-05

(measured), but the band discarded by reject becomes 1.5 times wider. We chose to follow the standard and document the deviation. The sum of roughness and waviness always matches the input exactly

(roughness + waviness = 1.000000 in the table above), so only the dividing boundary

shifts by 1.9 %; no energy vanishes or appears.

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

Detailed usage guide

• surface_roughness family guide

References (sample data, literature)

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

Runnable examples (verified samples that actually call this op)

• poc_surface_roughness — py -3.11 examples/poc_surface_roughness.py

Ops the type connects to (they accept depth as input)

surface_form_remove · surface_params · surface_psd

Same category (prepare)

surface_form_remove


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