How to Create a Simple Low-Pass Filter

Summary: This article shows how to create a simple low-pass filter, starting from a cutoff frequency \(f_c\) and a transition bandwidth \(b\). This article is complemented by a Filter Design tool that allows you to create your own custom versions of the example filter that is shown below, and download the resulting filter coefficients.

How to create a simple low-pass filter? A low-pass filter is meant to allow low frequencies to pass, but to stop high frequencies. Theoretically, the ideal (i.e., perfect) low-pass filter is the sinc filter. The sinc function (normalized, hence the \(\pi\)’s, as is customary in signal processing), is defined as

\[\mathrm{sinc}(x)=\frac{\sin(\pi x)}{\pi x}.\]

The sinc filter is a scaled version of this that I’ll define below. When convolved with an input signal, the sinc filter results in an output signal in which the frequencies up to the cutoff frequency are all included, and the higher frequencies are all blocked. This is because the sinc function is the inverse Fourier transform of the rectangular function. Multiplying the frequency representation of a signal by a rectangular function can be used to generate the ideal frequency response, since it completely removes the frequencies above the cutoff point. And, since multiplication in the frequency domain is equivalent with convolution in the time domain, the sinc filter has exactly the same effect.

The windowed-sinc filter that is described in this article is an example of a Finite Impulse Response (FIR) filter.

Sinc Filter

The sinc function must be scaled and sampled to create a sequence and turn it into a (digital) filter. The impulse response of the sinc filter is defined as

\[h[n]=2f_c\mathrm{sinc}(2f_cn),\]

where \(f_c\) is the cutoff frequency. The cutoff frequency should be specified as a fraction of the sampling rate. For example, if the sampling rate is 10 kHz, then \(f_c=0.1\) will result in the frequencies above 1 kHz being removed. The central part of a sinc filter with \(f_c=0.1\) is illustrated in Figure 1.

Figure 1. Sinc filter.Figure 1. Sinc filter.

The problem with the sinc filter is that it has an infinite length, in the sense that its values do not drop to zero. This means that the delay of the filter will also be infinite, making this filter unrealizable. The straightforward solution is to simply stop computing at a certain point (effectively truncating the filter), but that produces excessive ripple. A better solution is to window the sinc filter, which results in, you guessed it, a windowed-sinc filter.

Window

A window function is a function that is zero outside of some interval. There exist a great variety of these functions, tuned for different properties, but I’ll simply use the well-known Blackman window here, which is a good choice for general usage. It is defined as (for \(N\) points)

\[w[n]=0.42-0.5\cos\left({\frac{2\pi n}{N-1}}\right)+0.08\cos\left({\frac{4\pi n}{N-1}}\right),\]

with \(n\in[0,\,N-1]\). It is shown in Figure 2, for \(N=51\).

Figure 2. Blackman window.Figure 2. Blackman window.

Windowed-Sinc Filter

The final windowed-sinc filter is then simply the product of the two preceding expressions, as follows (with the sinc filter shifted to the range \([0,\,N-1]\)).

\[h[n]=\mathrm{sinc}\left(2f_c\left(n-\frac{N-1}{2}\right)\right)\left(0.42-0.5\cos\left({\frac{2\pi n}{N-1}}\right)+0.08\cos\left({\frac{4\pi n}{N-1}}\right)\right),\]

with \(h[n]=0\) for \(n\notin[0,\,N-1]\). I have dropped the factor \(2f_c\) from the sinc filter, since it is much easier to ignore constants at first and normalize the complete filter at the very end, by simply making sure that the sum of all coefficients is one, giving the filter unity gain, with

\[h_\mathrm{normalized}[n]=h[n]/\sum_{i=0}^{N-1}h[i].\]

This results in the normalized windowed-sinc filter of Figure 3.

Figure 3. Normalized windowed-sinc filter.Figure 3. Normalized windowed-sinc filter.

Transition Bandwidth

The final task is to incorporate the desired transition bandwidth (or roll-off) of the filter. To keep things simple, you can use the following approximation of the relation between the transition bandwidth \(b\) and the filter length \(N\),

\[b\approx\frac{4}{N},\]

with the additional condition that it is best to make \(N\) odd. This is not really required, but an odd-length symmetrical FIR filter has a delay that is an integer number of samples, which makes it easy to compare the filtered signal with the original one. Setting \(N=51\) above was reached by setting \(b=0.08\). As for \(f_c\), the parameter \(b\) should be specified as a fraction of the sampling rate. Hence, for a sampling rate of 10 kHz, setting \(b=0.08\) results in a transition bandwidth of about 800 Hz, which means that the filter transitions from letting through frequencies to blocking them over a range of about 800 Hz. The values for \(f_c\) and \(b\) in this article were chosen to make the figures as clear as possible. The frequency response of the final filter (with \(f_c=0.1\) and \(b=0.08\)) is shown in Figure 4.

Figure 4. Frequency response on a linear (left) and logarithmic (right) scale.Figure 4. Frequency response on a linear (left) and logarithmic (right) scale.

Python Code

In Python, all these formulas can be implemented concisely.

from __future__ import division
 
import numpy as np
 
fc = 0.1  # Cutoff frequency as a fraction of the sampling rate (in (0, 0.5)).
b = 0.08  # Transition band, as a fraction of the sampling rate (in (0, 0.5)).
N = int(np.ceil((4 / b)))
if not N % 2: N += 1  # Make sure that N is odd.
n = np.arange(N)
 
# Compute sinc filter.
h = np.sinc(2 * fc * (n - (N - 1) / 2))
 
# Compute Blackman window.
w = 0.42 - 0.5 * np.cos(2 * np.pi * n / (N - 1)) + \
    0.08 * np.cos(4 * np.pi * n / (N - 1))
 
# Multiply sinc filter by window.
h = h * w
 
# Normalize to get unity gain.
h = h / np.sum(h)

Applying the filter \(h\) to a signal \(s\) by convolving both sequences can then be as simple as writing the single line:

s = np.convolve(s, h)

In the Python script above, I compute everything in full to show you exactly what happens, but, in practice, shortcuts are available. For example, the Blackman window can be computed with w = np.blackman(N).

In the follow-up article How to Create a Simple High-Pass Filter, I convert this low-pass filter into a high-pass one using spectral inversion. Both kinds of filters are then combined in How to Create Simple Band-Pass and Band-Reject Filters

Filter Design Tool

This article is complemented with a Filter Design tool. Experiment with different values for \(f_c\) and \(b\), visualize the resulting filters, and download the filter coefficients. Try it now!

Filter designer.Filter designer.

leon (not verified)

Wed, 12/10/2014 - 17:37

Thanks! That's a very useful article.
Could you explain, what can I do with delay between source and filtered signals?

The nice thing about these kinds of filters is that it is easy to compensate for their delay. Symmetrical FIR filters, of which the presented windowed-sinc filter is an example, delay all frequency components in the same way. This means that the delay can be characterized by a single number. The delay of a filter of length M equals (M-1)/2. Hence, the shown filter with 51 coefficients has a delay of exactly 25 samples. So, if you want to overlay the original signal with the filtered one to compare them, you just need to shift one of them by 25 samples!

Jeremy (not verified)

Sun, 03/22/2015 - 23:47

Really appreciate how concise this article is. Thanks!

Marie Harpøth (not verified)

Mon, 08/17/2015 - 11:26

Thank you! It's a very nice article. Could you maybe explain why it is necessary to normalize each element in h by the sum(h)?

It’s not really necessary, but if you make sure that the sum of the coefficients is one, then the gain of the filter at DC is also one. Additionally, it allows you to make the gain of the filter whatever you want simply by multiplying the coefficients of the normalized filter by the required gain factor.

Josef Sobotka (not verified)

Fri, 02/26/2016 - 21:57

Thanks very much for your article... I had considered this topic whole week and now my mind is clear!

yitzhak (not verified)

Mon, 08/22/2016 - 10:57

Thanks, this is really helpful !

Noboa (not verified)

Fri, 08/26/2016 - 22:05

Great article!
How do you make your plots?
Thanks

Thanks! The plots for most of the articles, including this one, were made with Python (using matplotlib).

Vrish (not verified)

Mon, 10/10/2016 - 00:27

Very interesting and clear article, thanks Tom! Question: You say that the number of coefficients is approx 4/b. If i use your fir designer tool and design a lowpass (windowed sinc) with Samplerate=44100, Cutoff=1000, Bandwidth=1000, i would expect approx 4/(1000/44100) = 176.4 thus 177 coefficients, but it gives 203 coefficients. I have been searching for the logic in that but i don't understand it. How do you define the specific number of coefficients? Thanks.

That’s a good observation. The reason for this is that the number of coefficients in the tool depends on the window function. This is because the window has a large influence on the transition bandwidth, so that, e.g., the rectangular window can get by with much less coefficients than the Blackman window. Maybe I'll write a separate article on this with more details, because it’s quite interesting stuff. In practice, the tool uses 4.6/b for Blackman, 3.1/b for Hamming, and 0.91/b for rectangular. From your example, I can tell that you’ve used a Blackman window, since 4.6/(1000/44100) = 202.86.

Fadil (not verified)

Mon, 10/17/2016 - 16:22

Thanks for your tutorial, well detailed!
Is the signal that you filter an array containing the amplitude of the signal?

Thanks! And yes, the variable s in the example is an array of numbers. I would say that this is the signal, and not just its amplitude.

I was just being nitpicky a bit, I think... :-) The signal is indeed really just a series of numbers that represent an amplitude. It’s just that saying “the amplitude of the signal” could suggest that you’ve left something out or did some sort of preprocessing. That’s why I’d just write “the signal”…

Jimmy (not verified)

Fri, 02/17/2017 - 17:36

Thanks for this great article. I have a question regarding Figure 4. How did you create the frequency response diagram?

This is another great idea for a follow-up article! I'll do one where I explain this in detail. In short, you first pad the filter with zeros to increase the resolution of the frequency plot, then take an fft, compute the power, and plot the result, either on a linear scale or in dB.

chang (not verified)

Wed, 11/01/2017 - 16:16

Thank you. Would you write something about IIR?

Ben (not verified)

Thu, 04/19/2018 - 08:50

Well-elaborated! Many thanks.

Angeles Gallego (not verified)

Thu, 05/03/2018 - 22:46

This was very useful, thanks!

Santiago (not verified)

Mon, 05/07/2018 - 08:05

Hello, i'm implementing a fir filter, band reject to be exact, could the signal s be the data of an fft from an audio.wav?

Your signal s should be the data of the audio.wav file, not the FFT of the data. Of course, if you are going to implement the convolution in the frequency domain, then you need to take the FFT of both the signal and the filter coefficients.

Jim Frazer (not verified)

Tue, 05/08/2018 - 12:42

Sorry Tom,
I was wrong in my earlier comment. fS/fL evaluates to zero in Python 3 as well, so the code gives the Blackman weights.

Your plots are, of course, correct but the display maybe doesn't come from the sample code shown below ?

I am a geophysicist, seismic sampling is often 500 -1000Hz The input box is labelled Hz.

Once again, thanks !
very impressive !

Jim
______________________________
import numpy as np

# Example code, computes the coefficients of a low-pass windowed-sinc filter.

# Configuration.
fS = 1000 # Sampling rate, ***** works fS = 1000.0 ? *****
fL = 20 # Cutoff frequency, *****works fL = 20.0 ? *****
N = 461 # Filter length, must be odd.

# Compute sinc filter.
h = np.sinc(2 * fL / fS * (np.arange(N) - (N - 1) / 2.))

# Apply window.
h *= np.blackman(N)

# Normalize to get unity gain.
h /= np.sum(h)

print(h)

# Applying the filter to a signal s can be as simple as writing
# s = np.convolve(s, h)

Hello Jim,

fL/fS (or fS/fL) does definitely not evaluate to zero in Python 3. Maybe two version of Python are installed on your computer, and you're still using Python 2? The plots on fiiir.com are indeed not generated in Python.

Tom

Jim Frazer (not verified)

Wed, 05/09/2018 - 07:42

Tom,
Yes, indeed, I did have both versions active and must have used 2.7 again.

I just ran it in Python 3.4.5 and it works fine, a lesson learned.

I should have checked, was confused debugging in Idle

Jim

Ruben (not verified)

Thu, 05/10/2018 - 00:52

Awesome! you made windowed-sinc-filter design very easy!

Mamatha (not verified)

Tue, 08/28/2018 - 17:17

Hi Tom,
I'm trying to apply this filter to my data which has sample rate of 250Hz and contains 1000 samples. After applying the filter my data has 1050 samples instead of 1000. Am I doing something wrong? Please help.

Thanks in advance.

Tom

Tue, 08/28/2018 - 18:37

In reply to by Mamatha (not verified)

This is normal. The length of the output signal is the length of the input signal plus the length of the filter minus one. This follows from the definition of convolution, but for a more intuitive understanding, have a look at the nice animations on https://en.wikipedia.org/wiki/Convolution#Visual_explanation. An effect of this is that you will see a so-called transient response of the filter in the beginning of your output signal, and that you have to wait a number of samples (the length of the filter, i.e., 51 samples in case of the example filter) before the filter is "filled up" and you get the actual response for which the filter was designed (the so-called steady state response).

Mamatha (not verified)

Tue, 08/28/2018 - 19:18

In reply to by Mamatha (not verified)

I found it myself :). That was due to numpy.convolve. I used mode='same' and I got what I want.

Girish Subbarao (not verified)

Tue, 12/04/2018 - 16:25

Very good article.

Thanks for the articale and online filter-designer!

But it seems to me there must bу 2. * fL шт formula:

h = np.sinc(2 * fL / fS * (np.arange(N) - (N - 1) / 2.))

Ran it on Python 2.7 and found out with integer 2 the filter coefficients differ drastically (but with float 2, they correspond to the generated in pyhon list).

Thanks for pointing this out! (For other readers, the code snipped is from one of the generated Python programs from fiiir.com.) I've assumed Python 3 for a long time for all code on this site and on fiiir.com (you know, looking towards the future and all), but I think that I'll have to fold and make all my code compatible with both Python 2 and Python 3, because people keep reporting these kinds of problems… I'll look into it…

Gi Tae Seo (not verified)

Tue, 04/16/2019 - 13:42

Thanks for your article.
That's a very useful article.
Could you explain, could we define this(=low_pass_filter) as one of FFT filters?
I saw someone called it(=low_pass_filter) as one of FFT filters.
Sorry for my English.

Yes, you can use FFT convolution with these filters, with exactly the same result. Instead of applying the filter with s = np.convolve(s, h) as described above, you could use

from fft.signal import fftconvolve
s = fftconvolve(s, h)

According to the documentation for SciPy fftconvolve(), the SciPy convolve() even picks the best algorithm (direct or FFT) automatically. The NumPy convolve() that I've used above doesn't do that.

DrakeL (not verified)

Thu, 05/30/2019 - 08:16

Hi Tom,
Thanks for the article. How would you determine the transition band width of Kaiser window function? Is it possible to provide the article where you found the transition band width for other window functions? And what is the criteria to determine the cutoff frequency? Thank you in advance!

Tom

Sat, 06/01/2019 - 16:02

In reply to by DrakeL (not verified)

I might misunderstand your question, but the cutoff frequency and rolloff normally follow from the problem that you are trying to solve by applying the filter that is being designed. For example, if the useful signal in the input has low frequencies and there is high frequency interference, then you could put the cutoff frequency "halfway" and make the rolloff as large as possible (to make the filter shorter) without removing part of the signal or letting through part of the noise.

I don't remember where I got the values for the other window functions… For (some) more details, see The Transition Bandwidth of a Filter Depends on the Window Type).

Thank you for the response, Tom. I am doing a lab experience, so I need to find a way to determine the cutoff frequency and rolloff parameters. I am new for signal processing, so I'm not quite sure the limitation to determine the parameters. When you say to make the rolloff as large as possible, is that mean to make the transition bandwith is smaller (the slope is steeper)?

As large as possible means to make the transition bandwitdh larger, so the that the slope is less steep. The effect of this is that the filter becomes shorter (has less coefficients). This can be important if you have large amounts of data or for real-time processing, because a short filter can be faster to compute (depending on its implementation). You can play with this on fiiir.com. To determine the cutoff frequency and rolloff, you have to look at your data: which frequencies do you want to let through, and which ones do you want to block? That's what will determine your choice of parameters.

I am a bit confused. I thought an ideal case for the filter is when it has a straight down transition bandwidth (zero width), which means we need the roll-off as narrow as possible, thus the slope is steeper. This can increase the signal-to-noise ratio. Am I misunderstanding the terminologies? Thanks!

It all depends on your input signal: the rolloff only needs to be steep enough to filter out the unwanted frequencies. It's fine if it's steeper, but then your filter will be longer…

满 畅 (not verified)

Mon, 09/30/2019 - 06:13

thank you TomRoelandts. the article really help.

Add new comment

The content of this field is kept private and will not be shown publicly.
Spam avoidance measure, sorry for this.

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Submitted on 15 April 2014