Files
Feature-Extraction/dist/client/mne/preprocessing/ecg.py

542 lines
15 KiB
Python

# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import numpy as np
from .._fiff.meas_info import create_info
from .._fiff.pick import _picks_to_idx, pick_channels, pick_types
from ..annotations import _annotations_starts_stops
from ..epochs import BaseEpochs, Epochs
from ..evoked import Evoked
from ..filter import filter_data
from ..io import BaseRaw, RawArray
from ..utils import int_like, logger, sum_squared, verbose, warn
@verbose
def qrs_detector(
sfreq,
ecg,
thresh_value=0.6,
levels=2.5,
n_thresh=3,
l_freq=5,
h_freq=35,
tstart=0,
filter_length="10s",
verbose=None,
):
"""Detect QRS component in ECG channels.
QRS is the main wave on the heart beat.
Parameters
----------
sfreq : float
Sampling rate
ecg : array
ECG signal
thresh_value : float | str
qrs detection threshold. Can also be "auto" for automatic
selection of threshold.
levels : float
number of std from mean to include for detection
n_thresh : int
max number of crossings
l_freq : float
Low pass frequency
h_freq : float
High pass frequency
%(tstart_ecg)s
%(filter_length_ecg)s
%(verbose)s
Returns
-------
events : array
Indices of ECG peaks.
"""
win_size = int(round((60.0 * sfreq) / 120.0))
filtecg = filter_data(
ecg,
sfreq,
l_freq,
h_freq,
None,
filter_length,
0.5,
0.5,
phase="zero-double",
fir_window="hann",
fir_design="firwin2",
)
ecg_abs = np.abs(filtecg)
init = int(sfreq)
n_samples_start = int(sfreq * tstart)
ecg_abs = ecg_abs[n_samples_start:]
n_points = len(ecg_abs)
maxpt = np.empty(3)
maxpt[0] = np.max(ecg_abs[:init])
maxpt[1] = np.max(ecg_abs[init : init * 2])
maxpt[2] = np.max(ecg_abs[init * 2 : init * 3])
init_max = np.mean(maxpt)
if thresh_value == "auto":
thresh_runs = np.arange(0.3, 1.1, 0.05)
elif isinstance(thresh_value, str):
raise ValueError('threshold value must be "auto" or a float')
else:
thresh_runs = [thresh_value]
# Try a few thresholds (or just one)
clean_events = list()
for thresh_value in thresh_runs:
thresh1 = init_max * thresh_value
numcross = list()
time = list()
rms = list()
ii = 0
while ii < (n_points - win_size):
window = ecg_abs[ii : ii + win_size]
if window[0] > thresh1:
max_time = np.argmax(window)
time.append(ii + max_time)
nx = np.sum(
np.diff(((window > thresh1).astype(np.int64) == 1).astype(int))
)
numcross.append(nx)
rms.append(np.sqrt(sum_squared(window) / window.size))
ii += win_size
else:
ii += 1
if len(rms) == 0:
rms.append(0.0)
time.append(0.0)
time = np.array(time)
rms_mean = np.mean(rms)
rms_std = np.std(rms)
rms_thresh = rms_mean + (rms_std * levels)
b = np.where(rms < rms_thresh)[0]
a = np.array(numcross)[b]
ce = time[b[a < n_thresh]]
ce += n_samples_start
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.0 * 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.0, rates >= 40.0))[0]
if idx.size > 0:
ideal_rate = np.median(rates[idx]) # get close to the median
else:
ideal_rate = 80.0 # get close to a reasonable default
idx = np.argmin(np.abs(rates - ideal_rate))
clean_events = clean_events[idx]
else:
clean_events = np.array([])
return clean_events
@verbose
def find_ecg_events(
raw,
event_id=999,
ch_name=None,
tstart=0.0,
l_freq=5,
h_freq=35,
qrs_threshold="auto",
filter_length="10s",
return_ecg=False,
reject_by_annotation=True,
verbose=None,
):
"""Find ECG events by localizing the R wave peaks.
Parameters
----------
raw : instance of Raw
The raw data.
%(event_id_ecg)s
%(ch_name_ecg)s
%(tstart_ecg)s
%(l_freq_ecg_filter)s
qrs_threshold : float | str
Between 0 and 1. qrs detection threshold. Can also be "auto" to
automatically choose the threshold that generates a reasonable
number of heartbeats (40-160 beats / min).
%(filter_length_ecg)s
return_ecg : bool
Return the ECG data. This is especially useful if no ECG channel
is present in the input data, so one will be synthesized. Defaults to
``False``.
%(reject_by_annotation_all)s
.. versionadded:: 0.18
%(verbose)s
Returns
-------
ecg_events : array
The events corresponding to the peaks of the R waves.
ch_ecg : int | None
Index of channel used.
average_pulse : float
The estimated average pulse. If no ECG events could be found, this will
be zero.
ecg : array | None
The ECG data of the synthesized ECG channel, if any. This will only
be returned if ``return_ecg=True`` was passed.
See Also
--------
create_ecg_epochs
compute_proj_ecg
"""
skip_by_annotation = ("edge", "bad") if reject_by_annotation else ()
del reject_by_annotation
idx_ecg = _get_ecg_channel_index(ch_name, raw)
if idx_ecg is not None:
logger.info(f"Using channel {raw.ch_names[idx_ecg]} to identify heart beats.")
ecg = raw.get_data(picks=idx_ecg)
else:
ecg, _ = _make_ecg(raw, start=None, stop=None)
assert ecg.ndim == 2 and ecg.shape[0] == 1
ecg = ecg[0]
# Deal with filtering the same way we do in raw, i.e. filter each good
# segment
onsets, ends = _annotations_starts_stops(
raw, skip_by_annotation, "reject_by_annotation", invert=True
)
ecgs = list()
max_idx = (ends - onsets).argmax()
for si, (start, stop) in enumerate(zip(onsets, ends)):
# Only output filter params once (for info level), and only warn
# once about the length criterion (longest segment is too short)
use_verbose = verbose if si == max_idx else "error"
ecgs.append(
filter_data(
ecg[start:stop],
raw.info["sfreq"],
l_freq,
h_freq,
[0],
filter_length,
0.5,
0.5,
1,
"fir",
None,
copy=False,
phase="zero-double",
fir_window="hann",
fir_design="firwin2",
verbose=use_verbose,
)
)
ecg = np.concatenate(ecgs)
# detecting QRS and generating events. Since not user-controlled, don't
# output filter params here (hardcode verbose=False)
ecg_events = qrs_detector(
raw.info["sfreq"],
ecg,
tstart=tstart,
thresh_value=qrs_threshold,
l_freq=None,
h_freq=None,
verbose=False,
)
# map ECG events back to original times
remap = np.empty(len(ecg), int)
offset = 0
for start, stop in zip(onsets, ends):
this_len = stop - start
assert this_len >= 0
remap[offset : offset + this_len] = np.arange(start, stop)
offset += this_len
assert offset == len(ecg)
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
duration_min = duration_sec / 60.0
average_pulse = n_events / duration_min
logger.info(
f"Number of ECG events detected : {n_events} "
f"(average pulse {average_pulse} / min.)"
)
ecg_events = np.array(
[
ecg_events + raw.first_samp,
np.zeros(n_events, int),
event_id * np.ones(n_events, int),
]
).T
out = (ecg_events, idx_ecg, average_pulse)
ecg = ecg[np.newaxis] # backward compat output 2D
if return_ecg:
out += (ecg,)
return out
def _get_ecg_channel_index(ch_name, inst):
"""Get ECG channel index, if no channel found returns None."""
if ch_name is None:
ecg_idx = pick_types(
inst.info,
meg=False,
eeg=False,
stim=False,
eog=False,
ecg=True,
emg=False,
ref_meg=False,
exclude="bads",
)
else:
if ch_name not in inst.ch_names:
raise ValueError(f"{ch_name} not in channel list ({inst.ch_names})")
ecg_idx = pick_channels(inst.ch_names, include=[ch_name])
if len(ecg_idx) == 0:
return None
# raise RuntimeError('No ECG channel found. Please specify ch_name '
# 'parameter e.g. MEG 1531')
if len(ecg_idx) > 1:
warn(
f"More than one ECG channel found. Using only {inst.ch_names[ecg_idx[0]]}."
)
return ecg_idx[0]
@verbose
def create_ecg_epochs(
raw,
ch_name=None,
event_id=999,
picks=None,
tmin=-0.5,
tmax=0.5,
l_freq=8,
h_freq=16,
reject=None,
flat=None,
baseline=None,
preload=True,
keep_ecg=False,
reject_by_annotation=True,
decim=1,
verbose=None,
):
"""Conveniently generate epochs around ECG artifact events.
%(create_ecg_epochs)s
.. note:: Filtering is only applied to the ECG channel while finding
events. The resulting ``ecg_epochs`` will have no filtering
applied (i.e., have the same filter properties as the input
``raw`` instance).
Parameters
----------
raw : instance of Raw
The raw data.
%(ch_name_ecg)s
%(event_id_ecg)s
%(picks_all)s
tmin : float
Start time before event.
tmax : float
End time after event.
%(l_freq_ecg_filter)s
%(reject_epochs)s
%(flat)s
%(baseline_epochs)s
preload : bool
Preload epochs or not (default True). Must be True if
keep_ecg is True.
keep_ecg : bool
When ECG is synthetically created (after picking), should it be added
to the epochs? Must be False when synthetic channel is not used.
Defaults to False.
%(reject_by_annotation_epochs)s
.. versionadded:: 0.14.0
%(decim)s
.. versionadded:: 0.21.0
%(verbose)s
Returns
-------
ecg_epochs : instance of Epochs
Data epoched around ECG R wave peaks.
See Also
--------
find_ecg_events
compute_proj_ecg
Notes
-----
If you already have a list of R-peak times, or want to compute R-peaks
outside MNE-Python using a different algorithm, the recommended approach is
to call the :class:`~mne.Epochs` constructor directly, with your R-peaks
formatted as an :term:`events` array (here we also demonstrate the relevant
default values)::
mne.Epochs(raw, r_peak_events_array, tmin=-0.5, tmax=0.5,
baseline=None, preload=True, proj=False) # doctest: +SKIP
"""
has_ecg = "ecg" in raw or ch_name is not None
if keep_ecg and (has_ecg or not preload):
raise ValueError(
"keep_ecg can be True only if the ECG channel is "
"created synthetically and preload=True."
)
events, _, _, ecg = find_ecg_events(
raw,
ch_name=ch_name,
event_id=event_id,
l_freq=l_freq,
h_freq=h_freq,
return_ecg=True,
reject_by_annotation=reject_by_annotation,
)
picks = _picks_to_idx(raw.info, picks, "all", exclude=())
# create epochs around ECG events and baseline (important)
ecg_epochs = Epochs(
raw,
events=events,
event_id=event_id,
tmin=tmin,
tmax=tmax,
proj=False,
flat=flat,
picks=picks,
reject=reject,
baseline=baseline,
reject_by_annotation=reject_by_annotation,
preload=preload,
decim=decim,
)
if keep_ecg:
# We know we have created a synthetic channel and epochs are preloaded
ecg_raw = RawArray(
ecg,
create_info(
ch_names=["ECG-SYN"], sfreq=raw.info["sfreq"], ch_types=["ecg"]
),
first_samp=raw.first_samp,
)
with ecg_raw.info._unlock():
ignore = ["ch_names", "chs", "nchan", "bads"]
for k, v in raw.info.items():
if k not in ignore:
ecg_raw.info[k] = v
syn_epochs = Epochs(
ecg_raw,
events=ecg_epochs.events,
event_id=event_id,
tmin=tmin,
tmax=tmax,
proj=False,
picks=[0],
baseline=baseline,
decim=decim,
preload=True,
)
ecg_epochs = ecg_epochs.add_channels([syn_epochs])
return ecg_epochs
@verbose
def _make_ecg(inst, start, stop, reject_by_annotation=False, verbose=None):
"""Create ECG signal from cross channel average."""
if not any(c in inst for c in ["mag", "grad"]):
raise ValueError(
"Generating an artificial ECG channel can only be done for MEG data"
)
for ch in ["mag", "grad"]:
if ch in inst:
break
logger.info(
"Reconstructing ECG signal from {}".format(
{"mag": "Magnetometers", "grad": "Gradiometers"}[ch]
)
)
picks = pick_types(inst.info, meg=ch, eeg=False, ref_meg=False)
# Handle start/stop
msg = (
"integer arguments for the start and stop parameters are "
"not supported for Epochs and Evoked objects. Please "
"consider using float arguments specifying start and stop "
"time in seconds."
)
begin_param_name = "tmin"
if isinstance(start, int_like):
if isinstance(inst, BaseRaw):
# Raw has start param, can just use int
begin_param_name = "start"
else:
raise ValueError(msg)
end_param_name = "tmax"
if isinstance(start, int_like):
if isinstance(inst, BaseRaw):
# Raw has stop param, can just use int
end_param_name = "stop"
else:
raise ValueError(msg)
kwargs = {begin_param_name: start, end_param_name: stop}
if isinstance(inst, BaseRaw):
reject_by_annotation = "omit" if reject_by_annotation else None
ecg, times = inst.get_data(
picks,
return_times=True,
**kwargs,
reject_by_annotation=reject_by_annotation,
)
elif isinstance(inst, BaseEpochs):
ecg = np.hstack(inst.copy().get_data(picks, **kwargs))
times = inst.times
elif isinstance(inst, Evoked):
ecg = inst.get_data(picks, **kwargs)
times = inst.times
return ecg.mean(0, keepdims=True), times