Signal Processing Basics

Written by Luke Chang In this lab, we will cover the basics of convolution, sine waves, and fourier transforms. This lab is largely based on exercises from Mike X Cohen's excellent book, Analyzing Neural Data Analysis: Theory and Practice. If you are interested in learning in more detail about the basics of EEG and time-series analyses I highly recommend his accessible introduction. I also encourage you to watch his accompanying freely available lecturelets to learn more about each topic introduced in this notebook.

Time Domain

First we will work on signals in the time domain. This requires measuring a signal at a constant interval over time. The frequency with which we measure a signal is referred to as the sampling frequency. The units of this are typically described in ||(Hz||) - or the number of cycles per second. It is critical that the sampling frequency is consistent over the entire measurement of the time series.

Dot Product

To understand convolution, we first need to familiarize ourselves with the dot product. The dot product is simply the sum of the elements of a vector weighted by the elements of another vector. This method is commonly used in signal processing, and also in statistics as a measure of similarity between two vectors. Finally, there is also a geometric interpretation which is a mapping between vectors (i.e., the product of the magnitudes of the two vectors scaled by the cosine of the angle between them). For a more in depth overview of the dot product and its relation to convolution, you can watch this optional video. ||[dotproduct_{ab}=\sum\limits_{i=1}^n a_i b_i||]Let's create some vectors of random numbers and see how the dot product works. First, the two vectors need to be of the same length.
Dot Product: 567
What happens when we make the two vectors more similar? Use the slider to move b from unrelated to a (0) to identical to a (1). Each bar below is one term ||(a_i b_i||), and the dot product is the sum of all the bars. When the vectors agree, the terms are mostly positive and pile up. When they are unrelated, positive and negative terms cancel out. These vectors are centered on zero, so "unrelated" gives a dot product near zero. Standardize both vectors and divide the dot product by ||(n||), and you get the familiar correlation coefficient.
Dot product: 0.3   (correlation r = 0.01)

Convolution

Convolution in the time domain is an extension of the dot product in which the dot product is computed iteratively over time. One way to think about it is that one signal weights each time point of the other signal and then slides forward over time. Let's call the timeseries variable signal and the other vector the kernel. To gain an intuition of how convolution works, let's play with some data. First, let's create a time series of spikes. Then let's convolve this signal with a boxcar kernel.
Notice how the kernel is only 10 samples long and the boxcar is 6 samples wide, while the signal is 100 samples long with 5 single pulses. Now let's convolve the signal with the kernel by taking the dot product of the kernel with each time point of the signal. This can be illustrated by creating a matrix of the kernel shifted each time point of the signal. Now, let's take the dot product of the signal with this matrix. Matrix multiplication consists of taking the dot product of the signal vector with each row of this expanded kernel matrix.
After convolution, each spike has become the shape of the kernel. np.convolve (dashed) does exactly this matrix multiplication for us. Signal: 100, Kernel: 10, Convolved: 109 samples (the kernel hangs off the end).

Step Through Convolution

Use the slider to step through the convolution one timepoint at a time. Watch the kernel (red) slide across the signal and see the output build up.
What happens if the spikes have different intensities? Now what happens if we switch out the boxcar kernel for a hemodynamic response function (HRF)? Here we will use a double gamma hemodynamic function (HRF) developed by Gary Glover. Use the sliders to explore how the TR and oversampling affect the HRF shape. Oversampling the function will help make it look more smooth. In practice we will want to make sure that the kernel is the correct shape given our sampling resolution. Be sure to set the oversampling to 1. Notice how the function looks more jagged now?
Now let's convolve our event pulses with this HRF kernel. If you are interested in a more detailed overview of convolution in the time domain, I encourage you to watch this video by Mike X Cohen. For more details about convolution and the HRF function, see this overview using python examples.

Oscillations

Ok, now let’s move on to studying time-varying signals that have the shape of oscillating waves. Let’s watch a short video by Mike X Cohen to get some more background on sine waves. Don’t worry too much about the matlab code as we will work through similar Python examples in this notebook.
Oscillations can be described mathematically as: ||(A\sin(2 \pi ft + \theta)||) where ||(f||) is frequency or the speed of the oscillation described in the number of cycles per second (||(Hz||)), Amplitude ||(A||) refers to the height of the waves, which is half the distance of the peak to the trough. Finally, ||(\theta||) describes the phase angle offset, which is in radians. Here we will plot a simple sine wave. Try playing with the different parameters (i.e., amplitude, frequency, & theta) to gain an intuition of how they each impact the shape of the wave. Try the sliders:
Next we will build a more interesting signal by adding together sine waves at five different frequencies, each with its own amplitude and phase. What is the effect of changing the sampling frequency on our ability to measure these oscillations? Try dropping it to be very low (e.g., less than 70 hz.) Notice that signals will alias when the sampling frequency is below the nyquist frequency of a signal. To observe the oscillations, we need to be sampling at least two times for each oscillation cycle. This will result in a jagged view of the data, but we can still theoretically observe the frequency. Practically, higher sampling rates allow us to better observe the underlying signals.

Aliasing at fMRI sampling rates

This matters for fMRI. We sample the brain once per TR (repetition time), so the sampling frequency is ||(f_s = 1/\text{TR}||). A typical TR of 2 s gives ||(f_s||) = 0.5 Hz and a Nyquist frequency of only 0.25 Hz. Breathing (~0.3 Hz) and the heartbeat (~1 Hz) are faster than that, so they can't be measured directly. They alias, folding down into slower oscillations. Use the slider to change the TR. The gray line is the true physiological signal, and the dots are what the scanner records.
TR = 2 s → ||(f_s||) = 0.50 Hz → Nyquist = 0.25 Hz

Time & Frequency Domains

We have seen above how to represent signals in the time domain. However, these signals can also be represented in the frequency domain. Let’s get started by watching a short video by Mike X Cohen to get an overview of how a signal can be represented in both of these different domains.
For the rest of this chapter we'll analyze one fixed version of our five-sine-wave signal: sampled at 500 Hz (well above Nyquist for all five components) for exactly 2 seconds, plus some noise. Every spectrum and filter below uses this same signal. Why exactly 2 s? Each component then completes a whole number of cycles in the recording. If a wave stops partway through a cycle, its energy smears into neighboring frequencies in the spectrum (called spectral leakage), and its peak comes out lower than its true amplitude.

Frequency Domain

In the previous example, we generated a complex signal composed of multiple sine waves oscillating at different frequencies. Typically in data analysis, we only observe the signal and are trying to uncover the generative processes that gave rise to the signal. In this section, we will introduce the frequency domain and how we can identify if there are any frequencies oscillating at a consistent frequency in our signal using the fourier transform. The fourier transform convolves different frequencies of sine waves with our data to identify oscillatory components. One important assumption: stationarity — the generative processes don't vary over time. See this video or a more in depth discussion on stationarity. In practice, this assumption is rarely true. Often it can be useful to use other techniques such as wavelets to look at time x frequency representations. We will not be covering wavelets here, but see this series of videos for more information.

Discrete Time Fourier Transform

We will gain an intution of how the fourier transform works by building our own discrete time fourier transform. Let’s watch this short video about the fourier transform by Mike X Cohen. Don’t worry too much about the details of the discussion on the matlab code as we will be exploring these concepts in python below.
The discrete Fourier transform of variable ||(x||) at frequency ||(f||) can be defined as: ||[X_f = \sum\limits_{k=0}^{n-1} x_k \cdot e^{\frac{-i2\pi fk}{n}}||]where ||(n||) refers to the number of data points in vector ||(x||), and the capital letter ||(X_f||) is the fourier coefficient of time series variable ||(x||) at frequency ||(f||). Essentially, we create a bank of complex sine waves at different frequencies that are linearly spaced. The zero frequency component reflects the mean offset over the entire signal and will simply be zero in our example.

Complex Sine Waves

You may have noticed that we are computing complex sine waves using the np.exp function instead of the np.sin function. ||[e^{i(2\pi ft + \theta)}||]We will not spend too much time on the details, but basically complex sine waves have three components: time, a real part of the sine wave, and the imaginary part of the sine wave, which are basically phase shifted by ||(\frac{\pi}{2}||). ||(1j||) is how we can specify a complex number in python. We can extract the real components using np.real or the imaginary using np.imag. We can visualize complex sine waves in three dimensions. For more information, watch this video. If you need a refresher on complex numbers, you may want to watch this video. In this plot, we show this complex signal in 3 dimensions and also project on two dimensional planes to show that the real and imaginary create a unit circle, and are phase offset by ||(\frac{\pi}{2}||) with respect to time.

Create a filter bank

Ok, now let’s create a bank of n-1 linearly spaced complex sine waves and the plot first 5 waves to see their frequencies. Remember the first basis function is zero frequency (DC) component and reflects the mean offset over the entire signal.
We can visualize a whole bank of sine waves at once using a heatmap representation. Our bank has one row per sample, too many to draw clearly, so the heatmap shows a smaller bank of 64 waves built the same way. Each row is a different sine wave, and columns reflect time. The intensity of the value is like if the sine wave was coming towards and away rather than up and down. Notice how it looks like that the second half of the sine waves appear to be a mirror image of the first half. This is because the first half contain the positive frequencies, while the second half contains the negative frequencies. Negative frequencies capture sine waves that travel in reverse order around the complex plane compared to that travel forward. This becomes more relevant with the hilbert transform, but for the purposes of this tutorial we will be ignoring the negative frequencies.

Estimate Fourier Coefficients

Now let’s take the dot product of each of the sine wave basis set with our signal to get the fourier coefficients. We can scale the coefficients to be more interpretable by dividing by the number of time points and multiplying by 2. Watch this video if you’re interested in a more detailed explanation. Basically, this only needs to be done if you want the amplitude to be in the same units as the original data. In practice, this scaling factor will not change your interpretation of the spectrum.
Recall: freq = [3, 10, 5, 15, 35], amplitude = [5, 15, 10, 5, 7]. Each peak lands on (or, because of the noise, very near) its true amplitude, shown as red dots. The small bumps everywhere else are the noise.Each coefficient belongs to one frequency: coefficient ||(k||) is ||(k||) cycles per recording, so with 2 s of data it sits at ||(k / 2||) Hz. In practice you'd use np.fft.fft, which computes exactly these coefficients (np.allclose → True) much faster: ||(O(n \log n)||) operations instead of ||(O(n^2)||). That's the fast Fourier transform.
Let's learn a few more important details about the DFT:

Inverse Fourier Transform

The fourier transform allows you to represent a time series in the frequency domain. This is a lossless operation, meaning that no information in the original signal is lost by the transform. This means that we can reconstruct the original signal by inverting the operation. Thus, we can create a time series with only the frequency domain information using the inverse fourier transform. Watch this video if you would like a more in depth explanation. ||[x_k = \sum\limits_{k=0}^{n-1} X_f \cdot e^\frac{i2\pi fk}{n}||]Notice that we are computing the dot product between the complex sine wave and the fourier coefficients ||(X||) instead of the time series data ||(x||).
Largest difference from the original: 9.4e-12, i.e. rounding error.

Phase Spectrum

Each Fourier coefficient is a complex number, so it carries two things: its size (the amplitude, plotted above) and its angle (the phase). The phase says where in its cycle each wave is at time zero. Phase is only meaningful at frequencies where there is a real oscillation; elsewhere it is just the phase of the noise. So below we read off the phase at our five frequencies and compare it to the phases we used to build the signal. The FFT measures phase relative to a cosine, and a sine is a cosine shifted by ||(\pi/2||), so we add ||(\pi/2||) before comparing.

Convolution Theorem

Convolution in the time domain is the same as multiplication in the frequency domain. This means that time domain convolution computations can be performed much more efficiently in the frequency domain via simple multiplication. (The opposite is also true: multiplication in the time domain is the same as convolution in the frequency domain.) Watch this video for an overview of the convolution theorem and convolution in the frequency domain. Below, the top row convolves our signal with a short smoothing kernel in time. The bottom row shows the same three things in the frequency domain: the signal's spectrum, times the kernel's spectrum, gives the spectrum of the result.

Filters

A filter changes how much of each frequency survives. It can be described in two equivalent ways:
  • In the frequency domain, by its gain: a number for every frequency, from 1 (keep it) to 0 (remove it).
  • In the time domain, by its kernel, also called its impulse response: the output you get when you feed the filter a single spike.
These are two views of the same object. The convolution theorem says the gain is the Fourier transform of the kernel. So there are also two ways to apply a filter: convolve the signal with the kernel, or multiply the signal's Fourier transform by the gain. Both give the same answer. Filters can be classified as finite impulse response (FIR) or infinite impulse response (IIR). These terms describe how a filter responds to a single input impulse. FIR filters have a response that ends at a discrete point in time, while IIR filters have a response that continues indefinitely. Filters are designed in the frequency domain and have several properties that need to be considered.
  • ripple in the pass-band
  • attenuation in the stop-band
  • steepness of roll-off
  • filter order (i.e., length for FIR filters)
  • time-domain ringing
In general, there is a frequency by time tradeoff. The sharper something is in frequency, the broader it is in time, and vice versa.

Two ways to apply the same filter

Let's apply one low-pass filter both ways. The signal has two components, a slow 5 Hz wave and a fast 35 Hz wave, and the filter should keep everything below 15 Hz. We'll use a simple FIR filter from scipy.signal.firwin, whose kernel is just a list of 101 weights.

Way 1: in the time domain, convolve with the kernel

This is exactly the convolution from the start of this chapter. Slide the kernel along the signal and take a dot product at every time point. The kernel is a smooth bump, so each output sample is a weighted average of its neighbors. Averaging over ~100 ms smooths away the fast 35 Hz wiggles but barely touches the slow 5 Hz wave.

Way 2: in the frequency domain, multiply by the gain

  1. Take the FFT of the signal. Remember that the FFT returns positive and negative frequencies: the second half of the array is a mirror image of the first. The plots below show only the positive half.
  2. Build the filter's gain at every FFT frequency, the negative ones included, so the mirror image stays intact. Here we take the FFT of the kernel, padded to the length of the signal, which takes care of both halves. Because the kernel is symmetric, its gain is real-valued: about 1 below 15 Hz and about 0 above.
  3. Multiply the two, frequency by frequency.
  4. Take the inverse FFT to get back to time. The result should be real; .real drops the leftover rounding error.
Away from the edges, the two results differ by at most 5.3e-15, i.e. only by rounding error.

Which one is used in practice?

Both, and you'll meet each:
  • Frequency domain: you see exactly what the filter does to every frequency, and for long signals the FFT makes it fast. Watch the edges, though: the FFT treats the signal as if it wraps around in a loop, so the end leaks into the beginning.
  • Time domain: works on a stream, one sample at a time. Real-time audio does this. The equalizer later in this chapter filters your music in the time domain, with small IIR filters running sample by sample, while its gray spectrum display is an FFT.
  • scipy.signal.filtfilt, which we use for the Butterworth filters below, is also a time-domain method. Butterworth filters are IIR, so instead of convolving with a fixed kernel, each output sample is computed from recent inputs and recent outputs. filtfilt runs the filter forward and then backward, which cancels the time shift a one-way filter would add.

Two easy mistakes in the frequency domain

  1. Forgetting the negative frequencies. If you set the gain at +35 Hz to 0 but not at −35 Hz, the spectrum is no longer mirror-symmetric, and the inverse FFT comes back complex instead of real.
  2. A brick-wall gain. Jumping straight from 1 to 0 at the cutoff is the sharpest filter possible in frequency, so by the frequency × time trade-off it rings in time. You can see this around sharp edges, like the onsets of a block design.
Mistake 1: zeroing only the positive frequencies above 15 Hz leaves an imaginary part as large as 2.50 after the inverse FFT. Zero the negative ones too (np.abs(freqs) > 15) and it drops to rounding error.

Four common filters

From here on we'll use IIR Butterworth filters, applied in the time domain with filtfilt. There are four basic types, named for what they let through:
  • High-pass keeps frequencies above a cutoff and removes slow ones (like scanner drift).
  • Low-pass keeps frequencies below a cutoff and removes fast ones.
  • Band-pass keeps only a range of frequencies. A Morlet wavelet, for example, is a band-pass filter centered on one frequency.
  • Band-stop removes a range of frequencies and keeps everything else.
Each row below shows one filter: its gain in the frequency domain (left; the red lines mark our signal's five components) and what it does to our signal in the time domain (right).
What does a filter look like in the time domain? Feed it a single spike, and the output is its impulse response. The order sets how sharp the cutoff is. Compare a high-pass filter of order 2 and order 8 below: the sharper gain rings for longer in time. That is the frequency × time trade-off again. Notice that the response starts before the spike. filtfilt runs the filter forward and then backward over the whole recording, so its response is symmetric around the spike. That's what keeps it from shifting the signal in time, and it's possible because we filter a recording after the fact, not a live stream.

Interactive Filter Explorer

Explore all filter types interactively:
Upper cutoff only used for bandpass/bandstop

High-pass filtering fMRI data

The most common filter in fMRI preprocessing is a high-pass filter. It removes slow scanner drift, which otherwise dwarfs the effects we care about. The simulated voxel below has a block design (20 s on, 20 s off, so the task repeats every 40 s, or 0.025 Hz) sampled at TR = 2 s, plus slow drift and noise. The cutoff is usually written as a period, in seconds per cycle, rather than a frequency. Use the slider to choose it. Everything slower than that period is removed (the red band in the spectrum). What happens when the cutoff period is shorter than the 40 s task cycle?
SPM's default cutoff is 128 s (≈ 0.008 Hz). SPM and nilearn implement it as a set of slow cosine regressors in the GLM rather than a Butterworth filter, but the idea is the same. Low-pass filtering is rarely used for fMRI: it removes little noise and adds temporal autocorrelation that the GLM then has to model.

Hearing filters

Filters are easier to understand when you can hear them. Music is a mixture of many frequencies at once: the kick drum and bass live below ~250 Hz, voices and most instruments between ~250 Hz and 4 kHz, and the hiss of cymbals and the "air" of a recording above ~4 kHz. Drop any song (mp3, wav, …) onto the player below. It loops, and every sound passes through a bank of filters before it reaches your speakers. Each handle sets the gain at one frequency, from 1 (pass it through untouched) to 0 (remove it), with low frequencies on the left — exactly like the filter plots above. The dark curve is the filter's actual frequency response, and the gray shading is the spectrum of what you are hearing right now. Your file never leaves your browser.
Try these:
  1. Click Low-pass. Which instruments disappear, and which survive? Now drag the 500 Hz and 1k handles back up — how does moving the cutoff change the sound?
  2. Click High-pass. What happens to the bass and drums? Why does the song sound "thin"?
  3. Click Band-pass, which keeps only ~500 Hz–1 kHz. Why does it sound like an old telephone or AM radio?
  4. Click Band-stop and listen to what is missing. Compare the gray spectrum to the one you see with Flat.
  5. Look at the curve between handles. Even a sharp step on the sliders becomes a smooth roll-off — a real filter can't jump from 1 to 0 at a single frequency. How does that relate to the filter order you explored above?

Exercises

Exercise 1: Create a simulated time series with 7 different frequencies with noise

Ellipsis

Exercise 2: Show that you can identify each signal using a FFT

Ellipsis

Exercise 3: Remove one frequency with a bandstop filter

Ellipsis

Exercise 4: Remove frequency with a bandstop filter in the frequency domain and reconstruct the signal in the time domain with the frequency removed and compare it to the original

Ellipsis

Graded assignment

The graded version of these exercises is the Signal Processing assignment at the end of this page, which opens in a drawer from the Assignment button in the header. Open it from that page (in molab or by download), sign in with your Dartmouth account inside the notebook, and submit each question when you are ready.

Assignment: Signal Processing

signal-processing · v6

  1. Setup
  2. Q1. Simulate a time series with 7 frequencies plus noise
  3. Q2. Identify each frequency with an FFT
  4. Q3. Remove one frequency with a bandstop filter
  5. Q4. Remove the same frequency in the frequency domain
  6. Q5. Interpretation
Open in molab Opens in a drawer at the bottom of the page, so you can keep reading while you work. Autosaves in this browser.