Skip to content

Commit d93b8dc

Browse files
committed
Adds sigavg function
1 parent 9bc52fd commit d93b8dc

File tree

4 files changed

+153
-5
lines changed

4 files changed

+153
-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: 138 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,143 @@ 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, time_start=0, time_stop=-1,
4101+
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+
time_start : float, int, optional
4143+
Begin at the specified time in record. The default is 0 which denotes
4144+
the start of the record.
4145+
time_stop : float, int, optional
4146+
Process until the specified time in record. The default is -1 which
4147+
denotes the end of the record.
4148+
verbose : bool, optional
4149+
Whether to print the headers (True) or not (False).
4150+
4151+
Returns
4152+
-------
4153+
N/A : Pandas dataframe
4154+
If `return_df` is set to True, return a Pandas dataframe representing
4155+
the output from the original WFDB package. This is the same content as
4156+
if `return_df` were set to False, just in dataframe form.
4157+
4158+
"""
4159+
if start_range >= stop_range:
4160+
raise Exception('`start_range` must be less than `stop_range`')
4161+
if time_start == time_stop:
4162+
raise Exception('`time_start` must be different than `time_stop`')
4163+
if (time_stop != -1) and (time_start >= time_stop):
4164+
raise Exception('`time_start` must be less than `time_stop`')
4165+
if time_start < 0:
4166+
raise Exception('`time_start` must be at least 0')
4167+
if (time_stop != -1) and (time_stop <= 0):
4168+
raise Exception('`time_stop` must be at least greater than 0')
4169+
4170+
if (pn_dir is not None) and ('.' not in pn_dir):
4171+
dir_list = pn_dir.split('/')
4172+
pn_dir = posixpath.join(dir_list[0], get_version(dir_list[0]),
4173+
*dir_list[1:])
4174+
4175+
rec = rdrecord(record_name, pn_dir=pn_dir, physical=False)
4176+
ann = annotation.rdann(record_name, extension)
4177+
4178+
if time_stop == -1:
4179+
time_stop = max(ann.sample) / ann.fs
4180+
samp_start = int(time_start * ann.fs)
4181+
samp_stop = int(time_stop * ann.fs)
4182+
filtered_samples = ann.sample[(ann.sample>=samp_start) & (ann.sample<=samp_stop)]
4183+
4184+
times = np.arange(int(start_range*rec.fs) / rec.fs,
4185+
int(-(-stop_range // (1/rec.fs))) / rec.fs,
4186+
1/rec.fs)
4187+
indices = np.rint(times*rec.fs).astype(np.int64)
4188+
4189+
n_beats = 0
4190+
initial_sig_avgs = np.zeros((times.shape[0],rec.n_sig))
4191+
for samp in filtered_samples:
4192+
samp_i = np.where(ann.sample==samp)[0][0]
4193+
4194+
all_symbols = [a.symbol for a in annotation.ann_labels]
4195+
try:
4196+
if not annotation.is_qrs[all_symbols.index(ann.symbol[samp_i])]:
4197+
continue
4198+
except ValueError:
4199+
continue
4200+
4201+
for c,i in enumerate(indices):
4202+
for j in range(rec.n_sig):
4203+
try:
4204+
initial_sig_avgs[c][j] += rec.d_signal[samp+i][j]
4205+
except IndexError:
4206+
initial_sig_avgs[c][j] += 0
4207+
n_beats += 1
4208+
4209+
if n_beats < 1:
4210+
raise Exception('No beats found')
4211+
4212+
if verbose and not return_df:
4213+
print(f'# Average of {n_beats} beats:')
4214+
s = '{:>14}' * rec.n_sig
4215+
print(f'# Time{s.format(*rec.sig_name)}')
4216+
print(f'# sec{s.format(*rec.units)}')
4217+
4218+
final_sig_avgs = []
4219+
for i,time in enumerate(times):
4220+
sig_avgs = []
4221+
for j in range(rec.n_sig):
4222+
temp_sig_avg = initial_sig_avgs[i][j]/n_beats
4223+
temp_sig_avg -= rec.baseline[j]
4224+
temp_sig_avg /= rec.adc_gain[j]
4225+
sig_avgs.append(round(temp_sig_avg,5))
4226+
final_sig_avgs.append(sig_avgs)
4227+
4228+
df = pd.DataFrame(final_sig_avgs, columns=rec.sig_name)
4229+
df.insert(0, 'Time', np.around(times,decimals=5))
4230+
if return_df:
4231+
return df
4232+
else:
4233+
print(df.to_string(index=False, header=False, col_space=13))
4234+
4235+
40984236
def _get_date_from_time(start_time, hours, minutes, seconds, out_time):
40994237
"""
41004238
Convert a starting time to a date using the elapsed time.

0 commit comments

Comments
 (0)