Introduction
When analyzing audio, vibration, or biomedical signals, it is often necessary to know the instantaneous amplitude or instantaneous frequency at each point in time. However, the conventional Fourier transform only reveals the overall frequency content of a signal, losing the information about when each frequency component is present.
The Hilbert Transform is a powerful tool that solves this problem. By using the Hilbert transform to construct the Analytic Signal, we can directly compute the instantaneous amplitude, instantaneous phase, and instantaneous frequency of any real signal.
This article covers the mathematical definition of the Hilbert transform through to practical Python implementations using scipy.signal.hilbert.
Definition of the Hilbert Transform
Continuous-Time Hilbert Transform
The Hilbert transform \(\hat{x}(t)\) of a real signal \(x(t)\) is defined as:
\[\hat{x}(t) = \mathcal{H}\{x(t)\} = \frac{1}{\pi} \text{P.V.} \int_{-\infty}^{\infty} \frac{x(\tau)}{t - \tau} d\tau \tag{1}\]where P.V. denotes the Cauchy principal value integral, which symmetrically avoids the singularity at \(t = \tau\).
Equation \((1)\) can be viewed as the convolution of \(x(t)\) with \(h(t) = \frac{1}{\pi t}\):
\[\hat{x}(t) = x(t) * \frac{1}{\pi t} \tag{2}\]Frequency-Domain Interpretation
By the convolution theorem, the Hilbert transform takes a particularly elegant form in the frequency domain:
\[\hat{X}(f) = H(f) \cdot X(f) \tag{3}\]where the filter \(H(f)\) is:
\[H(f) = \begin{cases} -j & (f > 0) \\ 0 & (f = 0) \\ +j & (f < 0) \end{cases} \tag{4}\]In other words, the Hilbert transform is an all-pass filter that shifts positive frequency components by \(-90°\) and negative frequency components by \(+90°\). The amplitude spectrum is unchanged; only the phase is rotated.
This property leads to the well-known result: applying the Hilbert transform to a cosine gives a sine:
\[\mathcal{H}\{\cos(2\pi f_0 t)\} = \sin(2\pi f_0 t) \tag{5}\]Analytic Signal
Definition
From a real signal \(x(t)\), we construct the analytic signal \(z(t)\) as:
\[z(t) = x(t) + j\hat{x}(t) \tag{6}\]The analytic signal is complex-valued: its real part is the original signal \(x(t)\) and its imaginary part is the Hilbert transform \(\hat{x}(t)\).
In the frequency domain:
\[Z(f) = \begin{cases} 2X(f) & (f > 0) \\ X(0) & (f = 0) \\ 0 & (f < 0) \end{cases} \tag{7}\]That is, the analytic signal suppresses the negative-frequency components and doubles the positive-frequency components. This one-sided spectrum representation is what makes instantaneous feature extraction possible.
Instantaneous Amplitude, Phase, and Frequency
Writing the analytic signal in polar form:
\[z(t) = A(t) \cdot e^{j\phi(t)} \tag{8}\]we can directly extract the following instantaneous characteristics.
Instantaneous amplitude (envelope):
\[A(t) = |z(t)| = \sqrt{x(t)^2 + \hat{x}(t)^2} \tag{9}\]Instantaneous phase:
\[\phi(t) = \angle z(t) = \arctan\!\left(\frac{\hat{x}(t)}{x(t)}\right) \tag{10}\]Instantaneous frequency:
\[f_i(t) = \frac{1}{2\pi} \frac{d\phi(t)}{dt} \tag{11}\]The instantaneous frequency is the time derivative of the phase and tracks how the signal’s frequency evolves over time.
Python Implementation
Using scipy.signal.hilbert
SciPy provides scipy.signal.hilbert, which computes the analytic signal efficiently:
from scipy.signal import hilbert
import numpy as np
# Input signal
x = np.array([...]) # real-valued signal
# Compute the analytic signal (uses discrete Hilbert transform internally)
z = hilbert(x)
# Instantaneous amplitude (envelope)
amplitude = np.abs(z)
# Instantaneous phase (radians)
phase = np.angle(z)
# Instantaneous frequency (Hz)
fs = 1000 # sampling frequency
inst_freq = np.diff(np.unwrap(phase)) / (2 * np.pi) * fs
scipy.signal.hilbert is implemented using the FFT, with complexity \(O(N \log N)\). Internally it applies the frequency-domain filter in equation \((4)\) and returns the analytic signal \(z(t)\).
Note: The return value of
scipy.signal.hilbertis the full analytic signal \(z(t)\) (real part = original signal, imaginary part = Hilbert transform). To obtain only \(\hat{x}(t)\), usenp.imag(hilbert(x)).
Basic Verification
We verify the \(\mathcal{H}\{\cos\} = \sin\) identity numerically:
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import hilbert
# --- Signal generation ---
fs = 1000 # sampling frequency [Hz]
f0 = 10 # signal frequency [Hz]
t = np.arange(0, 1, 1/fs)
cos_signal = np.cos(2 * np.pi * f0 * t)
sin_signal = np.sin(2 * np.pi * f0 * t)
# --- Hilbert transform ---
cos_hilbert = np.imag(hilbert(cos_signal)) # expected: sin
sin_hilbert = np.imag(hilbert(sin_signal)) # expected: -cos
# --- Plot ---
fig, axes = plt.subplots(2, 1, figsize=(10, 6))
axes[0].plot(t[:200], cos_signal[:200], label='cos(t)')
axes[0].plot(t[:200], cos_hilbert[:200], '--', label='H{cos(t)} ≈ sin(t)')
axes[0].set_title('Hilbert Transform of cos(t)')
axes[0].set_xlabel('Time [s]')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].plot(t[:200], sin_signal[:200], label='sin(t)')
axes[1].plot(t[:200], sin_hilbert[:200], '--', label='H{sin(t)} ≈ -cos(t)')
axes[1].set_title('Hilbert Transform of sin(t)')
axes[1].set_xlabel('Time [s]')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Verify errors
print(f"Max error H{{cos}} vs sin: {np.max(np.abs(cos_hilbert - sin_signal)):.2e}")
print(f"Max error H{{sin}} vs -cos: {np.max(np.abs(sin_hilbert + cos_signal)):.2e}")
# Example output:
# Max error H{cos} vs sin: 1.33e-15
# Max error H{sin} vs -cos: 1.33e-15
The errors are at the level of floating-point precision (\(10^{-15}\)), confirming that \(\mathcal{H}\{\cos\} = \sin\) and \(\mathcal{H}\{\sin\} = -\cos\) hold exactly.
Application 1: Envelope Detection
Extracting the Envelope of an AM Signal
In amplitude modulation (AM), a carrier wave is modulated by an information signal. Hilbert-based envelope detection allows us to recover the original message signal.
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import hilbert
# --- Generate AM signal ---
fs = 10000 # sampling frequency [Hz]
t = np.arange(0, 1, 1/fs)
# Message signal (5 Hz)
f_msg = 5
message = 0.5 * np.sin(2 * np.pi * f_msg * t)
# Carrier (500 Hz)
f_carrier = 500
carrier = np.cos(2 * np.pi * f_carrier * t)
# AM modulation: x(t) = (1 + m(t)) * carrier
am_signal = (1 + message) * carrier
# --- Envelope detection using Hilbert transform ---
analytic = hilbert(am_signal)
envelope = np.abs(analytic) # instantaneous amplitude = envelope
# --- Plot ---
fig, axes = plt.subplots(3, 1, figsize=(10, 9))
axes[0].plot(t, am_signal, alpha=0.5, label='AM Signal')
axes[0].plot(t, envelope, 'r-', linewidth=2, label='Envelope (Hilbert)')
axes[0].plot(t, -envelope, 'r-', linewidth=2)
axes[0].set_xlim(0, 0.4)
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('AM Signal and Envelope')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].plot(t, 1 + message, 'k--', label='True Envelope')
axes[1].plot(t, envelope, 'r-', alpha=0.8, label='Detected Envelope')
axes[1].set_xlim(0, 0.4)
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('True Envelope vs Detected Envelope')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
demodulated = envelope - 1 # remove DC component
axes[2].plot(t, message, 'k--', label='Original Message')
axes[2].plot(t, demodulated, 'r-', alpha=0.8, label='Demodulated (Hilbert)')
axes[2].set_xlim(0, 0.4)
axes[2].set_xlabel('Time [s]')
axes[2].set_ylabel('Amplitude')
axes[2].set_title('Demodulation Result')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
rmse = np.sqrt(np.mean((message - demodulated)**2))
print(f"Demodulation RMSE: {rmse:.6f}")
The detected envelope closely follows the shape of the original 5 Hz message signal, demonstrating accurate AM demodulation.
Application 2: Instantaneous Frequency Estimation
Chirp Signal Analysis
We apply the Hilbert transform to a chirp signal whose frequency varies linearly over time.
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import hilbert, chirp
# --- Generate chirp signal (50 Hz → 200 Hz over 1 second) ---
fs = 4000
t = np.arange(0, 1, 1/fs)
f_start, f_end = 50, 200
signal = chirp(t, f0=f_start, f1=f_end, t1=1, method='linear')
# --- Instantaneous frequency via Hilbert transform ---
analytic = hilbert(signal)
phase = np.unwrap(np.angle(analytic))
inst_freq = np.diff(phase) / (2 * np.pi) * fs
# Theoretical frequency (linear ramp)
f_theory = f_start + (f_end - f_start) * t[:-1]
# --- Plot ---
fig, axes = plt.subplots(3, 1, figsize=(10, 9))
axes[0].plot(t, signal, alpha=0.7)
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title(f'Chirp Signal ({f_start} Hz → {f_end} Hz)')
axes[0].grid(True, alpha=0.3)
axes[1].plot(t, np.abs(analytic), 'r-')
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('Instantaneous Amplitude (Envelope)')
axes[1].set_ylim(0, 1.5)
axes[1].grid(True, alpha=0.3)
axes[2].plot(t[:-1], inst_freq, label='Instantaneous Frequency (Hilbert)')
axes[2].plot(t[:-1], f_theory, 'k--', label='Theoretical Frequency')
axes[2].set_xlabel('Time [s]')
axes[2].set_ylabel('Frequency [Hz]')
axes[2].set_title('Instantaneous Frequency vs Theoretical')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
mae = np.mean(np.abs(inst_freq - f_theory))
print(f"Mean absolute error in instantaneous frequency: {mae:.4f} Hz")
The estimated instantaneous frequency closely matches the theoretical linear ramp, demonstrating the accuracy of Hilbert-based frequency tracking.
Application 3: Envelope Spectrum Analysis for Fault Diagnosis
A practical application of the Hilbert transform is envelope spectrum analysis for rotating machinery fault diagnosis. Bearing defects generate repetitive impulse-like vibrations, and the envelope spectrum of the filtered vibration signal reveals characteristic frequency peaks related to the fault.
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import hilbert, butter, sosfiltfilt
# --- Simulate bearing fault vibration signal ---
np.random.seed(42)
fs = 20000
t = np.arange(0, 1, 1/fs)
# High-frequency resonance (bearing natural frequency: 5 kHz)
f_resonance = 5000
resonance_carrier = np.cos(2 * np.pi * f_resonance * t)
# Fault frequency: 100 Hz repetitive impulses
f_fault = 100
impulse_train = np.zeros_like(t)
impulse_indices = (np.arange(0, 1, 1/f_fault) * fs).astype(int)
impulse_indices = impulse_indices[impulse_indices < len(t)]
impulse_train[impulse_indices] = 1.0
alpha = 500
decay = np.exp(-alpha * np.mod(t, 1/f_fault))
fault_component = np.convolve(impulse_train, decay[:len(t)//f_fault], mode='same')
fault_signal = fault_component * 0.3 * resonance_carrier
observed = fault_signal + 0.5 * np.random.randn(len(t))
# --- Step 1: Bandpass filter around resonance frequency ---
sos = butter(4, [4000, 6000], btype='bandpass', fs=fs, output='sos')
filtered = sosfiltfilt(sos, observed)
# --- Step 2: Envelope via Hilbert transform ---
analytic = hilbert(filtered)
envelope = np.abs(analytic)
# --- Step 3: FFT of the envelope ---
N = len(envelope)
envelope_spectrum = np.abs(np.fft.rfft(envelope - np.mean(envelope))) / N
freqs = np.fft.rfftfreq(N, 1/fs)
# --- Plot ---
fig, axes = plt.subplots(3, 1, figsize=(10, 9))
axes[0].plot(t[:3000], observed[:3000], alpha=0.7)
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Observed Vibration Signal')
axes[0].grid(True, alpha=0.3)
axes[1].plot(t[:3000], filtered[:3000], alpha=0.5, label='Bandpass Filtered')
axes[1].plot(t[:3000], envelope[:3000], 'r-', linewidth=1.5, label='Envelope')
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('Bandpass Filtered Signal and Envelope')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
mask = freqs <= 500
axes[2].plot(freqs[mask], envelope_spectrum[mask])
axes[2].axvline(f_fault, color='r', linestyle='--', alpha=0.7,
label=f'Fault Freq = {f_fault} Hz')
for harmonic in range(2, 6):
axes[2].axvline(f_fault * harmonic, color='r', linestyle=':', alpha=0.4)
axes[2].set_xlabel('Frequency [Hz]')
axes[2].set_ylabel('Amplitude')
axes[2].set_title('Envelope Spectrum (Fault Diagnosis)')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Peaks appear in the envelope spectrum at the fault frequency (100 Hz) and its harmonics (200 Hz, 300 Hz, …). This technique is widely used in industrial bearing and gear fault detection.
Practical Considerations
Edge Effects
Because scipy.signal.hilbert is FFT-based, it assumes the input signal is periodic. When the signal is not periodic over the analysis window, discontinuities at the boundaries cause accuracy degradation near the edges — the so-called edge effect.
Mitigation strategies:
- Pad the signal with zeros at both ends so that edge artifacts fall outside the region of interest.
- Acquire extra data before and after the analysis window and discard the buffered portions after processing.
Narrowband Condition
A physically meaningful interpretation of instantaneous frequency requires the signal to be narrowband (a single dominant frequency component at any given time). For wideband or multi-component signals, the instantaneous frequency can become negative or otherwise meaningless.
| Signal Type | Recommended Pre-processing |
|---|---|
| Wideband signal | Bandpass filter to isolate the band of interest |
| Multi-component signal | EMD (Empirical Mode Decomposition) to separate IMFs |
| Non-stationary signal | Consider STFT or wavelet transform |
Summary
- The Hilbert transform is a \(-90°\) phase-shift operator; in the frequency domain it is expressed as \(H(f) = -j\,\text{sgn}(f)\)
- The analytic signal, formed by combining the original signal with its Hilbert transform, has a one-sided spectrum and enables extraction of instantaneous signal characteristics
- The modulus, argument, and time derivative of the argument of the analytic signal yield the instantaneous amplitude, instantaneous phase, and instantaneous frequency, respectively
scipy.signal.hilbertprovides an \(O(N \log N)\) FFT-based implementation suitable for envelope detection, AM demodulation, and fault diagnosis- Edge effects and the narrowband condition must be considered; appropriate bandpass filtering or padding is recommended
Related Articles
- Fast Fourier Transform (FFT): Algorithm and Python Implementation - Provides the FFT foundations that scipy.signal.hilbert relies on internally.
- Window Functions and Power Spectral Density (PSD) - Spectral analysis techniques that complement Hilbert-based analysis.
- Notch Filter Design and Python Implementation - Noise removal prior to envelope analysis is a common workflow.
- Bandpass Filter Design and Python Implementation - Bandpass filtering is often a required pre-processing step before applying the Hilbert transform.
- Wavelet Transform: Theory and Python Implementation - An alternative time-frequency method frequently compared with Hilbert-Huang Transform (HHT).
References
- Gabor, D. (1946). “Theory of communication.” Journal of the Institution of Electrical Engineers, 93(3), 429-457.
- Huang, N. E., et al. (1998). “The empirical mode decomposition and the Hilbert spectrum for nonlinear and non-stationary time series analysis.” Proceedings of the Royal Society of London A, 454, 903-995.
- Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
- SciPy signal.hilbert documentation