a b/featurebased-approach/subfunctions/morphofeatures.m
1
function feats = morphofeatures(signal,fs,qrs,recordName)
2
%  This function obtains morphological features from raw ECG data.
3
%
4
% --
5
% ECG classification from single-lead segments using Deep Convolutional Neural 
6
% Networks and Feature-Based Approaches - December 2017
7
% 
8
% Released under the GNU General Public License
9
%
10
% Copyright (C) 2017  Fernando Andreotti, Oliver Carr
11
% University of Oxford, Insitute of Biomedical Engineering, CIBIM Lab - Oxford 2017
12
% fernando.andreotti@eng.ox.ac.uk
13
%
14
% 
15
% For more information visit: https://github.com/fernandoandreotti/cinc-challenge2017
16
% 
17
% Referencing this work
18
%
19
% Andreotti, F., Carr, O., Pimentel, M.A.F., Mahdi, A., & De Vos, M. (2017). 
20
% Comparing Feature Based Classifiers and Convolutional Neural Networks to Detect 
21
% Arrhythmia from Short Segments of ECG. In Computing in Cardiology. Rennes (France).
22
%
23
% Last updated : December 2017
24
% 
25
% This program is free software: you can redistribute it and/or modify
26
% it under the terms of the GNU General Public License as published by
27
% the Free Software Foundation, either version 3 of the License, or
28
% (at your option) any later version.
29
% 
30
% This program is distributed in the hope that it will be useful,
31
% but WITHOUT ANY WARRANTY; without even the implied warranty of
32
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
33
% GNU General Public License for more details.
34
% 
35
% You should have received a copy of the GNU General Public License
36
% along with this program.  If not, see <http://www.gnu.org/licenses/>.
37
fsnew = 250;
38
gain = 3000;
39
T_LENGTH = 200; % about a second
40
NB_BINS = 250;
41
Nfeats = 17;
42
% Bandpass filter for morphological analysis
43
Fhigh = 1;  % highpass frequency [Hz]
44
Flow = 100;   % low pass frequency [Hz]
45
Nbut = 12;     % order of Butterworth filter
46
d_bp= design(fdesign.bandpass('N,F3dB1,F3dB2',Nbut,Fhigh,Flow,fs),'butter');
47
[b_bp2,a_bp2] = tf(d_bp);
48
clear Fhigh Flow Nbut d_bp
49
signal = filtfilt(b_bp2,a_bp2,signal);               % filtering
50
signal = detrend(signal);
51
52
recordName = ['pu' recordName];
53
% Resampling for ECGPUWAVE
54
sigres=resample(signal,fsnew,fs);
55
qrsfinal = round(qrs{end}*fsnew/fs);
56
57
58
% Morphological features
59
%% Running ECGPUWAVE for normal beats
60
% == Using average normal beat
61
try
62
    [M,RES] = stackbeats(sigres,qrsfinal,T_LENGTH,NB_BINS);
63
    template = mean(M,2);
64
    template = resample(template,RES,NB_BINS);
65
    template = detrend(template);
66
    fake_sig = repmat(template,1,20);
67
    smoothvec = [0:0.1:0.9, ones(1,size(fake_sig,1)-20),0.9:-0.1:0]';
68
    fake_sig = bsxfun(@times,fake_sig,smoothvec); % smoothing edges
69
    fake_sig = reshape(fake_sig,[],1);
70
    fake_sig = detrend(fake_sig);
71
    tm = 1/fsnew:1/fsnew:length(fake_sig)/fsnew;
72
    tm = tm'-1/fsnew;
73
    [~,I] = max(abs(fake_sig));
74
    wsign = sign(fake_sig(I)); % looking for signal sign
75
    qrsfake = cumsum([0 repmat(RES,1,19)]) + floor(RES/2);
76
    fake_sig = 2*gain*wsign(1)*fake_sig/max(abs(fake_sig));
77
    wrsamp(tm,fake_sig,recordName,fsnew,gain,'')
78
    wrann(recordName,'qrs',qrsfake-1,repmat('N',length(qrsfake),1));
79
    if isunix
80
        system(['ecgpuwave -r ' recordName '.hea' ' -a ecgpu -i qrs']);
81
    else
82
       ecgpuwave(recordName,'ecgpu',[],[],'qrs'); % important to specify the QRS because it seems that ecgpuwave is crashing sometimes otherwise
83
    end
84
    [allref,alltypes_r] = rdann(recordName,'ecgpu');
85
    fake_sig = fake_sig./gain;
86
    featavgnorm = FECGSYN_QTcalc(alltypes_r,allref,fake_sig,fsnew);
87
    feats = struct2array(featavgnorm);
88
    feats(isnan(feats)) = 0;
89
catch
90
    warning('ecgpuwave NORMAL average has failed.')
91
    feats = zeros(1,Nfeats);
92
end
93
94
delete([recordName '.*'])
95
96
end
97
98
function feats = FECGSYN_QTcalc(ann_types,ann_stamp,signal,fs)
99
100
temp_types = ann_types;     % allows exclusion of unsuitable annotations
101
temp_stamp = ann_stamp;zeros(1,10);
102
feats = struct('QRSheight',0,'QRSwidth',0,'QRSpow',0,...
103
    'noPwave',0,'Pheight',0,'Pwidth',0,'Ppow',0,...
104
    'Theight',0,'Twidth',0,'Tpow',0,...
105
    'Theightnorm',0,'Pheightnorm',0,'Prelpow',0,...
106
    'PTrelpow',0,'Trelpow',0,'QTlen',0,'PRlen',0);
107
108
%== Treat biphasic T-waves
109
annstr = strcat({temp_types'});
110
idxbi=cell2mat(regexp(annstr,'tt')); % biphasic
111
nonbi2= cell2mat(regexp(annstr,'\)t\(')) +1; % weird t
112
nonbi3= cell2mat(regexp(annstr,'\)t\)')) +1; % weird t2
113
if sum(idxbi) > 0   % case biphasic waves occured
114
    posmax = [idxbi' idxbi'+1];
115
    [~,bindx]=max(abs(signal(ann_stamp(posmax))),[],2); % max abs value between tt
116
    clearidx = [idxbi+double(bindx'==1) nonbi2 nonbi3];
117
    %idxbi = idxbi + bindx -1; % new index to consider
118
    temp_types(clearidx) = [];    % clearing biphasic cases
119
    temp_stamp(clearidx) = [];
120
end
121
122
clear biphasic teesfollowed nonbi1 nonbi2 nonbi3 bindx clearidx posmax idxbi
123
124
%== Remove incomplete beats
125
comp=cell2mat(regexp(cellstr(temp_types'),'\(p\)\(N\)\(t\)')); % regular
126
beats = temp_stamp(cell2mat(arrayfun(@(x) x:x+8,comp','uniformoutput',0)));
127
skipP = false;
128
if isempty(beats)
129
    comp=cell2mat(regexp(cellstr(temp_types'),'\(t\)\(N\)\(t\)')); % missing P-wave
130
    if isempty(comp), return, end % if ECGPUWAVE does not really work then output zeros
131
    beats = temp_stamp(cell2mat(arrayfun(@(x) x:x+8,comp','uniformoutput',0)));
132
    skipP = true;
133
    feats.noPwave = 1;
134
else
135
    feats.noPwave = 0;
136
end
137
138
if size(beats,2)==1; beats = beats';end % case single beat is available
139
Pstart = beats(:,1);
140
validP = beats(:,2);
141
Pend = beats(:,3);
142
Rstart = beats(:,4);
143
validR = beats(:,5);
144
Rend = beats(:,6);
145
Tstart = beats(:,7);
146
validT = beats(:,8);
147
Tend = beats(:,9);
148
149
%% Calculating features
150
feats.QRSheight = abs(median(min([signal(Rstart) signal(Rend)],[],2) - signal(validR))); %        T-wave height
151
feats.QRSwidth = median(Rend - Rstart)/fs; %  T-wave width
152
feats.QRSpow = sum(signal(Rstart:Rend).^2);
153
154
if ~skipP
155
    feats.Pheight = abs(median(median([signal(Pstart) signal(Pend)],2) - signal(validP))); %        P-wave height
156
    feats.Pwidth = median(Pend - Pstart)/fs; %  P-wave width
157
    feats.Ppow = sum(signal(Pstart:Pend).^2);
158
end
159
160
feats.Theight = abs(median(median([signal(Tstart) signal(Tend)],2) - signal(validT))); %        T-wave height
161
feats.Twidth = median(Tend - Tstart)/fs; %  T-wave width
162
feats.Tpow = sum(signal(Tstart:Tend).^2);
163
164
feats.Theightnorm =  feats.Theight/feats.QRSheight;
165
feats.Trelpow = feats.Tpow/feats.QRSpow;
166
167
if ~skipP
168
    feats.Pheightnorm =  feats.Pheight/feats.QRSheight;
169
    feats.Prelpow = feats.Ppow/feats.QRSpow;
170
    feats.PTrelpow = feats.Ppow/feats.Tpow;
171
end
172
173
QT_MAX = 0.5*fs; % Maximal QT length (in s)  MAY VARY DEPENDING ON APPLICATION!
174
QT_MIN = 0.1*fs; % Minimal QT length (in s)  MAY VARY DEPENDING ON APPLICATION!
175
[~,dist] = dsearchn(Rstart,Tend);         % closest ref for each point in test qrs
176
feats.QTlen = median(dist(dist<QT_MAX&dist>QT_MIN))/fs;
177
178
if ~skipP
179
    PR_MAX = 0.3*fs; % Maximal QT length (in s)  MAY VARY DEPENDING ON APPLICATION!
180
    PR_MIN = 0.05*fs; % Minimal QT length (in s)  MAY VARY DEPENDING ON APPLICATION!
181
    [~,dist] = dsearchn(validP,validR);         % closest ref for each point in test qrs
182
    feats.PRlen = median(dist(dist<PR_MAX&dist>PR_MIN))/fs;
183
end
184
185
end