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