|
a |
|
b/featurebased-approach/subfunctions/lib/JQRS/jqrs.m |
|
|
1 |
function qrs_pos = jqrs(ecg,varargin) |
|
|
2 |
% QRS detector based on the P&T method. This is an offline implementation |
|
|
3 |
% of the detector. |
|
|
4 |
% |
|
|
5 |
% inputs |
|
|
6 |
% ecg: one ecg channel on which to run the detector (required) |
|
|
7 |
% in [mV] |
|
|
8 |
% varargin |
|
|
9 |
% THRES: energy threshold of the detector (default: 0.6) |
|
|
10 |
% [arbitrary units] |
|
|
11 |
% REF_PERIOD: refractory period in sec between two R-peaks (default: 0.250) |
|
|
12 |
% in [ms] |
|
|
13 |
% fs: sampling frequency (default: 1KHz) [Hz] |
|
|
14 |
% fid_vec: if some subsegments should not be used for finding the |
|
|
15 |
% optimal threshold of the P&Tthen input the indices of |
|
|
16 |
% the corresponding points here |
|
|
17 |
% SIGN_FORCE: force sign of peaks (positive value/negative value). |
|
|
18 |
% Particularly usefull if we do window by window detection and want to |
|
|
19 |
% unsure the sign of the peaks to be the same accross |
|
|
20 |
% windows (which is necessary to build an FECG template) |
|
|
21 |
% debug: 1: plot to bebug, 0: do not plot |
|
|
22 |
% |
|
|
23 |
% outputs |
|
|
24 |
% qrs_pos: indexes of detected peaks (in samples) |
|
|
25 |
% sign: sign of the peaks (a pos or neg number) |
|
|
26 |
% en_thres: energy threshold used |
|
|
27 |
% |
|
|
28 |
% |
|
|
29 |
% |
|
|
30 |
% Physionet Challenge 2014, version 1.0 |
|
|
31 |
% Released under the GNU General Public License |
|
|
32 |
% |
|
|
33 |
% Copyright (C) 2014 Joachim Behar |
|
|
34 |
% Oxford university, Intelligent Patient Monitoring Group |
|
|
35 |
% joachim.behar@eng.ox.ac.uk |
|
|
36 |
% |
|
|
37 |
% Last updated : 13-09-2014 |
|
|
38 |
% - bug on refrac period fixed |
|
|
39 |
% - sombrero hat for prefiltering added |
|
|
40 |
% - code a bit more tidy |
|
|
41 |
% - condition added on flatline detection for overall segment (if flatline |
|
|
42 |
% then returns empty matrices rather than some random stuff) |
|
|
43 |
% |
|
|
44 |
% This program is free software; you can redistribute it and/or modify it |
|
|
45 |
% under the terms of the GNU General Public License as published by the |
|
|
46 |
% Free Software Foundation; either version 2 of the License, or (at your |
|
|
47 |
% option) any later version. |
|
|
48 |
% This program is distributed in the hope that it will be useful, but |
|
|
49 |
% WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
50 |
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
|
|
51 |
% Public License for more details. |
|
|
52 |
|
|
|
53 |
% == managing inputs |
|
|
54 |
if nargin > 8 |
|
|
55 |
error('Too many arguments') |
|
|
56 |
end |
|
|
57 |
|
|
|
58 |
% optional input |
|
|
59 |
optargs = {0.250 0.6 1000 [] false 0}; |
|
|
60 |
newVals = cellfun(@(x) ~isempty(x), varargin); |
|
|
61 |
optargs(newVals) = varargin(newVals); |
|
|
62 |
[REF_PERIOD, THRES, fs, fid_vec,SIGN_FORCE,debug] = optargs{:}; |
|
|
63 |
SEARCH_BACK = 1; % perform search back (FIXME: should be in function param) |
|
|
64 |
|
|
|
65 |
if size(ecg,2) > size(ecg,1); ecg = ecg'; end; |
|
|
66 |
|
|
|
67 |
% == constants |
|
|
68 |
NB_SAMP=length(ecg); |
|
|
69 |
tm = 1/fs:1/fs:ceil(NB_SAMP/fs); |
|
|
70 |
MED_SMOOTH_NB_COEFF = round(fs/100); |
|
|
71 |
INT_NB_COEFF = round(0.1*fs); % length is 100 ms |
|
|
72 |
MAX_FORCE = []; % if you want to force the energy threshold value (FIXME: should be in function param) |
|
|
73 |
NB_SAMP = length(ecg); % number of input samples |
|
|
74 |
|
|
|
75 |
|
|
|
76 |
% == Bandpass filtering for ECG signalREF_PERIOD |
|
|
77 |
% this sombrero hat has shown to give slightly better results than a |
|
|
78 |
% standard band-pass filter. Plot the frequency response to convince |
|
|
79 |
% yourself of what it does |
|
|
80 |
b1 = [-7.757327341237223e-05 -2.357742589814283e-04 -6.689305101192819e-04 -0.001770119249103 ... |
|
|
81 |
-0.004364327211358 -0.010013251577232 -0.021344241245400 -0.042182820580118 -0.077080889653194... |
|
|
82 |
-0.129740392318591 -0.200064921294891 -0.280328573340852 -0.352139052257134 -0.386867664739069 ... |
|
|
83 |
-0.351974030208595 -0.223363323458050 0 0.286427448595213 0.574058766243311 ... |
|
|
84 |
0.788100265785590 0.867325070584078 0.788100265785590 0.574058766243311 0.286427448595213 0 ... |
|
|
85 |
-0.223363323458050 -0.351974030208595 -0.386867664739069 -0.352139052257134... |
|
|
86 |
-0.280328573340852 -0.200064921294891 -0.129740392318591 -0.077080889653194 -0.042182820580118 ... |
|
|
87 |
-0.021344241245400 -0.010013251577232 -0.004364327211358 -0.001770119249103 -6.689305101192819e-04... |
|
|
88 |
-2.357742589814283e-04 -7.757327341237223e-05]; |
|
|
89 |
|
|
|
90 |
b1 = resample(b1,fs,250); |
|
|
91 |
bpfecg = filtfilt(b1,1,ecg)'; |
|
|
92 |
|
|
|
93 |
|
|
|
94 |
|
|
|
95 |
% == P&T operations |
|
|
96 |
dffecg = diff(bpfecg'); % (4) differentiate (one datum shorter) |
|
|
97 |
sqrecg = dffecg.*dffecg; % (5) square ecg |
|
|
98 |
intecg = filter(ones(1,INT_NB_COEFF),1,sqrecg); % (6) integrate |
|
|
99 |
mdfint = medfilt1(intecg,MED_SMOOTH_NB_COEFF); % (7) smooth |
|
|
100 |
delay = ceil(INT_NB_COEFF/2); |
|
|
101 |
mdfint = circshift(mdfint,-delay); % remove filter delay for scanning back through ECG |
|
|
102 |
|
|
|
103 |
% look for some measure of signal quality with signal fid_vec? (FIXME) |
|
|
104 |
if isempty(fid_vec); mdfintFidel = mdfint; else mdfintFidel(fid_vec>2) = 0; end; |
|
|
105 |
|
|
|
106 |
% == P&T threshold |
|
|
107 |
if NB_SAMP/fs>90; xs=sort(mdfintFidel(fs:fs*90)); else xs = sort(mdfintFidel(fs:end)); end; |
|
|
108 |
|
|
|
109 |
if isempty(MAX_FORCE) |
|
|
110 |
ind_xs = ceil(0.98*length(xs)); |
|
|
111 |
en_thres = xs(ind_xs); % else 98% CI |
|
|
112 |
else |
|
|
113 |
en_thres = MAX_FORCE; |
|
|
114 |
end |
|
|
115 |
|
|
|
116 |
% build an array of segments to look into |
|
|
117 |
poss_reg = mdfint>(THRES*en_thres); |
|
|
118 |
|
|
|
119 |
% in case empty because force threshold and crap in the signal |
|
|
120 |
if isempty(poss_reg); poss_reg(10) = 1; end; |
|
|
121 |
|
|
|
122 |
% == P&T QRS detection & search back |
|
|
123 |
if SEARCH_BACK |
|
|
124 |
indAboveThreshold = find(poss_reg); % ind of samples above threshold |
|
|
125 |
RRv = diff(tm(indAboveThreshold)); % compute RRv |
|
|
126 |
medRRv = median(RRv(RRv>0.01)); |
|
|
127 |
indMissedBeat = find(RRv>1.5*medRRv); % missedSIGN_FORCE a peak? |
|
|
128 |
% find interval onto which a beat might have been missed |
|
|
129 |
indStart = indAboveThreshold(indMissedBeat); |
|
|
130 |
indEnd = indAboveThreshold(indMissedBeat+1); |
|
|
131 |
|
|
|
132 |
for i=1:length(indStart) |
|
|
133 |
% look for a peak on this interval by lowering the energy threshold |
|
|
134 |
poss_reg(indStart(i):indEnd(i)) = mdfint(indStart(i):indEnd(i))>(0.5*THRES*en_thres); |
|
|
135 |
end |
|
|
136 |
end |
|
|
137 |
|
|
|
138 |
% find indices into boudaries of each segment |
|
|
139 |
left = find(diff([0 poss_reg'])==1); % remember to zero pad at start |
|
|
140 |
right = find(diff([poss_reg' 0])==-1); % remember to zero pad at end |
|
|
141 |
|
|
|
142 |
% Better align to peaks (independent if max/min) |
|
|
143 |
qs = num2cell([left' right'],2); |
|
|
144 |
[~,lag]=cellfun(@(x) max(abs(ecg(x(1):x(2)))),qs); |
|
|
145 |
qrs_pos = left' + lag -1; |
|
|
146 |
d = diff(qrs_pos); % remove doubles |
|
|
147 |
qrs_pos([false; d<REF_PERIOD])=[]; |
|
|
148 |
|
|
|
149 |
% == plots |
|
|
150 |
if debug |
|
|
151 |
figure; |
|
|
152 |
FONTSIZE = 20; |
|
|
153 |
ax(1) = subplot(4,1,1); plot(tm,ecg); hold on;plot(tm,bpfecg,'r') |
|
|
154 |
title('raw ECG (blue) and zero-pahse FIR filtered ECG (red)'); ylabel('ECG'); |
|
|
155 |
xlim([0 tm(end)]); hold off; |
|
|
156 |
ax(2) = subplot(4,1,2); plot(tm(1:length(mdfint)),mdfint);hold on; |
|
|
157 |
plot(tm,max(mdfint)*bpfecg/(2*max(bpfecg)),'r',tm(left),mdfint(left),'og',tm(right),mdfint(right),'om'); |
|
|
158 |
title('Integrated ecg with scan boundaries over scaled ECG'); |
|
|
159 |
ylabel('Int ECG'); xlim([0 tm(end)]); hold off; |
|
|
160 |
ax(3) = subplot(4,1,3); plot(tm,bpfecg,'r');hold on; |
|
|
161 |
plot(tm(qrs_pos),bpfecg(qrs_pos),'+k'); |
|
|
162 |
title('ECG with R-peaks (black) and S-points (green) over ECG') |
|
|
163 |
ylabel('ECG+R+S'); xlim([0 tm(end)]); hold off; |
|
|
164 |
|
|
|
165 |
|
|
|
166 |
%linkaxes(ax,'x'); |
|
|
167 |
set(gca,'FontSize',FONTSIZE); |
|
|
168 |
allAxesInFigure = findall(gcf,'type','axes'); |
|
|
169 |
set(allAxesInFigure,'fontSize',FONTSIZE); |
|
|
170 |
end |
|
|
171 |
|
|
|
172 |
|
|
|
173 |
% NOTES |
|
|
174 |
% Finding the P&T energy threshold: in order to avoid crash due to local |
|
|
175 |
% huge bumps, threshold is choosen at 98-99% of amplitude distribution. |
|
|
176 |
% first sec removed for choosing the thres because of filter init lag. |
|
|
177 |
% |
|
|
178 |
% Search back: look for missed peaks by lowering the threshold in area where the |
|
|
179 |
% RR interval variability (RRv) is higher than 1.5*medianRRv |
|
|
180 |
% |
|
|
181 |
% Sign of the QRS (signForce): look for the mean sign of the R-peak over the |
|
|
182 |
% first 30sec when looking for max of abs value. Then look for the |
|
|
183 |
% R-peaks over the whole record that have this given sign. This allows to |
|
|
184 |
% not alternate between positive and negative detections which might |
|
|
185 |
% happen in some occasion depending on the ECG morphology. It is also |
|
|
186 |
% better than forcing to look for a max or min systematically. |
|
|
187 |
|
|
|
188 |
|