Filters

The filters package provides digital signal processing filters specifically designed for HD-sEMG signal processing. All filters are implemented with a focus on phase preservation and numerical stability.

Bandpass Filter

The package provides a zero-phase Butterworth bandpass filter implementation that is particularly suited for EMG signal processing. The filter is implemented using SciPy's butter and sosfiltfilt functions for optimal numerical stability.

API Reference

def bandpass_filter(data: np.ndarray, order: int, lowcut: float, highcut: float, fs: float) -> np.ndarray:
    """Apply a zero-phase Butterworth bandpass filter to 1D data."""

Parameters

  • data (np.ndarray):

    • 1D array of signal samples to be filtered
    • Shape: (n_samples,)
    • Type: float
  • order (int):

    • Filter order
    • Higher orders give sharper cutoffs but may introduce more numerical artifacts
    • Typical values: 2-8
  • lowcut (float):

    • Lower cutoff frequency in Hz
    • Typical EMG values: 10-30 Hz
  • highcut (float):

    • Upper cutoff frequency in Hz
    • Typical EMG values: 400-500 Hz
  • fs (float):

    • Sampling frequency in Hz
    • Must be at least 2x the highest frequency component (Nyquist criterion)

Returns

  • filtered_data (np.ndarray):
    • Filtered signal
    • Same shape as input data
    • Zero-phase (no temporal shifting)

Implementation Details

The filter uses: - Butterworth filter design for maximally flat frequency response - Second-order sections (SOS) form for improved numerical stability - Zero-phase filtering via forward-backward application (sosfiltfilt) - Automatic scaling of frequencies to Nyquist frequency

Example Usage

import numpy as np
from hdsemg_shared.filters.bandpass import bandpass_filter

# Generate sample EMG data
fs = 2000  # Sample rate: 2kHz
t = np.linspace(0, 1, fs)  # 1 second of data
emg = np.random.randn(fs)  # Simulated noise-like EMG

# Apply bandpass filter (20-450 Hz, 4th order)
filtered_emg = bandpass_filter(
    data=emg,
    order=4,
    lowcut=20,    # Remove low-frequency drift
    highcut=450,  # Remove high-frequency noise
    fs=fs
)

Best Practices

  1. Filter Order Selection:
  2. Start with order=4 for most applications
  3. Increase order if you need sharper cutoffs
  4. Decrease order if you observe ringing artifacts

  5. Frequency Selection:

  6. For surface EMG: lowcut=20Hz, highcut=450Hz is typical
  7. Adjust based on your specific application and noise conditions
  8. Ensure highcut < fs/2 (Nyquist frequency)

  9. Edge Effects:

  10. The filter may introduce edge effects at the start/end of the signal
  11. Consider padding your signal or discarding edge regions in critical analyses

Common Use Cases

  • Removing power line interference and baseline drift
  • Isolating the main EMG frequency band (20-450 Hz)
  • Pre-processing before amplitude analysis or feature extraction

Future Extensions

The filters package is designed to be extensible. Planned or possible additions include:

  • Notch filters for power line interference
  • Adaptive filters for specific noise types
  • Wavelet-based filtering approaches
  • Multi-channel filtering utilities

Source Code

The implementation can be found in src/hdsemg_shared/filters/bandpass.py.

API Dokumentation

bandpass_filter(data, N, fcl, fch, fs)

Exact Python replica of Ton van den Bogert's MATLAB bandpassfilter. @credit Ton van den Bogert, https://biomch-l.isbweb.org/archive/index.php/t-26625.html


Parameters

data : 1-D ndarray Signal to be filtered. N : int Total filter order requested by the user (must be even). Internally the Butterworth prototype is designed with order N/2 and then applied forwards & backwards (filtfilt) which doubles the effective order. fcl, fch : float Lower and higher cut-off frequencies [Hz]. fs : float Sampling frequency [Hz].

Returns

fdata : ndarray Zero-phase band-pass filtered signal (same length as data).

Source code in hdsemg_shared/filters/bandpass.py
def bandpass_filter(data: np.ndarray,
                    N: int,
                    fcl: float,
                    fch: float,
                    fs: float) -> np.ndarray:
    """
    Exact Python replica of Ton van den Bogert's MATLAB `bandpassfilter`.
    @credit Ton van den Bogert, https://biomch-l.isbweb.org/archive/index.php/t-26625.html

    -------------------------------------------------------------------
    Parameters
    ----------
    data : 1-D ndarray
        Signal to be filtered.
    N : int
        *Total* filter order requested by the user (must be even).
        Internally the Butterworth prototype is designed with order N/2 and then
        applied forwards & backwards (filtfilt) which doubles the effective order.
    fcl, fch : float
        Lower and higher cut-off frequencies [Hz].
    fs : float
        Sampling frequency [Hz].

    Returns
    -------
    fdata : ndarray
        Zero-phase band-pass filtered signal (same length as `data`).
    """

    # ------------- 1. argument checks  ----------
    if N < 2 or N % 2:
        raise ValueError("N must be an even integer ≥ 2 (bi-directional filtering).")
    if fs <= 0 or fcl <= 0 or fch <= 0 or fcl >= fch:
        raise ValueError("Cut-off frequencies must satisfy 0 < fcl < fch < fs/2.")
    # ------------------------------------------------------------------------

    # ------------- 2. translate MATLAB design rule --------------------------
    # In Ton’s routine Wn is *pre-warped* so that after filtfilt()
    # the –3 dB point sits exactly at the user’s fcl/fch.
    halfN = N // 2  # order actually given to butter()
    beta = (np.sqrt(2) - 1) ** (1 / (2 * N))  # pre-warping constant
    Wn = (2.0 * np.asarray([fcl, fch])) / (fs * beta)  # normalised (0–1)
    Wn = np.clip(Wn, 1e-6, 0.999)  # avoid numerical issues with very low frequencies
    # ------------------------------------------------------------------------

    # ------------- 3. design filter & apply zero-phase ----------------------
    sos = butter(halfN, Wn, btype='bandpass', output='sos')  # identical poles/zeros
    fdata = sosfiltfilt(sos, data, axis=-1)  # zero-phase (like filtfilt)
    return fdata