--- a +++ b/featurebased-approach/subfunctions/multi_qrsdetect.m @@ -0,0 +1,290 @@ +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 +