Highpass Filter Design: Theory and Python Implementation

Learn highpass filter theory (cutoff frequency, stopband) through Z-transform and frequency response, with IIR/FIR design using scipy.signal and Python visualization.

What Is a Highpass Filter?

A highpass filter (HPF) passes frequency components above a specified cutoff frequency and attenuates components below it.

Applications include DC removal from audio signals, drift suppression in vibration sensors, and edge enhancement in image processing.

Key Parameters

ParameterDescription
\(f_c\)Cutoff frequency (-3 dB point)
\(\omega_c\)Cutoff angular frequency (\(\omega_c = 2\pi f_c\))
\(N\)Filter order (higher = steeper roll-off)
PassbandFrequency range \(f > f_c\)
StopbandFrequency range \(f < f_c\)

A higher order \(N\) yields a steeper roll-off, but also increases phase delay and the risk of numerical instability.

Frequency Response Derivation

Lowpass-to-Highpass Transformation

A highpass filter can be designed from a lowpass prototype (LPF) via a frequency transformation. In the analog domain, replace the complex variable \(s\) as:

\[ s \rightarrow \frac{\omega_c^2}{s} \tag{1}\]

Applying transformation \((1)\) to the \(N\)th-order Butterworth LPF:

\[H_{LP}(s) = \frac{1}{\prod_{k=1}^{N}(s - s_k)} \tag{2}\]

yields a Butterworth highpass filter of the same order. For the 1st-order case:

\[H_{HP}(s) = \frac{s}{s + \omega_c} \tag{3}\]

Equation \((3)\) has a “differentiator + 1st-order lowpass” structure: output approaches zero as \(s \to 0\) (DC) and approaches 1 as \(s \to \infty\) (high frequency).

Discrete-Time Highpass Filter (Bilinear Transform)

Equation \((3)\) is converted to the digital domain via the bilinear transform:

\[s = \frac{2}{T} \cdot \frac{1 - z^{-1}}{1 + z^{-1}} \tag{4}\]

Substituting \((4)\) into \((3)\) yields the 1st-order digital highpass transfer function:

\[H(z) = \frac{1 - z^{-1}}{1 + \alpha z^{-1}} \cdot \frac{1}{1 + \frac{1}{\alpha}} \tag{5}\]

where \(\alpha = 1 - 2f_c / f_s\) is the pre-warping coefficient. In practice, scipy.signal handles this computation automatically.

Python Implementation

IIR Highpass Filter with scipy.signal

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

# --- Parameters ---
fs = 1000.0      # Sampling frequency [Hz]
fc = 100.0       # Cutoff frequency [Hz]
order = 4        # Filter order

# --- Design Butterworth highpass filter ---
nyq = fs / 2.0   # Nyquist frequency
wn = fc / nyq    # Normalized cutoff frequency

b, a = signal.butter(order, wn, btype='high')

# --- Compute frequency response ---
w, h = signal.freqz(b, a, worN=8000, fs=fs)

# --- Plot ---
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

# Gain (dB)
ax1.semilogx(w, 20 * np.log10(np.abs(h) + 1e-10))
ax1.axvline(fc, color='r', linestyle='--', label=f'$f_c$ = {fc} Hz')
ax1.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax1.set_xlabel('Frequency [Hz]')
ax1.set_ylabel('Gain [dB]')
ax1.set_title(f'Butterworth Highpass Filter (order={order})')
ax1.legend()
ax1.grid(True, which='both', alpha=0.3)
ax1.set_xlim([1, nyq])
ax1.set_ylim([-80, 5])

# Phase
ax2.semilogx(w, np.angle(h, deg=True))
ax2.axvline(fc, color='r', linestyle='--')
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.set_title('Phase Response')
ax2.grid(True, which='both', alpha=0.3)
ax2.set_xlim([1, nyq])

plt.tight_layout()
plt.savefig('highpass_response.png', dpi=150, bbox_inches='tight')
plt.show()

FIR Highpass Filter Design

IIR filters have nonlinear phase, but FIR filters can achieve linear phase. This is important in audio and medical signal processing where phase distortion is unacceptable.

from scipy.signal import firwin, freqz

# --- FIR highpass filter ---
numtaps = 101    # Number of taps (odd number recommended)
fir_hpf = firwin(
    numtaps,
    fc,
    pass_zero=False,  # Highpass specification
    fs=fs,
    window='hamming'
)

w_fir, h_fir = freqz(fir_hpf, worN=8000, fs=fs)

# IIR vs FIR comparison
plt.figure(figsize=(10, 5))
plt.semilogx(w, 20 * np.log10(np.abs(h) + 1e-10),
             label='IIR Butterworth (order=4)', linewidth=2)
plt.semilogx(w_fir, 20 * np.log10(np.abs(h_fir) + 1e-10),
             label=f'FIR Hamming (taps={numtaps})', linewidth=2, linestyle='--')
plt.axvline(fc, color='r', linestyle=':', alpha=0.7, label=f'$f_c$ = {fc} Hz')
plt.axhline(-3, color='gray', linestyle=':', alpha=0.7)
plt.xlabel('Frequency [Hz]')
plt.ylabel('Gain [dB]')
plt.title('Highpass Filter Comparison: IIR vs FIR')
plt.legend()
plt.grid(True, which='both', alpha=0.3)
plt.xlim([1, nyq])
plt.ylim([-80, 5])
plt.tight_layout()
plt.show()

Applying the Filter to a Signal

# --- Generate test signal ---
t = np.linspace(0, 1.0, int(fs), endpoint=False)

# Target signal: 200 Hz sine wave (high-frequency component)
signal_pure = np.sin(2 * np.pi * 200 * t)

# Noise: 5 Hz low-frequency drift (large amplitude) + white noise
noise = (
    2.0 * np.sin(2 * np.pi * 5 * t) +   # Low-frequency drift
    0.3 * np.random.randn(len(t))         # White noise
)

x_noisy = signal_pure + noise

# --- Apply filter ---
# lfilter: causal filter (real-time, has phase delay)
y_lfilter = signal.lfilter(b, a, x_noisy)

# filtfilt: zero-phase filter (offline, no phase delay)
y_filtfilt = signal.filtfilt(b, a, x_noisy)

# --- Comparison plot ---
fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)

axes[0].plot(t[:300], x_noisy[:300], alpha=0.8, label='Noisy signal (with low-freq drift)')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Input Signal (with low-frequency drift)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(t[:300], y_lfilter[:300], color='orange', label='lfilter (causal)')
axes[1].plot(t[:300], signal_pure[:300], color='gray', linestyle='--', alpha=0.6, label='True signal')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('After lfilter (with phase delay)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

axes[2].plot(t[:300], y_filtfilt[:300], color='green', label='filtfilt (zero-phase)')
axes[2].plot(t[:300], signal_pure[:300], color='gray', linestyle='--', alpha=0.6, label='True signal')
axes[2].set_ylabel('Amplitude')
axes[2].set_xlabel('Time [s]')
axes[2].set_title('After filtfilt (zero-phase)')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('highpass_filtering_result.png', dpi=150, bbox_inches='tight')
plt.show()

IIR vs FIR Design Comparison

PropertyIIR (Butterworth, etc.)FIR
Phase responseNonlinear (phase distortion)Linear phase achievable
Number of coefficientsSmall (\(2N+1\) approx.)Large (tens to hundreds)
Computational costLowHigh (depends on tap count)
StabilityRisk with high ordersAlways stable
Stopband roll-offSteep at low orderImproves with tap count
Group delayNon-constantConstant (\((N-1)/2\) samples)
Typical useReal-time processingAudio, medical signals

Design guidelines:

  • Low computational cost in real-time → IIR (Butterworth / Chebyshev)
  • Linear phase required (audio, medical) → FIR (Hamming / Kaiser window)
  • Offline processing, zero phase delay → filtfilt (works with both IIR and FIR)

Effect of Filter Order

fig, ax = plt.subplots(figsize=(10, 6))

for ord_n in [1, 2, 4, 8]:
    b_n, a_n = signal.butter(ord_n, wn, btype='high')
    w_n, h_n = signal.freqz(b_n, a_n, worN=8000, fs=fs)
    ax.semilogx(w_n, 20 * np.log10(np.abs(h_n) + 1e-10), label=f'order={ord_n}')

ax.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax.axvline(fc, color='r', linestyle='--', alpha=0.5, label=f'$f_c$ = {fc} Hz')
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Gain [dB]')
ax.set_title('Effect of Filter Order on Highpass Response')
ax.legend()
ax.grid(True, which='both', alpha=0.3)
ax.set_xlim([1, nyq])
ax.set_ylim([-80, 5])
plt.tight_layout()
plt.show()

Order 1 gives a gradual roll-off, while order 8 gives a steep cutoff. However, IIR filters become numerically unstable at very high orders — order ≤ 8 is a practical guideline.

Practical Applications

ApplicationTypical \(f_c\)Notes
DC removal (microphone)20–80 HzPrevents signal saturation from DC offset
Accelerometer drift removal0.1–1 HzRemoves gravity component (static bias)
ECG baseline wander removal0.5–1 HzRemoves respiratory low-frequency variation
Image edge enhancement2D HPF sharpens contours
Vibration analysis noise removal1–10 HzRemoves DC bias and mains hum from machinery

References