4f system
The 4f system is the classical optical configuration for spatial filtering. In this tutorial we build one and use it to remove or enhance selected spatial frequencies of an object.
Theory
Structure
input → f → L1 → f → Fourier plane → f → L2 → f → outputTwo lenses of equal focal length are placed apart, with the object one focal length in front of the first lens and the image one focal length behind the second.
The optical Fourier transform
In the back focal plane of the first lens (the Fourier plane) the field is the Fourier transform of the input:
A periodic object of period produces peaks in that plane at
Filtering
Placing a mask in the Fourier plane modifies the spectrum:
- Low-pass — passes only the central region (blurring);
- High-pass — blocks the centre (edge enhancement);
- Band-pass — passes an annulus of frequencies.
Step 1: Simulation setup
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
from svetlanna import SimulationParameters, Wavefront
from svetlanna.elements import ThinLens, FreeSpace, Aperture
from svetlanna.units import ureg
params = SimulationParameters.from_ranges(
x_range=(-5*ureg.mm, 5*ureg.mm), x_points=1024,
y_range=(-5*ureg.mm, 5*ureg.mm), y_points=1024,
wavelength=632.8*ureg.nm,
)
f = 100*ureg.mm # focal length of both lensesStep 2: A test object
A binary grating inside a circular aperture:
X, Y = params.meshgrid(x_axis="x", y_axis="y")
period = 0.5*ureg.mm
grating = (torch.cos(2 * torch.pi * X / period) > 0).float()
R = 2*ureg.mm
circular_mask = (X**2 + Y**2 < R**2).float()
test_object = grating * circular_mask
wf_input = Wavefront(test_object.to(torch.complex64))With mm and mm the diffraction orders in the Fourier plane are spaced by mm — the scale that all filter radii below are chosen against.
Step 3: Building the 4f system
class FourFSystem(nn.Module):
"""4f system with an optional filter in the Fourier plane."""
def __init__(self, params, focal_length, filter_mask=None):
super().__init__()
self.prop_f = FreeSpace(params, distance=focal_length, method="zpASM")
self.lens1 = ThinLens(params, focal_length=focal_length)
self.lens2 = ThinLens(params, focal_length=focal_length)
self.filter = (
Aperture(params, mask=filter_mask) if filter_mask is not None else None
)
def forward(self, wf, return_fourier=False):
# first half: input → Fourier plane
wf = self.prop_f(wf)
wf = self.lens1(wf)
wf = self.prop_f(wf)
wf_fourier = wf.clone() if return_fourier else None
if self.filter is not None:
wf = self.filter(wf)
# second half: Fourier plane → output
wf = self.prop_f(wf)
wf = self.lens2(wf)
wf = self.prop_f(wf)
if return_fourier:
return wf, wf_fourier
return wfStep 4: The system without a filter
An unfiltered 4f system reproduces the input, inverted:
system_no_filter = FourFSystem(params, f)
wf_output, wf_fourier = system_no_filter(wf_input, return_fourier=True)
print(f"Input max intensity: {wf_input.max_intensity:.4f}")
print(f"Output max intensity: {wf_output.max_intensity:.4f}")Output:
Input max intensity: 1.0000
Output max intensity: 1.0979The small overshoot is Gibbs ringing at the sharp edges of the binary object, which the finite grid cannot represent exactly.
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
axes[0].imshow(wf_input.intensity.cpu(), cmap='gray')
axes[0].set_title('Input object')
axes[1].imshow(torch.log1p(wf_fourier.intensity).cpu(), cmap='hot')
axes[1].set_title('Fourier plane (log)')
axes[2].imshow(wf_output.intensity.cpu(), cmap='gray')
axes[2].set_title('Output (inverted)')
plt.tight_layout()
plt.show()Step 5: Low-pass filter
A disc that passes only the zero and first orders:
filter_radius = 0.2*ureg.mm
low_pass_filter = (X**2 + Y**2 < filter_radius**2).float()
system_lowpass = FourFSystem(params, f, filter_mask=low_pass_filter)
wf_lowpass = system_lowpass(wf_input)
print(f"Low-pass output max intensity: {wf_lowpass.max_intensity:.4f}")Output:
Low-pass output max intensity: 1.5589Keeping only the first orders turns the square-wave grating into a sinusoid: the sharp edges are gone and the contrast rises.
Step 6: High-pass filter
Blocking the centre removes the DC term and leaves only the edges:
block_radius = 0.06*ureg.mm
high_pass_filter = (X**2 + Y**2 > block_radius**2).float()
system_highpass = FourFSystem(params, f, filter_mask=high_pass_filter)
wf_highpass = system_highpass(wf_input)
print(f"High-pass output max intensity: {wf_highpass.max_intensity:.4f}")Output:
High-pass output max intensity: 0.7093Step 7: Inspecting the spectrum
def plot_spectrum(wf_fourier, params, title="Spectrum"):
"""Show the Fourier plane with correctly scaled frequency axes."""
dx = (params.x[1] - params.x[0]).item()
freq_max = 1 / (2 * dx) # Nyquist frequency
fig, ax = plt.subplots(figsize=(6, 6))
im = ax.imshow(
torch.log1p(wf_fourier.intensity).cpu(),
cmap='hot',
extent=[-freq_max*1e-3, freq_max*1e-3, -freq_max*1e-3, freq_max*1e-3],
)
ax.set_xlabel('$f_x$, mm$^{-1}$')
ax.set_ylabel('$f_y$, mm$^{-1}$')
ax.set_title(title)
plt.colorbar(im, ax=ax, label='log(I + 1)')
return figStep 8: Band-pass filter
Selecting a single annulus of frequencies isolates structures of one particular scale:
inner_radius = 0.09*ureg.mm
outer_radius = 0.16*ureg.mm
R_sq = X**2 + Y**2
bandpass_filter = ((R_sq > inner_radius**2) & (R_sq < outer_radius**2)).float()
system_bandpass = FourFSystem(params, f, filter_mask=bandpass_filter)
wf_bandpass = system_bandpass(wf_input)Full code
import torch
import torch.nn as nn
from svetlanna import SimulationParameters, Wavefront
from svetlanna.elements import ThinLens, FreeSpace, Aperture
from svetlanna.units import ureg
params = SimulationParameters.from_ranges(
x_range=(-5*ureg.mm, 5*ureg.mm), x_points=1024,
y_range=(-5*ureg.mm, 5*ureg.mm), y_points=1024,
wavelength=632.8*ureg.nm,
)
f = 100*ureg.mm
X, Y = params.meshgrid(x_axis="x", y_axis="y")
period = 0.5*ureg.mm
grating = (torch.cos(2 * torch.pi * X / period) > 0).float()
circular_mask = (X**2 + Y**2 < (2*ureg.mm)**2).float()
wf_input = Wavefront((grating * circular_mask).to(torch.complex64))
class FourFSystem(nn.Module):
def __init__(self, params, focal_length, filter_mask=None):
super().__init__()
self.prop = FreeSpace(params, distance=focal_length, method="zpASM")
self.lens1 = ThinLens(params, focal_length=focal_length)
self.lens2 = ThinLens(params, focal_length=focal_length)
self.filter = (
Aperture(params, mask=filter_mask) if filter_mask is not None else None
)
def forward(self, wf):
wf = self.prop(self.lens1(self.prop(wf)))
if self.filter is not None:
wf = self.filter(wf)
return self.prop(self.lens2(self.prop(wf)))
lp_filter = (X**2 + Y**2 < (0.2*ureg.mm)**2).float()
wf_out = FourFSystem(params, f, lp_filter)(wf_input)
print(f"Input intensity: {wf_input.max_intensity:.4f}")
print(f"Output intensity: {wf_out.max_intensity:.4f}")Conclusions
- A 4f system performs an optical Fourier transform.
- A mask in the Fourier plane reshapes the spectrum of the object.
- A low-pass filter blurs the image; a high-pass filter enhances edges.
- A band-pass filter isolates structures of a given scale.
What next?
- Phase grating on an SLM — a programmable Fourier-plane mask
- Phase retrieval — iterative algorithms
- Optical systems — assembling setups