├── LICENSE.md ├── README.md └── rpeakdetect.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jami Pekkanen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ECG beat detection 2 | 3 | Implements an ECG beat (acutally R-peak/QRS-complex) detection 4 | algorithm based on the article [An Efficient R-peak Detection Based on New Nonlinear Transformation and First-Order Gaussian Differentiator](http://link.springer.com/article/10.1007/s13239-011-0065-3/fulltext.html) with some tweaks. 5 | 6 | 7 | ## Requirements 8 | 9 | NumPy and SciPy and matplotlib if you want to use the plotting. 10 | 11 | ## Usage 12 | 13 | The module can be run from command line. Reads newline-delimited raw 14 | ECG sample values and takes the sampling rate in Hz as an argument. In 15 | default mode outputs the detected peak sample numbers in the same format. Eg: 16 | 17 | python2 rpeakdetect.py 128 < ecg_data.csv > beat_samples.csv 18 | 19 | With added argument `plot` plots the detection. 20 | 21 | python2 rpeakdetect.py 128 < ecg_data.csv 22 | 23 | Running the latter with some [sample data](https://raw.github.com/tru-hy/rpeakdetect/gh-pages/ecg_sample.csv) 24 | produces something like the image below. 25 | (The recording is from mobile setting with a rather unconventional 26 | electrode placment, hence the noisiness and a bit weird ECG waveform.) 27 | 28 | Also note that despite the name, the algorithm doesn't actually detect 29 | the R-peaks themselves. Rather the detected time is better described as 30 | "midpoint of the QRS complex". Further the implementation may cause an artificial 31 | shifting of a few (1-2) samples due to not compensating the signal shifting during 32 | taking differences. If you need/want to detect the exact R-peak, 33 | it's quite straightforward to find by locating the maximum signal value 34 | in a small (some milliseconds) window around the detected position. 35 | 36 | ![Detection result example](https://raw.github.com/tru-hy/rpeakdetect/gh-pages/rpeakdetect_sample.png) 37 | -------------------------------------------------------------------------------- /rpeakdetect.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013 Jami Pekkanen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | """ 22 | 23 | import sys 24 | import numpy as np 25 | import scipy.signal 26 | import scipy.ndimage 27 | 28 | def detect_beats( 29 | ecg, # The raw ECG signal 30 | rate, # Sampling rate in HZ 31 | # Window size in seconds to use for 32 | ransac_window_size=5.0, 33 | # Low frequency of the band pass filter 34 | lowfreq=5.0, 35 | # High frequency of the band pass filter 36 | highfreq=15.0, 37 | ): 38 | """ 39 | ECG heart beat detection based on 40 | http://link.springer.com/article/10.1007/s13239-011-0065-3/fulltext.html 41 | with some tweaks (mainly robust estimation of the rectified signal 42 | cutoff threshold). 43 | """ 44 | 45 | ransac_window_size = int(ransac_window_size*rate) 46 | 47 | lowpass = scipy.signal.butter(1, highfreq/(rate/2.0), 'low') 48 | highpass = scipy.signal.butter(1, lowfreq/(rate/2.0), 'high') 49 | # TODO: Could use an actual bandpass filter 50 | ecg_low = scipy.signal.filtfilt(*lowpass, x=ecg) 51 | ecg_band = scipy.signal.filtfilt(*highpass, x=ecg_low) 52 | 53 | # Square (=signal power) of the first difference of the signal 54 | decg = np.diff(ecg_band) 55 | decg_power = decg**2 56 | 57 | # Robust threshold and normalizator estimation 58 | thresholds = [] 59 | max_powers = [] 60 | for i in range(int(len(decg_power)/ransac_window_size)): 61 | sample = slice(i*ransac_window_size, (i+1)*ransac_window_size) 62 | d = decg_power[sample] 63 | thresholds.append(0.5*np.std(d)) 64 | max_powers.append(np.max(d)) 65 | 66 | threshold = np.median(thresholds) 67 | max_power = np.median(max_powers) 68 | decg_power[decg_power < threshold] = 0 69 | 70 | decg_power /= max_power 71 | decg_power[decg_power > 1.0] = 1.0 72 | square_decg_power = decg_power**2 73 | 74 | shannon_energy = -square_decg_power*np.log(square_decg_power) 75 | shannon_energy[~np.isfinite(shannon_energy)] = 0.0 76 | 77 | mean_window_len = int(rate*0.125+1) 78 | lp_energy = np.convolve(shannon_energy, [1.0/mean_window_len]*mean_window_len, mode='same') 79 | #lp_energy = scipy.signal.filtfilt(*lowpass2, x=shannon_energy) 80 | 81 | lp_energy = scipy.ndimage.gaussian_filter1d(lp_energy, rate/8.0) 82 | lp_energy_diff = np.diff(lp_energy) 83 | 84 | zero_crossings = (lp_energy_diff[:-1] > 0) & (lp_energy_diff[1:] < 0) 85 | zero_crossings = np.flatnonzero(zero_crossings) 86 | zero_crossings -= 1 87 | return zero_crossings 88 | 89 | def plot_peak_detection(ecg, rate): 90 | import matplotlib.pyplot as plt 91 | dt = 1.0/rate 92 | t = np.linspace(0, len(ecg)*dt, len(ecg)) 93 | plt.plot(t, ecg) 94 | 95 | peak_i = detect_beats(ecg, rate) 96 | plt.scatter(t[peak_i], ecg[peak_i], color='red') 97 | plt.show() 98 | 99 | if __name__ == '__main__': 100 | rate = float(sys.argv[1]) 101 | 102 | ecg = np.loadtxt(sys.stdin) 103 | if len(sys.argv) > 2 and sys.argv[2] == 'plot': 104 | plot_peak_detection(ecg, rate) 105 | else: 106 | peaks = detect_beats(ecg, rate) 107 | sys.stdout.write("\n".join(map(str, peaks))) 108 | sys.stdout.write("\n") 109 | --------------------------------------------------------------------------------