[302dd3]: / featurebased-approach / subfunctions / multi_qrsdetect.m

Download this file

291 lines (255 with data), 9.4 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
function [qrs,feats]=multi_qrsdetect(signal,fs,fname)
% This function detects QRS peaks on ECG signals by taking the consensus of multiple algorithms, namely:
% - gqrs (WFDB Toolbox)
% - Pan-Tompkins (FECGSYN)
% - Maxima Search (OSET/FECGSYN)
% - matched filtering
%
% --
% ECG classification from single-lead segments using Deep Convolutional Neural
% Networks and Feature-Based Approaches - December 2017
%
% Released under the GNU General Public License
%
% Copyright (C) 2017 Fernando Andreotti, Oliver Carr
% University of Oxford, Insitute of Biomedical Engineering, CIBIM Lab - Oxford 2017
% fernando.andreotti@eng.ox.ac.uk
%
%
% For more information visit: https://github.com/fernandoandreotti/cinc-challenge2017
%
% Referencing this work
%
% Andreotti, F., Carr, O., Pimentel, M.A.F., Mahdi, A., & De Vos, M. (2017).
% Comparing Feature Based Classifiers and Convolutional Neural Networks to Detect
% Arrhythmia from Short Segments of ECG. In Computing in Cardiology. Rennes (France).
%
% Last updated : December 2017
%
% This program is free software: you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation, either version 3 of the License, or
% (at your option) any later version.
%
% This program is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with this program. If not, see <http://www.gnu.org/licenses/>.
qrs = cell(1,5); % CHANGE ME! Using 6 levels of detection
WIN_ALG = round(0.08*fs); % 100ms allow re-alignment of 80ms for gqrs
REF_WIN = round(0.17*fs); % refractory window PT algorithm, no detection should occur here.
WIN_BLOCK = round(5*fs); % window for PT algorithm
OLAP = round(1*fs);
MIRR = round(2*fs); % length to mirror signal at beginning and end to avoid border effects
LEN = length(signal);
signal = [signal(1:MIRR); signal ; signal(LEN-MIRR+1:LEN)];
LEN = LEN + 2*MIRR;
%% gQRS
try
recordName = ['gd_' fname];
tm = 1/fs:1/fs:LEN/fs;
wrsamp(tm',signal,recordName,fs,1)
disp(['Running gqrs ' fname '...'])
if isunix
system(['gqrs -r ' recordName ' -f 0 -s 0']);
disp(['gqrs ran ' fname '...'])
else
gqrs(recordName)
end
qrs{1} = rdann(recordName,'qrs');
disp(['read gqrs output ' fname '...'])
[~,lag]=arrayfun(@(x) max(abs(signal(x-WIN_ALG:x+WIN_ALG))),qrs{1}(2:end-1));
qrs{1}(2:end-1) = qrs{1}(2:end-1) + lag - WIN_ALG - 1;
delete([recordName '.*'])
catch
warning('gqrs failed.')
delete([recordName '.*'])
qrs{1} = [];
end
clear lag tm recordName
%% P&T algorithm (in windows of 5 seconds - 1 sec overlap)
try
disp(['Running jqrs ' fname '...'])
qrs{2} = jqrs(signal,REF_WIN,[],fs,[],[],0);
catch
warning('Pan Tompkins failed.')
qrs{2} = [];
end
clear qrs_tmp startp endp count
%% MaxSearch (mQRS)
qrsmax=qrs{double(length(qrs{2})>length(qrs{1}))+1}; % selecting as reference detector with most detections
try
disp(['Running maxsearch ' fname '...'])
if length(qrsmax)<5
HR_PARAM = 1.3; % default value
else
HR_PARAM = fs./nanmedian(diff(qrsmax))+0.1; % estimate rate (in Hz) for MaxSearch
end
qrs{3} = OSET_MaxSearch(signal,HR_PARAM/fs)';
catch
warning('MaxSearch failed.')
qrs{3} = [];
end
% Put all detectors on same sign (max or min) - shift by median lag
x = decimate(signal,5); % just to reduce calculus
y = sort(x);
flag = mean(abs(y(round(end-0.05*length(y)):end)))>mean(abs(y(1:round(0.05*length(y)))));
for i = 1:length(qrs)
if flag
[~,lag]=arrayfun(@(x) max(signal(x-WIN_ALG:x+WIN_ALG)),qrs{i}(3:end-2));
else
[~,lag]=arrayfun(@(x) min(signal(x-WIN_ALG:x+WIN_ALG)),qrs{i}(3:end-2));
end
qrs{i}(3:end-2) = qrs{i}(3:end-2) + lag - WIN_ALG - 1;
end
clear HR_PARAM qrsmax
%% Matched filtering QRS detection
try
disp(['Running matchedfilter ' fname '...'])
[btype,templates,tempuncut] = beat_class(signal,qrs{3},round(WIN_ALG));
[qrs{4},ect_qrs]=matchedQRS(signal,templates,qrs{3},btype);
matchedf = 1; % status signal used as feature
catch
warning('Matched filter failed.')
disp('Matched filter failed.')
qrs{4} = [];
ect_qrs = [];
matchedf = 0;
end
%% Consensus detection
try
consqrs = kde_fusion(cell2mat(qrs(1:4)'),fs,length(signal)); % 2x gqrs
consqrs(diff(consqrs)<REF_WIN) = []; % removing double detections
[~,lag]=arrayfun(@(x) max(abs(signal(x-WIN_ALG:x+WIN_ALG))),consqrs(2:end-2));
consqrs(2:end-2) = consqrs(2:end-2) + lag - WIN_ALG - 1;
% Put all detectors on same sign (max or min) - shift by median lag
if flag
[~,lag]=arrayfun(@(x) max(signal(x-WIN_ALG:x+WIN_ALG)),consqrs(3:end-2));
else
[~,lag]=arrayfun(@(x) min(signal(x-WIN_ALG:x+WIN_ALG)),consqrs(3:end-2));
end
qrs{5} = consqrs(3:end-2) + lag - WIN_ALG - 1;
catch
disp('Consensus failed.')
qrs{5} = qrs{3};
end
% Remove border detections
qrs = cellfun(@(x) x(x>MIRR&x<LEN-MIRR)-MIRR,qrs,'UniformOutput',false);
signal = signal(MIRR+1:end-MIRR); % just for plotting
ect_qrs = ect_qrs(ect_qrs>MIRR&ect_qrs<(LEN-MIRR)) - MIRR;
clear lag flag consqrs
%% In case ectopic beats are present (consensus QRS locations is ignored, matched filter assumes)
% try
% if ~isempty(ect_qrs)
% % Remove ectopic beats
% cons = qrs{4};
% [~,dist] = dsearchn(cons,ect_qrs);
% insbeats = ect_qrs(dist>REF_WIN); % inserting these beats
%
% cons = sort([cons; insbeats]);
% cons(arrayfun(@(x) find(cons==x),insbeats)) = NaN; % insert NaN on missing beats
% cons = round(inpaint_nans(cons)); % Interpolate with simple spline over missing beats
% cons(cons<1|cons>length(signal)) = [];
% cons = sort(cons);
% cons(diff(cons)<REF_WIN) = [];
% qrs{6} = cons;
%
% % % visualise
% % plot(signal)
% % hold on
% % plot(qrs{4},2.*ones(size(qrs{4})),'ob')
% % plot(ect_qrs,2.*ones(size(ect_qrs)),'xr')
% % plot(qrs{6},3.*ones(size(qrs{6})),'md')
% % legend('signal','matchedf','ectopic','interpolated')
% % close
%
% else
% qrs{6} = qrs{5};
% end
% catch
% warning('Figuring out ectopic failed.')
% qrs{6} = qrs{5};
% ect_qrs = [];
% end
%% Generating some features
% Amplitude around QRS complex
normsig = signal./median(signal(qrs{end})); % normalized amplitude
feat_amp = var(normsig(qrs{end})); % QRS variations in amplitude
feat_amp2 = std(normsig(qrs{end})); % QRS variations in amplitude
feat_amp3 = mean(diff(normsig(qrs{end}))); % QRS variations in amplitude
feats = [feat_amp feat_amp2 feat_amp3];
%% Plots for sanity check
% subplot(2,1,1)
% plot(signal,'color',[0.7 0.7 0.7])
% hold on
% plot(qrs{1},signal(qrs{1}),'sg')
% plot(qrs{2},signal(qrs{2}),'xb')
% plot(qrs{3},signal(qrs{3}),'or')
% plot(qrs{4},signal(qrs{4}),'dy')
% plot(qrs{5},1.3*median(signal(qrs{5}))*ones(size(qrs{5})),'dm')
% legend('signal','gqrs','jqrs','maxsearch','matchedfilt','kde consensus')
% subplot(2,1,2)
% plot(qrs{6}(2:end),diff(qrs{6})*1000/fs)
% ylabel('RR intervals (ms)')
% ylim([250 1300])
% close
end
function det=kde_fusion(qrs,fs,dlength)
% Function uses Kernel density estimation on fusing multiple detections
%
% Input
% qrs Array with all detections
% fs Sampling frequency
% dlength Signal length
%
%
qrs = sort(qrs);
w_std = 0.10*fs; % standard deviation of gaussian kernels [ms]
pt_kernel = round(fs/2); % half window for kernel function [samples]
%% Calculating Kernel Density Estimation
% preparing annotations
peaks = hist(qrs,1:dlength);
% kde (adding gaussian kernels around detections)
kernel = exp(-(-pt_kernel:pt_kernel).^2./(2*w_std^2));
% calculating kernel density estimate
kde = conv(peaks,kernel,'same');
%% Decision
% Parameters
min_dist = round(0.2*fs); % minimal distance between consecutive peaks [ms]
th = max(kde)/3; % threshold for true positives (good measure is number_of_channels/3)
% Finding candidate peaks
wpoints = 1+min_dist:min_dist:dlength; % start points of windows (50% overlap)
wpoints(wpoints>dlength-2*min_dist) = []; % cutting edges
M = arrayfun(@(x) kde(x:x+2*min_dist-1)',wpoints(1:end), ...
'UniformOutput',false); % windowing signal (50% overlap)
M = cell2mat(M);
% adding first segment
head = [kde(1:min_dist) zeros(1,min_dist)]';
M = [head M];
% adding last segment
tail = kde((wpoints(end)+2*min_dist):dlength)';
tail = [tail; zeros(2*min_dist-length(tail),1)];
M = [M tail];
[~,idx] = max(M); % finding maxima
idx = idx+[1 wpoints wpoints(end)+2*min_dist]; % absolute locations
idx(idx < 0) = [];
idx(idx > length(kde)) = [];
i = 1;
while i<=length(idx)
doubled = (abs(idx-idx(i))<min_dist);
if sum(doubled) > 1
[~,idxmax]=max(kde(idx(doubled)));
idxmax = idx(idxmax + find(doubled,1,'first') - 1);
idx(doubled) = [];
idx = [idxmax idx];
clear doubled idxmax
end
i = i+1;
end
% threshold check
idx(kde(idx)<th) = [];
det = sort(idx)';
end