|
15 | 15 | from wfdb.io import _header
|
16 | 16 | from wfdb.io import _signal
|
17 | 17 | from wfdb.io import download
|
| 18 | +from wfdb.io import annotation |
18 | 19 |
|
19 | 20 |
|
20 | 21 | # -------------- WFDB Signal Calibration and Classification ---------- #
|
@@ -4095,6 +4096,143 @@ def wfdbtime(record_name, input_times, pn_dir=None):
|
4095 | 4096 | print(f'{sample_num:>12}{out_time:>24}{out_date:>32}')
|
4096 | 4097 |
|
4097 | 4098 |
|
| 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 | + |
4098 | 4236 | def _get_date_from_time(start_time, hours, minutes, seconds, out_time):
|
4099 | 4237 | """
|
4100 | 4238 | Convert a starting time to a date using the elapsed time.
|
|
0 commit comments