Skip to content

Commit d10c30a

Browse files
author
David Emanuel Luksic
committed
High pass cutoff
1 parent 7e454db commit d10c30a

1 file changed

Lines changed: 51 additions & 26 deletions

File tree

  • apps/typegpu-docs/src/examples/image-processing/camera-fft

apps/typegpu-docs/src/examples/image-processing/camera-fft/index.ts

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import tgpu, { common, d, std } from 'typegpu';
99
import { defineControls } from '../../common/defineControls.ts';
1010

1111
/**
12-
* Pipeline: camera → luminance (optional separable Hann window) → `encodeForward` → radial low-pass on the
13-
* spectrum buffer → optional `encodeInverse` (spatial) or log-magnitude spectrum colored in **Oklab**
12+
* Pipeline: camera → luminance (optional separable Hann window) → `encodeForward` → radial low- or high-pass on the
13+
* spectrum buffer (independent radial low- and high-pass masks, multiplied) → optional `encodeInverse` (spatial) or log-magnitude spectrum colored in **Oklab**
1414
* (lightness from magnitude, hue from complex phase via `a,b`).
1515
* Line FFT: **radix-4 (default)** (faster Stockham-style radix-4 + optional radix-2 tail) or **radix-2**
1616
* (pure Stockham radix-2). One compute pass chains fill, FFT, filter, inverse FFT, and spatial/mag; then a
@@ -144,8 +144,10 @@ const filterParamsType = d.struct({
144144
padWMask: d.u32,
145145
padHLog2: d.u32,
146146
padHMask: d.u32,
147-
/** 1 = pass all bins; 0 = DC only. Scales max toroidal radius √(hw²+hh²), hw = padW/2, hh = padH/2. */
148-
cutoffRadius: d.f32,
147+
/** Normalized low-pass radius vs max toroidal √(hw²+hh²); keep r ≤ cutoff. */
148+
lowPassCutoff: d.f32,
149+
/** Normalized high-pass inner radius; keep r > cutoff (DC at r = 0 is removed when cutoff = 0). */
150+
highPassCutoff: d.f32,
149151
/** 1 when `Fft2d.skipFinalTranspose`: spectrum is `W×H` row-major with stride `padH` (`r*padH+c`), not `y*padW+x`. */
150152
swapSpectrumAxes: d.u32,
151153
});
@@ -193,17 +195,21 @@ const filterKernel = tgpu.computeFn({
193195
const hw = d.f32(halfW);
194196
const hh = d.f32(halfH);
195197
const rMax = std.sqrt(hw * hw + hh * hh);
196-
const cutoff = filterLayout.$.params.cutoffRadius * rMax;
197198
const r = std.sqrt(r2);
198-
const mask = std.select(d.f32(0), d.f32(1), r <= cutoff);
199+
const lowC = filterLayout.$.params.lowPassCutoff * rMax;
200+
const highC = filterLayout.$.params.highPassCutoff * rMax;
201+
const lowMask = std.select(d.f32(0), d.f32(1), r <= lowC);
202+
const highMask = std.select(d.f32(0), d.f32(1), r > highC);
203+
const mask = lowMask * highMask;
199204
const c = filterLayout.$.spectrum[tid];
200205
filterLayout.$.spectrum[tid] = d.vec2f(c.x * mask, c.y * mask);
201206
});
202207

203208
const magParamsType = d.struct({
204209
padW: d.u32,
205210
padH: d.u32,
206-
gain: d.f32,
211+
/** Exposure in stops (EV): brightness scales as `exp2(exposure)` on top of the neutral log stretch. */
212+
exposure: d.f32,
207213
padWLog2: d.u32,
208214
padWMask: d.u32,
209215
swapSpectrumAxes: d.u32,
@@ -250,7 +256,9 @@ const magKernel = tgpu.computeFn({
250256
);
251257
const cShift = magLayout.$.spectrum[srcTid];
252258
const len = std.sqrt(cShift.x * cShift.x + cShift.y * cShift.y);
253-
const logv = std.log(1.0 + len) * magLayout.$.params.gain;
259+
/** Neutral at `exposure = 0` matches the former default `log(1+|·|) * 0.2` scale. */
260+
const logv =
261+
std.log(1.0 + len) * 0.2 * std.exp2(magLayout.$.params.exposure);
254262
const cv = std.clamp(logv, 0.0, 1.0);
255263
/** `cv` from log-magnitude; L ∈ [0.04, 1]; chroma → 0 at max `cv` so peaks go neutral white. */
256264
const L = 0.04 + cv * (1.0 - 0.04);
@@ -269,6 +277,8 @@ const spatialParamsType = d.struct({
269277
padWMask: d.u32,
270278
/** `1 / (padW * padH)` — unnormalized inverse scaling. */
271279
invSize: d.f32,
280+
/** Same EV as spectrum: linear sample is multiplied by `exp2(exposure)` before clamp. */
281+
exposure: d.f32,
272282
});
273283

274284
const spatialLayout = tgpu.bindGroupLayout({
@@ -297,7 +307,7 @@ const spatialKernel = tgpu.computeFn({
297307

298308
const c = spatialLayout.$.spectrum[tid];
299309
const inv = spatialLayout.$.params.invSize;
300-
const g = std.clamp(c.x * inv, 0.0, 1.0);
310+
const g = std.clamp(c.x * inv * std.exp2(spatialLayout.$.params.exposure), 0.0, 1.0);
301311
const padWLog2 = spatialLayout.$.params.padWLog2;
302312
const padWMask = spatialLayout.$.params.padWMask;
303313
const x = tid & padWMask;
@@ -476,9 +486,12 @@ const renderPipeline = root.createRenderPipeline({
476486

477487
/** When true: after forward FFT, run inverse and show grayscale spatial reconstruction. When false: show log-magnitude spectrum (forward only). */
478488
let applyInverseFft = false;
479-
let gainValue = 0.2;
480-
/** Normalized low-pass cutoff vs max toroidal radius (1 = no filtering). */
481-
let cutoffRadiusNorm = 1;
489+
/** Exposure in stops (EV), −4…+4; 0 is neutral vs the legacy spectrum scale. */
490+
let exposureEv = 0;
491+
/** Normalized low-pass radius vs max toroidal radius (1 = no attenuation). */
492+
let lowPassCutoffNorm = 1;
493+
/** Normalized high-pass inner radius (0 = no AC attenuation from this term). */
494+
let highPassCutoffNorm = 0;
482495
/** Separable Hann window on camera ROI before FFT (reduces periodic-boundary cross in spectrum). */
483496
let applyEdgeWindow = false;
484497
let lastFillUniformKey = '';
@@ -723,20 +736,20 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
723736
});
724737
}
725738

726-
const magUniformKey = `${padW}x${padH}x${gainValue}x${activeFft.skipFinalTranspose}`;
739+
const magUniformKey = `${padW}x${padH}x${exposureEv}x${activeFft.skipFinalTranspose}`;
727740
if (magUniformKey !== lastMagUniformKey) {
728741
lastMagUniformKey = magUniformKey;
729742
magParams.write({
730743
padW,
731744
padH,
732-
gain: gainValue,
745+
exposure: exposureEv,
733746
padWLog2,
734747
padWMask: padW - 1,
735748
swapSpectrumAxes: activeFft.skipFinalTranspose ? 1 : 0,
736749
});
737750
}
738751

739-
const spatialUniformKey = `${padW}x${padH}x${gainValue}`;
752+
const spatialUniformKey = `${padW}x${padH}x${exposureEv}`;
740753
if (spatialUniformKey !== lastSpatialUniformKey) {
741754
lastSpatialUniformKey = spatialUniformKey;
742755
spatialParams.write({
@@ -745,10 +758,11 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
745758
padWLog2,
746759
padWMask: padW - 1,
747760
invSize: 1 / (padW * padH),
761+
exposure: exposureEv,
748762
});
749763
}
750764

751-
const filterUniformKey = `${padW}x${padH}x${cutoffRadiusNorm}x${activeFft.skipFinalTranspose}`;
765+
const filterUniformKey = `${padW}x${padH}x${lowPassCutoffNorm}x${highPassCutoffNorm}x${activeFft.skipFinalTranspose}`;
752766
if (filterUniformKey !== lastFilterUniformKey) {
753767
lastFilterUniformKey = filterUniformKey;
754768
filterParams.write({
@@ -758,7 +772,8 @@ function processVideoFrame(_: number, metadata: VideoFrameCallbackMetadata) {
758772
padWMask: padW - 1,
759773
padHLog2: log2Int(padH),
760774
padHMask: padH - 1,
761-
cutoffRadius: cutoffRadiusNorm,
775+
lowPassCutoff: lowPassCutoffNorm,
776+
highPassCutoff: highPassCutoffNorm,
762777
swapSpectrumAxes: activeFft.skipFinalTranspose ? 1 : 0,
763778
});
764779
}
@@ -851,24 +866,34 @@ export const controls = defineControls({
851866
lineFftMode = value as LineFftMode;
852867
},
853868
},
854-
gain: {
855-
initial: gainValue,
856-
min: 0.01,
857-
max: 0.45,
858-
step: 0.005,
869+
exposure: {
870+
initial: exposureEv,
871+
min: -4,
872+
max: 4,
873+
step: 0.05,
859874
onSliderChange: (value) => {
860-
gainValue = value;
875+
exposureEv = value;
861876
lastMagUniformKey = '';
862877
lastSpatialUniformKey = '';
863878
},
864879
},
865-
cutoffRadius: {
866-
initial: 1,
880+
lowPassCutoff: {
881+
initial: lowPassCutoffNorm,
867882
min: 0,
868883
max: 1,
869884
step: 0.01,
870885
onSliderChange: (value) => {
871-
cutoffRadiusNorm = value;
886+
lowPassCutoffNorm = value;
887+
lastFilterUniformKey = '';
888+
},
889+
},
890+
highPassCutoff: {
891+
initial: highPassCutoffNorm,
892+
min: 0,
893+
max: 1,
894+
step: 0.01,
895+
onSliderChange: (value) => {
896+
highPassCutoffNorm = value;
872897
lastFilterUniformKey = '';
873898
},
874899
},

0 commit comments

Comments
 (0)