Categories
Physiological Signals Signal Processing

ECG Signal Processing: Part I, Signal Conditioning

Introduction

In the article series Efficient Realtime Signal Processing C++ Templates, we demonstrate highly efficient and manageable implementation methods and libraries for complex real-time signal processing and machine learning pipelines. This series provides an application example showing how these techniques and libraries support flexible, reliable, and maintainable implementations for electrocardiogram (ECG) signal conditioning as well as QRS complex detection, feature extraction, and classification. The first installment focuses on signal conditioning. See [1] and [2] for more signal processing detail underlying this implementation.

The ECG is a set of electrical signals derived from electrodes placed on the thorax or limbs. These signals represent the superposition of cellular electrical action potentials originating from pacemaker cells in the heart, which propagate through heart tissue and the body to reach surface electrodes. By analyzing features of these signals, clinicians and electrophysiologists gain insight into heart health and can detect irregular heart rhythms such as premature beats or atrial fibrillation. These signals are even more valuable when continuously and automatically monitored by automated machine learning or artificial intelligence algorithms – whose practical, efficient, and state-of-the-art realtime implementation is an area of focus for this series of articles.

An ECG waveform is a differential signal typically derived from 2 to 10 electrodes placed at standard points on the body. The time-varying differential voltage between any two of these electrodes is referred to as an ECG lead or vector. There can be any number of ECG leads computed. Wearables or Band-Aid style ECG patches often have 2 electrodes. In vital signs monitors a driven electrode often serves as a ground reference for 2 to 9 other electrodes to support 3 to 12 lead ECG systems. In the common and standard 12-lead ECG, the leads are labeled I, II, III, AVR, AVL, AVF, V1, V2, V3, V4, V5, and V6. The following code example shows how to compute them from 9 single-ended electrode inputs that use a right leg electrode as the driven ground reference.

A variable called wilsonCentral represents the “wilson central terminal” which is an average of the left arm (LA), right arm (RA), and left leg (LL) electrodes that provides a central body reference to compute the augmented limb leads (aVR, aVL, aVF) and the chest (precordial) leads (V1-V6).

Signal Conditioning C++ Template

ECG leads are prone to various types of noise and artifact that can affect their interpretation, making signal conditioning an essential first stage of processing. Typically, there are four main types of noise or artifact with corresponding filters:

Baseline Wander – low frequency changes or drift in the signal often from patient movement, such as impedance changes due to respiration. A high-pass filter with a cutoff frequency of around 0.5–1 Hz is often used to remove. There are also many other removal methods, such as Wavelet denoising or estimating the baseline drift using a polynomial fit and then subtracting.

Muscle Noise – a designation for any higher frequency noise where muscle activation potentials are significant contributors. However, this noise can come from many other environmental sources, such as electrocautery instruments. A lowpass filter with a cutoff frequency of around 100 Hz is commonly used to mitigate.

Powerline Noise – AC powerline interference of typically (50/60 Hz) is ubiquitous interference in any electronics with connection to a wall socket. A notch filter typically removes it, but because of the challenge in creating a narrow notch via linear time invariant methods, adaptive techniques are often used that adjust their coefficients in real-time to more precisely eliminate a single frequency while also minimizing computation and filter I/O delay. The implementation below employs a classic option described in [1] that is widely used commercially.

Pacemaker Spikes – Pacemakers generate brief, localized electrical impulses to stimulate the heart, compensating for abnormalities in the natural cardiac pacemaker cells or the electrical conduction pathways that coordinate the contraction and relaxation of the heart. These impulses do not resemble natural ECG waves but instead appear as sharp spikes lasting a few milliseconds, with a height between 2 and 20 mV, which can be either positive or negative depending on the lead placement and impulse orientation. The position of the spike in the ECG varies according to the pacing mode:

  • Atrial pacing: The spike appears before the P wave.
  • Ventricular pacing: The spike appears just before the QRS complex.
  • Dual-chamber pacing: Spikes precede both the P wave and the QRS complex.

Compared to older pacemakers, modern pacemakers generate less artifact but can also cause more complex signal dynamics. Rate adaptation pacing modes synchronize better with the heart’s natural speed ups and slowdowns with changes in metabolic demands, caused by exertion for example, which results in a more natural ECG. Because heartbeat time intervals and strength are both modulated beat-to-beat by various physiological influences, exact periodic pacing, for example, is very unnatural. In-demand pacing modes monitor natural pacing, where the pacemaker turns itself on only when natural pacing fails and turns itself off when natural pacing is adequate.

Dealing with pacemakers can be a significant signal processing challenge, but mathematical morphology filtering [TODO: provide link to future mathematical morphology filtering blog entry] is an elegant and efficient nonlinear approach for removing or identifying pacemaker spikes of any type within the ECG. This method, which requires no multiplications and allows for fast implementation, is well-suited for use in wearables that require long battery life.

The following template implements a complete, efficient, high quality, flexible ECG signal conditioning frontend with configurable bandwidths, powerline frequency, enable/disable of each filter stage, etc. The frontend takes a vector of multiple ECG leads as a template parameter and can thus process any number of ECG leads in parallel.

Note that implementation consists of selecting and configuring the desired filter types from the DSP template library and then writing a short loop to run them. The lowpass filter is the computationally efficient sharpened filter introduced in the article, Efficient Realtime Signal Processing C++ Templates and described in [2], and the powerline filter is an improved version of an efficient classic adaptive filter method to remove powerline noise [1] that removes the first and next several powerline frequency harmonics. The highpass filter in this example is a single-pole, single-zero recursive filter that has low real-time I/O delay (supports total signal conditioning I/O delay < 50 ms). However, unlike other component filters in the conditioning, this filter has nonlinear phase where higher frequencies have low I/O delay and lower frequencies longer delay. If bandwidth is set too high, it would cause the frontend to fail performance standards such as EC-13. However, if higher I/O delay is tolerable, it would be easy to replace this highpass with the optimal (highest cutoff while still passing EC-13 pulse distortion tests) linear phase option introduced in [2].

Default configuration parameters for the above example include a 2 kHz sampling frequency that is on the high-end side for ECG. However, all parameters are scaled at the top level without any underlying filter changes to support configuration of any sampling frequency. The template parameter is a vector type representing the number of leads, so this single template can be instanced to process any number of ECG leads – 1, 3, 5, 12, etc.

Implementation Example

Using PyBind11 discussed in article Python Binding of Realtime C++, we can easily instance ECG signal conditioning frontends for different numbers leads and expose them to Python. First instance the ECG::SignalConditioner class using the following template function,

Data moves between NumPy arrays and the filter’s vector inputs/outputs without memory allocation or data copies because vecIn.Init(...) and vecOut.Init(...) set up a view for these vectors to directly use the same NumPy array memory. The following code is then used to build the Python module that includes several ECG signal conditioning frontends with different numbers of leads:

The following example Python code downloads a 2-lead ECG signal from Physionet.org, filters it with one line of Python using the 2-lead external realtime C++ filter implementation, and then plots one of the leads with its filtered version shifted by the I/O delay of the filter returned by the Delay() operator. Because of its real-time design, the external C++ signal conditioner maintains state efficiently (minimal copies and zero memory allocation) via its internal circular buffers and the external code can iteratively send it samples forever in whatever chunks at time that it wishes – 1, 2, 10, 100, or N.

An example output from the Python code is:

Bibliography

[1] M.L. Ahlstrom and W.J. TOMPKINS. “Digital filters for real-time ECG signal processing using microcomputers“, IEEE Trans Biomed Eng, Vol BME-32, No. 9, Sept. 1985, pp 708-713

[2] Alexander Holland, [Online]. Available: Signal Conditioning with Almost No Multiplications via Filter Sharpening | IEEE Conference Publication | IEEE Xplore