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
| Parameter | Description |
|---|---|
| \(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) |
| Passband | Frequency range \(f > f_c\) |
| Stopband | Frequency 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
| Property | IIR (Butterworth, etc.) | FIR |
|---|---|---|
| Phase response | Nonlinear (phase distortion) | Linear phase achievable |
| Number of coefficients | Small (\(2N+1\) approx.) | Large (tens to hundreds) |
| Computational cost | Low | High (depends on tap count) |
| Stability | Risk with high orders | Always stable |
| Stopband roll-off | Steep at low order | Improves with tap count |
| Group delay | Non-constant | Constant (\((N-1)/2\) samples) |
| Typical use | Real-time processing | Audio, 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
| Application | Typical \(f_c\) | Notes |
|---|---|---|
| DC removal (microphone) | 20–80 Hz | Prevents signal saturation from DC offset |
| Accelerometer drift removal | 0.1–1 Hz | Removes gravity component (static bias) |
| ECG baseline wander removal | 0.5–1 Hz | Removes respiratory low-frequency variation |
| Image edge enhancement | — | 2D HPF sharpens contours |
| Vibration analysis noise removal | 1–10 Hz | Removes DC bias and mains hum from machinery |
Related Articles
- Lowpass Filter Design: Moving Average, Butterworth, Chebyshev — The dual of highpass filter design.
- Butterworth Filter Design: Theory and Python Implementation — The Butterworth prototype used in highpass design.
- Bandpass Filter Design: Theory and Python Implementation — Combining highpass and lowpass into a band-pass filter.
- FIR vs IIR Filters: A Comprehensive Comparison — Design philosophy differences between FIR and IIR filters.
- Notch Filter Design and Python Implementation — Band-stop filter that removes specific frequencies.
- Frequency Characteristics of the Exponential Moving Average Filter — EMA as a 1st-order IIR lowpass filter derived via Z-transform.
- FFT: Theory and Python Implementation — FFT fundamentals for verifying filter effects in the frequency domain.
- Adaptive Filters (LMS/RLS): Theory and Python Implementation — Filters that automatically adapt their frequency response.
References
- Proakis, J. G., & Manolakis, D. G. (2006). Digital Signal Processing: Principles, Algorithms, and Applications. Pearson.
- scipy.signal.butter — SciPy documentation
- scipy.signal.filtfilt — SciPy documentation
Related Tools
- DevToolBox - Free Developer Tools — 85+ tools including JSON formatter, regex tester
- CalcBox - Everyday Calculation Tools — 61+ tools including statistics and frequency conversion