Skip to content

Commit

Permalink
Make find_ecg_events work even if detection fails (#9236)
Browse files Browse the repository at this point in the history
Fixes #9225
  • Loading branch information
hoechenberger authored and larsoner committed Apr 2, 2021
1 parent 0defe0a commit 1b03e6a
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 15 deletions.
2 changes: 2 additions & 0 deletions doc/changes/0.22.inc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Bugs

- Fix bug with :func:`mne.SourceEstimate.plot` and related functions where the scalars were not interactively updated properly (:gh:`8985` by `Eric Larson`_)

- :func:`mne.preprocessing.find_ecg_events` now correctly handles situation where no ECG activity could be detected, and correctly returns an empty array of ECG events (:gh:`9236` by `Richard Höchenberger`_)

.. _changes_0_22:

Version 0.22.0
Expand Down
41 changes: 26 additions & 15 deletions mne/preprocessing/ecg.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def qrs_detector(sfreq, ecg, thresh_value=0.6, levels=2.5, n_thresh=3,
Returns
-------
events : array
Indices of ECG peaks
Indices of ECG peaks.
"""
win_size = int(round((60.0 * sfreq) / 120.0))

Expand Down Expand Up @@ -112,20 +112,27 @@ def qrs_detector(sfreq, ecg, thresh_value=0.6, levels=2.5, n_thresh=3,
ce = time[b[a < n_thresh]]

ce += n_samples_start
clean_events.append(ce)

# pick the best threshold; first get effective heart rates
rates = np.array([60. * len(cev) / (len(ecg) / float(sfreq))
for cev in clean_events])

# now find heart rates that seem reasonable (infant through adult athlete)
idx = np.where(np.logical_and(rates <= 160., rates >= 40.))[0]
if len(idx) > 0:
ideal_rate = np.median(rates[idx]) # get close to the median
if ce.size > 0: # We actually found an event
clean_events.append(ce)

if clean_events:
# pick the best threshold; first get effective heart rates
rates = np.array([60. * len(cev) / (len(ecg) / float(sfreq))
for cev in clean_events])

# now find heart rates that seem reasonable (infant through adult
# athlete)
idx = np.where(np.logical_and(rates <= 160., rates >= 40.))[0]
if idx.size > 0:
ideal_rate = np.median(rates[idx]) # get close to the median
else:
ideal_rate = 80. # get close to a reasonable default

idx = np.argmin(np.abs(rates - ideal_rate))
clean_events = clean_events[idx]
else:
ideal_rate = 80. # get close to a reasonable default
idx = np.argmin(np.abs(rates - ideal_rate))
clean_events = clean_events[idx]
clean_events = np.array([])

return clean_events


Expand Down Expand Up @@ -219,7 +226,11 @@ def find_ecg_events(raw, event_id=999, ch_name=None, tstart=0.0,
remap[offset:offset + this_len] = np.arange(start, stop)
offset += this_len
assert offset == len(ecg)
ecg_events = remap[ecg_events]

if ecg_events.size > 0:
ecg_events = remap[ecg_events]
else:
ecg_events = np.array([])

n_events = len(ecg_events)
duration_sec = len(ecg) / raw.info['sfreq'] - tstart
Expand Down
11 changes: 11 additions & 0 deletions mne/preprocessing/tests/test_ecg.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os.path as op
import pytest
import numpy as np

from mne.io import read_raw_fif
from mne import pick_types
Expand Down Expand Up @@ -84,3 +85,13 @@ def test_find_ecg():
assert len(ecg_epochs.events) == n_events
assert 'ECG-SYN' not in raw.ch_names
assert 'ECG-SYN' not in ecg_epochs.ch_names

# Test behavior if no peaks can be found -> achieve this by providing
# all-zero'd data
raw._data[ecg_idx] = 0.
ecg_events, _, average_pulse, ecg = find_ecg_events(
raw, ch_name=raw.ch_names[ecg_idx], return_ecg=True
)
assert ecg_events.size == 0
assert average_pulse == 0
assert np.allclose(ecg, np.zeros_like(ecg))

0 comments on commit 1b03e6a

Please sign in to comment.