Skip to content

Commit 57552cb

Browse files
authored
Merge pull request MIT-LCP#302 from MIT-LCP/sigavg
Adds sigavg function
2 parents 9bc52fd + 7ed6c24 commit 57552cb

File tree

4 files changed

+161
-5
lines changed

4 files changed

+161
-5
lines changed

wfdb/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from wfdb.io.record import (Record, MultiRecord, rdheader, rdrecord, rdsamp,
2-
wrsamp, dl_database, edf2mit, mit2edf, wav2mit, mit2wav,
3-
wfdb2mat, csv2mit, sampfreq, signame, wfdbdesc, wfdbtime)
2+
wrsamp, dl_database, edf2mit, mit2edf, wav2mit,
3+
mit2wav, wfdb2mat, csv2mit, sampfreq, signame,
4+
wfdbdesc, wfdbtime, sigavg)
45
from wfdb.io.annotation import (Annotation, rdann, wrann, show_ann_labels,
56
show_ann_classes, ann2rr, rr2ann, csv2ann,
67
rdedfann, mrgann)

wfdb/io/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from wfdb.io.record import (Record, MultiRecord, rdheader, rdrecord, rdsamp, wrsamp,
2-
dl_database, edf2mit, mit2edf, wav2mit, mit2wav, wfdb2mat,
3-
csv2mit, sampfreq, signame, wfdbdesc, wfdbtime, SIGNAL_CLASSES)
1+
from wfdb.io.record import (Record, MultiRecord, rdheader, rdrecord, rdsamp,
2+
wrsamp, dl_database, edf2mit, mit2edf, wav2mit,
3+
mit2wav, wfdb2mat, csv2mit, sampfreq, signame,
4+
wfdbdesc, wfdbtime, sigavg, SIGNAL_CLASSES)
45
from wfdb.io._signal import est_res, wr_dat_file
56
from wfdb.io.annotation import (Annotation, rdann, wrann, show_ann_labels,
67
show_ann_classes, ann2rr, rr2ann, csv2ann,

wfdb/io/annotation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3133,6 +3133,14 @@ def __init__(self, label_store, symbol, short_description, description):
31333133
def __str__(self):
31343134
return str(self.label_store)+', '+str(self.symbol)+', '+str(self.short_description)+', '+str(self.description)
31353135

3136+
is_qrs = [
3137+
False, True, True, True, True, True, True, True, True, True, # 0 - 9
3138+
True, True, True, True, False, False, False, False, False, False, # 10 - 19
3139+
False, False, False, False, False, True, False, False, False, False, # 20 - 29
3140+
True, True, False, False, True, True, False, False, True, False, # 30 - 39
3141+
False, True, False, False, False, False, False, False, False, False # 40 - 49
3142+
]
3143+
31363144
ann_labels = [
31373145
AnnotationLabel(0, " ", 'NOTANN', 'Not an actual annotation'),
31383146
AnnotationLabel(1, "N", 'NORMAL', 'Normal beat'),

wfdb/io/record.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from wfdb.io import _header
1616
from wfdb.io import _signal
1717
from wfdb.io import download
18+
from wfdb.io import annotation
1819

1920

2021
# -------------- WFDB Signal Calibration and Classification ---------- #
@@ -4095,6 +4096,151 @@ def wfdbtime(record_name, input_times, pn_dir=None):
40954096
print(f'{sample_num:>12}{out_time:>24}{out_date:>32}')
40964097

40974098

4099+
def sigavg(record_name, extension, pn_dir=None, return_df=False,
4100+
start_range=-0.05, stop_range=0.05, ann_type='all', start_time=0,
4101+
stop_time=-1, verbose=False):
4102+
"""
4103+
A common problem in signal processing is to determine the shape of a
4104+
recurring waveform in the presence of noise. If the waveform recurs
4105+
periodically (for example, once per second) the signal can be divided into
4106+
segments of an appropriate length (one second in this example), and the
4107+
segments can be averaged to reduce the amplitude of any noise that is
4108+
uncorrelated with the signal. Typically, noise is reduced by a factor of
4109+
the square root of the number of segments included in the average. For
4110+
physiologic signals, the waveforms of interest are usually not strictly
4111+
periodic, however. This function averages such waveforms by defining
4112+
segments (averaging windows) relative to the locations of waveform
4113+
annotations. By default, all QRS (beat) annotations for the specified
4114+
annotator are included.
4115+
4116+
Parameters
4117+
----------
4118+
record_name : str
4119+
The name of the WFDB record to be read, without any file
4120+
extensions. If the argument contains any path delimiter
4121+
characters, the argument will be interpreted as PATH/BASE_RECORD.
4122+
Both relative and absolute paths are accepted. If the `pn_dir`
4123+
parameter is set, this parameter should contain just the base
4124+
record name, and the files fill be searched for remotely.
4125+
Otherwise, the data files will be searched for in the local path.
4126+
pn_dir : str, optional
4127+
Option used to stream data from Physionet. The Physionet
4128+
database directory from which to find the required record files.
4129+
eg. For record '100' in 'http://physionet.org/content/mitdb'
4130+
pn_dir='mitdb'.
4131+
return_df : bool, optional
4132+
Whether to return a Pandas dataframe (True) or just print the output
4133+
(False).
4134+
start_range : float, int, optional
4135+
Set the measurement window relative to QRS annotations. Negative
4136+
values correspond to offsets that precede the annotations. The default
4137+
is -0.05 seconds.
4138+
stop_range : float, int, optional
4139+
Set the measurement window relative to QRS annotations. Negative
4140+
values correspond to offsets that precede the annotations. The default
4141+
is 0.05 seconds.
4142+
ann_type : list[str], str, optional
4143+
Include annotations of the specified types only (i.e. 'N'). Multiple
4144+
types are also accepted (i.e. ['V','N']). The default is 'all' which
4145+
means to include all QRS annotations.
4146+
start_time : float, int, optional
4147+
Begin at the specified time in record. The default is 0 which denotes
4148+
the start of the record.
4149+
stop_time : float, int, optional
4150+
Process until the specified time in record. The default is -1 which
4151+
denotes the end of the record.
4152+
verbose : bool, optional
4153+
Whether to print the headers (True) or not (False).
4154+
4155+
Returns
4156+
-------
4157+
N/A : Pandas dataframe
4158+
If `return_df` is set to True, return a Pandas dataframe representing
4159+
the output from the original WFDB package. This is the same content as
4160+
if `return_df` were set to False, just in dataframe form.
4161+
4162+
"""
4163+
if start_range >= stop_range:
4164+
raise Exception('`start_range` must be less than `stop_range`')
4165+
if start_time == stop_time:
4166+
raise Exception('`start_time` must be different than `stop_time`')
4167+
if (stop_time != -1) and (start_time >= stop_time):
4168+
raise Exception('`start_time` must be less than `stop_time`')
4169+
if start_time < 0:
4170+
raise Exception('`start_time` must be at least 0')
4171+
if (stop_time != -1) and (stop_time <= 0):
4172+
raise Exception('`stop_time` must be at least greater than 0')
4173+
4174+
if (pn_dir is not None) and ('.' not in pn_dir):
4175+
dir_list = pn_dir.split('/')
4176+
pn_dir = posixpath.join(dir_list[0], get_version(dir_list[0]),
4177+
*dir_list[1:])
4178+
4179+
rec = rdrecord(record_name, pn_dir=pn_dir, physical=False)
4180+
ann = annotation.rdann(record_name, extension)
4181+
4182+
if stop_time == -1:
4183+
stop_time = max(ann.sample) / ann.fs
4184+
samp_start = int(start_time * ann.fs)
4185+
samp_stop = int(stop_time * ann.fs)
4186+
filtered_samples = ann.sample[(ann.sample>=samp_start) & (ann.sample<=samp_stop)]
4187+
4188+
times = np.arange(int(start_range*rec.fs) / rec.fs,
4189+
int(-(-stop_range // (1/rec.fs))) / rec.fs,
4190+
1/rec.fs)
4191+
indices = np.rint(times*rec.fs).astype(np.int64)
4192+
4193+
n_beats = 0
4194+
initial_sig_avgs = np.zeros((times.shape[0],rec.n_sig))
4195+
all_symbols = [a.symbol for a in annotation.ann_labels]
4196+
4197+
for samp in filtered_samples:
4198+
samp_i = np.where(ann.sample==samp)[0][0]
4199+
current_ann = ann.symbol[samp_i]
4200+
if (ann_type != 'all') and (((type(ann_type) is str) and (current_ann != ann_type)) or
4201+
((type(ann_type) is list) and (current_ann not in ann_type))):
4202+
continue
4203+
try:
4204+
if not annotation.is_qrs[all_symbols.index(current_ann)]:
4205+
continue
4206+
except ValueError:
4207+
continue
4208+
4209+
for c,i in enumerate(indices):
4210+
for j in range(rec.n_sig):
4211+
try:
4212+
initial_sig_avgs[c][j] += rec.d_signal[samp+i][j]
4213+
except IndexError:
4214+
initial_sig_avgs[c][j] += 0
4215+
n_beats += 1
4216+
4217+
if n_beats < 1:
4218+
raise Exception('No beats found')
4219+
4220+
if verbose and not return_df:
4221+
print(f'# Average of {n_beats} beats:')
4222+
s = '{:>14}' * rec.n_sig
4223+
print(f'# Time{s.format(*rec.sig_name)}')
4224+
print(f'# sec{s.format(*rec.units)}')
4225+
4226+
final_sig_avgs = []
4227+
for i,time in enumerate(times):
4228+
sig_avgs = []
4229+
for j in range(rec.n_sig):
4230+
temp_sig_avg = initial_sig_avgs[i][j]/n_beats
4231+
temp_sig_avg -= rec.baseline[j]
4232+
temp_sig_avg /= rec.adc_gain[j]
4233+
sig_avgs.append(round(temp_sig_avg,5))
4234+
final_sig_avgs.append(sig_avgs)
4235+
4236+
df = pd.DataFrame(final_sig_avgs, columns=rec.sig_name)
4237+
df.insert(0, 'Time', np.around(times,decimals=5))
4238+
if return_df:
4239+
return df
4240+
else:
4241+
print(df.to_string(index=False, header=False, col_space=13))
4242+
4243+
40984244
def _get_date_from_time(start_time, hours, minutes, seconds, out_time):
40994245
"""
41004246
Convert a starting time to a date using the elapsed time.

0 commit comments

Comments
 (0)