Using Hilbert transform to get signal envelope
A walk through on obtaining signal envelope using analytic signal approach.
- Amplitude modulated tone
- Amplitude modulated harmonics
- Amplitude modulated white noise
- Speech signal
- That's it!
First we import some packages.
import numpy as np
import scipy.signal as signal
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()
sns.set_style("white")
sns.set_style("ticks")
Next, we define functions to create envelope and carrier signals.
def create_envelope(num_samples,fL_hz=100,N=1000,beta=3):
# we will create the envelope by low pass filtering white noise
mean = 0
std = 1
taps = signal.firwin(N, fL_hz/nyq_rate, window=('kaiser', beta))
x = np.random.normal(mean, std, size=num_samples)
x = signal.filtfilt(taps, 1,x)
x = (x-np.min(x))
x = x/np.max(x)
return x
def create_carrier_harmonic(fs=8e3,fc=200,num_samples=1000,ncomps=1):
# this will create a harmonic carrier with ncomps harmonics
x = []
for i in range(ncomps):
x.append(np.sin(2*np.pi*(i+1)*fc*np.arange(0,num_samples,1)/fs))
x = sum(x)
x = x/np.max(np.abs(x))
return x
def create_carrier_noise(mean=0,std=1,num_samples=1000):
# this will create a white noise carrier
x = np.random.normal(mean, std, size=num_samples)
x = x/np.abs(x)
return x
# init filter and signal params
fs = 8e3
dur = 5 # signal duration
num_samples = int(dur*fs)
t = np.arange(0,num_samples,1)/fs
nyq_rate = fs / 2.0
width = 5.0/nyq_rate # 5 Hz filter transition width.
ripple_db = 60.0 # stop band attenuation
N, beta = signal.kaiserord(ripple_db, width)
# create envelope
fL_hz = 50
envelope = create_envelope(num_samples=num_samples,fL_hz=fL_hz,N=N,beta=beta)
# create carrier
carrier = create_carrier_harmonic(fs=fs,fc=100,num_samples=num_samples,ncomps=1)
# create am-fm signal
x = np.multiply(envelope,carrier)
# estimate analytic signal
ax = signal.hilbert(x)
envelope_hat = np.abs(ax)
# plt.plot(x)
fig = plt.subplots(figsize=(8,4))
ax = plt.subplot(1,1,1)
ax.plot(t,x,color='black',label='SIGNAL')
ax.plot(t,envelope,color='blue',label='ENVP. TRUE')
ax.plot(t,envelope_hat,color='red',label='ENVP. EST.')
ax.set_xlabel('TIME [in s]')
ax.set_ylabel('A.U.')
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
plt.xlim(0,0.5)
plt.ylim(-1,1.75)
plt.show()
# init filter and signal params
fs = 8e3
dur = 5 # signal duration
num_samples = int(dur*fs)
t = np.arange(0,num_samples,1)/fs
nyq_rate = fs / 2.0
width = 5.0/nyq_rate # 5 Hz filter transition width.
ripple_db = 60.0 # stop band attenuation
N, beta = signal.kaiserord(ripple_db, width)
# create envelope
fL_hz = 50
envelope = create_envelope(num_samples=num_samples,fL_hz=fL_hz,N=N,beta=beta)
# create carrier
carrier = create_carrier_harmonic(fs=fs,fc=100,num_samples=num_samples,ncomps=2)
# create am-fm signal
x = np.multiply(envelope,carrier)
# estimate analytic signal
ax = signal.hilbert(x)
envelope_hat = np.abs(ax)
# plt.plot(x)
fig = plt.subplots(figsize=(8,4))
ax = plt.subplot(1,1,1)
ax.plot(t,x,color='black',label='SIGNAL')
ax.plot(t,envelope,color='blue',label='ENVP. TRUE')
ax.plot(t,envelope_hat,color='red',label='ENVP. EST.')
ax.set_xlabel('TIME [in s]')
ax.set_ylabel('A.U.')
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
plt.xlim(0,0.5)
plt.ylim(-.5,1)
plt.show()
The envelope estimated is a poor fit to the true envelope. Can we improve this? One option is to apply a lowpass filter to the estimated envelope. Lets see how does it do.
# init filter and signal params
fs = 8e3
dur = 5 # signal duration
num_samples = int(dur*fs)
t = np.arange(0,num_samples,1)/fs
nyq_rate = fs / 2.0
width = 5.0/nyq_rate # 5 Hz filter transition width.
ripple_db = 60.0 # stop band attenuation
N, beta = signal.kaiserord(ripple_db, width)
# create envelope
fL_hz = 50
envelope = create_envelope(num_samples=num_samples,fL_hz=fL_hz,N=N,beta=beta)
# create carrier
carrier = create_carrier_harmonic(fs=fs,fc=100,num_samples=num_samples,ncomps=2)
# create am-fm signal
x = np.multiply(envelope,carrier)
# estimate analytic signal
ax = signal.hilbert(x)
envelope_hat = np.abs(ax)
taps = signal.firwin(N, fL_hz/nyq_rate, window=('kaiser', beta))
envelope_hat_filt = signal.filtfilt(taps, 1,envelope_hat)
fig = plt.subplots(figsize=(16,4))
ax = plt.subplot(1,2,1)
ax.plot(t,x,color='black',label='SIGNAL')
ax.plot(t,envelope,color='blue',label='ENVP. TRUE')
ax.plot(t,envelope_hat,color='red',label='ENVP. EST.')
ax.set_xlabel('TIME [in s]')
ax.set_ylabel('A.U.')
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
plt.xlim(0,0.5)
plt.ylim(-.5,1)
ax = plt.subplot(1,2,2)
ax.plot(t,x,color='black',label='SIGNAL')
ax.plot(t,envelope,color='blue',label='ENVP. TRUE')
ax.plot(t,envelope_hat_filt,color='red',label='ENVP. EST. FILT.')
ax.set_xlabel('TIME [in s]')
ax.set_ylabel('A.U.')
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
plt.xlim(0,0.5)
plt.ylim(-.5,1)
plt.show()
# init filter and signal params
fs = 8e3
dur = 5 # signal duration
num_samples = int(dur*fs)
t = np.arange(0,num_samples,1)/fs
nyq_rate = fs / 2.0
width = 5.0/nyq_rate # 5 Hz filter transition width.
ripple_db = 60.0 # stop band attenuation
N, beta = signal.kaiserord(ripple_db, width)
# create envelope
fL_hz = 50
envelope = create_envelope(num_samples=num_samples,fL_hz=fL_hz,N=N,beta=beta)
# create carrier
carrier = create_carrier_noise(mean=0,std=1,num_samples=num_samples)
# create am-fm signal
x = np.multiply(envelope,carrier)
# estimate analytic signal
ax = signal.hilbert(x)
envelope_hat = np.abs(ax)
taps = signal.firwin(N, fL_hz/nyq_rate, window=('kaiser', beta))
envelope_hat_filt = signal.filtfilt(taps, 1,envelope_hat)
fig = plt.subplots(figsize=(16,4))
ax = plt.subplot(1,2,1)
ax.plot(t,x,color='black',label='SIGNAL')
ax.plot(t,envelope,color='blue',label='ENVP. TRUE')
ax.plot(t,envelope_hat,color='red',label='ENVP. EST.')
ax.set_xlabel('TIME [in s]')
ax.set_ylabel('A.U.')
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
plt.xlim(0,0.5)
plt.ylim(-2,2)
ax = plt.subplot(1,2,2)
ax.plot(t,x,color='black',label='SIGNAL')
ax.plot(t,envelope,color='blue',label='ENVP. TRUE')
ax.plot(t,envelope_hat_filt,color='red',label='ENVP. EST. FILT.')
ax.set_xlabel('TIME [in s]')
ax.set_ylabel('A.U.')
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
plt.xlim(0,0.5)
plt.ylim(-2,2)
plt.show()
from scipy.io import wavfile # package to read WAV file
# read WAV file and plot data
[fs, x] = wavfile.read('./my_sounds/count.wav')
x = x/np.max(np.abs(x))
t = np.arange(0,len(x))/fs
# get analytic signal
ax = signal.hilbert(x)
envelope_hat = np.abs(ax)
taps = signal.firwin(N, fL_hz/nyq_rate, window=('kaiser', beta))
envelope_hat_filt = signal.filtfilt(taps, 1,envelope_hat)
fig = plt.subplots(figsize=(16,4))
ax = plt.subplot(1,2,1)
ax.plot(t,x,color='black',label='SIGNAL')
ax.plot(t,envelope_hat,color='red',label='ENVP. EST.')
ax.set_xlabel('TIME [in s]')
ax.set_ylabel('A.U.')
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
# plt.xlim(0,0.5)
plt.ylim(-1,1.5)
ax = plt.subplot(1,2,2)
ax.plot(t,x,color='black',label='SIGNAL')
ax.plot(t,envelope_hat_filt,color='red',label='ENVP. EST. FILT.')
ax.set_xlabel('TIME [in s]')
ax.set_ylabel('A.U.')
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
# plt.xlim(0,0.5)
plt.ylim(-1,1.5)
plt.show()
Before we sum-up complete lets see how good is our filter. We will apply it on white noise and see the resulting spectrum before and after application.
# plot signal and spectrum
x = np.random.normal(0,1,size=num_samples)
y = signal.filtfilt(taps, 1,x)
fig = plt.subplots(figsize=(16,4))
ax = plt.subplot(1,2,1)
ax.plot(np.arange(0,num_samples//2+1,1)/num_samples*fs,10*np.log(abs(np.fft.rfft(x))),label='SIGNAL')
ax.plot(np.arange(0,num_samples//2+1,1)/num_samples*fs,10*np.log(abs(np.fft.rfft(y))),label='SIGNAL FILTERED')
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.set_xlabel('FREQUENCY [in Hz]',fontsize=14)
ax.set_ylabel('SPECTRUM [in dB]',fontsize=14)
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.ylim(10,80)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
ax = plt.subplot(1,2,2)
ax.plot(taps)
ax.grid(True)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.set_xlabel('taps',fontsize=14)
ax.set_ylabel('A.U.',fontsize=14)
ax.legend(loc='upper right',frameon=False,fontsize=12)
plt.xticks(fontsize=13)
plt.yticks(fontsize=13)
plt.show()
From the plot it is clear that FIR filter does a good job in removing the spectrum beyond 50 Hz. Lets visualize the filter response and spectrum.
That's it!
To sum it up, assuming you went through the above, we now understand that
- Hilbert transform can be used to estimate signal envelope
- The estimation is very accurate for tone signals. In general, it is accurate for narrowband carrier signals.
- The estimation performance degrades for wideband carriers, like sum of tones or white noise (broadband signal).
- The estimation performance improves on applying a lowpass filter to the envelope estimate.
- Such lowpass filtering of the envelope estimate can be also applied to speech signals.
The above observations can be reasoned from a theoretical angle. I will try to do it another post.