|
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 |
|