R-DECO: An open-source Matlab based graphical user interface for the detection and correction of R-peaks 1.0.0
(27,665 bytes)
function [R_peak,RR_int,parameters,check] = peak_detection(parameters,signal,fs,parameters_check)
% Output:
% R_peak - Location of the R-peaks in samples
% RR_int - Intervals between the R-peaks in ms
% check - Check if the method was run correctly
%
% Author(s): Jonathan Moeyersons (Jonathan.Moeyersons@esat.kuleuven.be)
% Sabine Van Huffel (Sabine.Vanhuffel@esat.kuleuven.be)
% Carolina Varon (Carolina.Varon@esat.kuleuven.be)
%
% Version History:
% - 06/05/2019 JM Initial version
%
% Copyright (c) 2019, Jonathan Moeyersons, KULeuven-ESAT-STADIUS
%
% This software is made available for non commercial research purposes only
% under the GNU General Public License. However, notwithstanding any
% provision of the GNU General Public License, this software may not be
% used for commercial purposes without explicit written permission after
% contacting jonathan.moeyersons@esat.kuleuven.be
%
% 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 <https://www.gnu.org/licenses/>.
% Set check
check = 1;
% Get the size of the signal
[original_signal_length,nr_ch] = size(signal);
% Pre-allocate the R-peak and RR-interval variable
R_peak = cell(1,nr_ch);
RR_int = cell(1,nr_ch);
if parameters_check
% Get the different parameters
parameters = Parameterdialog(parameters,signal,fs);
end
% Check validity of the parameter dialog
if ~isempty(parameters)
env = round(fs*parameters{1}/1000);
avgHR = parameters{2};
postproc = parameters{3};
ect = parameters{4};
inverted = parameters{5};
% Check if the signal is inverted, if so, take action
if inverted
signal = -signal;
end
% Set the segment size to one minute and compute the amount of minutes in
% the signal
segm_length = 60*fs;
nr_segm_original = floor(original_signal_length/segm_length);
% Add zeros if the signal does not contain a round number of minutes
if nr_segm_original ~= 0
signal = [signal; zeros(segm_length-(original_signal_length-nr_segm_original*segm_length),nr_ch)];
% Get the new length and amount of segments
[new_signal_length,~] = size(signal);
nr_segm_new = floor(new_signal_length/segm_length);
else
% Get the new length and amount of segments
[new_signal_length,~] = size(signal);
nr_segm_new = 1;
end
% Pre-allocate the R-peaks per channel
R_peak_ch = cell(nr_segm_new);
% Loop over the channels
for ii = 1:nr_ch
%% Segmentize the signals
avg = mean(signal(:,ii));
s = std(signal(:,ii));
if nr_segm_new > 1
segmented_ecg = (double(reshape(double(signal(:,ii)),segm_length,nr_segm_new)));
% segmented_ecg = segmented_ecg-(repmat(avg,size(segmented_ecg,1),size(segmented_ecg,2)));
% segmented_ecg = segmented_ecg./(repmat(s,size(segmented_ecg,1),size(segmented_ecg,2)));
else
segmented_ecg = (signal(:,ii)-avg)/s;
end
% Go through each segment
for iii = 1:nr_segm_new
%% Make the segments five seconds longer on each side
if nr_segm_new > 1
if iii == 1
% End the segment five seconds later
segm = [segmented_ecg(:,1); segmented_ecg(1:fs*5,2)];
elseif iii==nr_segm_new
% Start the segment five seconds earlier
segm = [segmented_ecg(end-(fs*5)+1:end,iii-1); segmented_ecg(1:end-(new_signal_length-original_signal_length),iii)];
else
% Start the segement five seconds earlier and end five seconds later
segm = [segmented_ecg(end-(fs*5)+1:end,iii-1);segmented_ecg(:,iii);segmented_ecg(1:fs*5,iii+1)];
end
else
% Take the whole segment
segm = segmented_ecg;
end
%% Envelope the signal
% Get the time
time = (1:length(segm))/fs;
% Upper envelope
up_env = env_secant(time,segm,env,'top');
% Lower envelope
low_env = env_secant(time,segm,env,'bottom');
% Enveloped segment
env_segm = up_env-low_env;
%% Detect peaks in the enveloped segment
% Get the frequency ratio with 250 Hz
freq_ratio = fs/250;
% Get the envelope ratio
env_ratio = env/(0.15*fs);
% Peaks are detected by checking whether the derivative is positive and
% if the peaks last long enough (step size). The smaller the step
% size, the more sure that all R-peaks are detected, but also the
% more non-R-peaks are detected. To avoid local minima, a step
% (~=1) is used. The original step size is 20 samples = 80ms
% for an envelope size of 150ms and a sampling frequency of
% 250 Hz.
all_peaks = calc_pos_opt(env_segm,floor(20*freq_ratio*env_ratio),1);
% figure; plot(env_segm); hold on; scatter(all_peaks,env_segm(all_peaks));
% Only continue if enough peaks have been detected
if length(all_peaks) > time(end)/2
% Adaptive thresholding
[R_peak_segm, RR_int_segm, ~] = adaptive_thresholding(all_peaks, env_segm, fs, avgHR, time);
% figure; plot(env_segm); hold on; scatter(R_peak_segm,env_segm(R_peak_segm));
if postproc && length(RR_int_segm) > 5
% Post-process the RR-intervals
[R_peak_segm] = post_processing(all_peaks, R_peak_segm, RR_int_segm, env_segm, fs);
end
else
% Set the R and RR variables empty
R_peak_segm = [];
end
% Remove excessive R-peaks on both sides
if length(R_peak_segm) > time(end)/2
if nr_segm_new > 1
if iii == 1
% Remove the last five seconds
R_peak_segm = R_peak_segm(R_peak_segm<=segm_length);
elseif iii == nr_segm_new
% Remove the first five seconds
R_peak_segm = R_peak_segm(R_peak_segm>fs*5)-(fs*5);
else
% Remove the first and last five seconds
R_peak_segm = R_peak_segm(R_peak_segm>fs*5)-fs*5;
R_peak_segm = R_peak_segm(R_peak_segm<=segm_length);
end
end
R_peak_segm = reshape(R_peak_segm,1,length(R_peak_segm));
else
R_peak_segm = [];
end
% Store the R-peaks of this channel in a cell
R_peak_ch{iii} = unique(R_peak_segm);
end
%% Merge segments
R_peak_temp = [];
% Loop over the segments
for iii = 1:nr_segm_new
R_peak_temp = [R_peak_temp,R_peak_ch{iii}+segm_length*(iii-1)]; %#ok
end
%% R-peak correction in the ECG signal
% Define window width (originally this was equal to the envelope size)
window = round(0.065*fs);
R_peak_temp = peaks_in_ecg(signal(:,ii),R_peak_temp,window);
% Take only the unique R-peaks
R_peak_temp = unique(R_peak_temp);
% Get the RR-intervals in ms
RR_int_temp = 1000*(diff(R_peak_temp)/fs);
if ~isempty(R_peak_temp)
%% Correct the R-peaks for ectopics
if ect
[R_peak_temp,~,~] = ectopic_detection_correction(fs,RR_int_temp,R_peak_temp);
end
end
%% Store the R-peaks and RR-intervals
if ~isempty(R_peak_temp)
R_peak{ii} = round(unique(R_peak_temp));
RR_int{ii} = 1000*(diff(R_peak{ii})/fs);
else
R_peak{ii} = [];
RR_int{ii} = [];
end
end
else
% Set check
check=0;
end
end
%%%%%% Functions
function [R_peak_segm, RR_int_segm, RR_avg] = adaptive_thresholding(peaks,signal,fs,avgHR,time)
% Get the standard deviation of the enveloped signal
STD = std(signal);
% Set the average RR-interval
RR_avg = zeros(1,length(peaks)-2);
RR_avg(1) = 60000/avgHR;
% % Set the average signal and noise peak
% peak_avg = 0.7*trimmean(signal(peaks),20);
% noise_avg = peak_avg/2;
peak_avg = 0.7*(median(signal(peaks))+mean(signal(peaks)))/2;
noise_avg = 0.3*(median(signal(peaks))+mean(signal(peaks)))/2;
% Set thresholds
peak_threshold = noise_avg + 0.25*(peak_avg-noise_avg);
noise_threshold = peak_threshold/2;
% Pre-allocate
R_peak_segm = zeros(1,length(peaks));
RR_int_segm = zeros(1,length(peaks)-1);
% Select the first R-peak
count = 1;
while R_peak_segm(1) == 0
if signal(peaks(count)) > noise_threshold
R_peak_segm(1) = peaks(count);
else
count = count + 1;
end
end
% Start the peak counter
peak_counter = 1;
% Loop over all peaks, starting from the second
for idx = count + 1 : length(peaks)
% Select the peak
peak = signal(peaks(idx));
% Define the lower limit of the RR-interval
RR_lower_limit = 0.65*RR_avg(idx-1);
if RR_lower_limit < 200
RR_lower_limit = 200; % Lower limit for RRI is 200 ms --> Includes every condition
end
% Define the upper limit of the RR-interval
RR_upper_limit = 1.35*RR_avg(idx-1);
if RR_upper_limit > 1600
RR_upper_limit = 1600; % Upper limit for RRI is 1600 ms
end
% Determine whether the peak is a signal or a noise peak
if peak >= peak_threshold % Peak is an R-peak
% Make the peak amplitude smaller if it is too high
% NOTE: This was previously done before the peak
% search, but this might cause the location of the
% peak to deviate from the actual location.
if peak > 4*STD
peak = 4*STD;
end
% Adjust the peak average
peak_avg = 0.125*peak + 0.875*peak_avg;
% Add signal peak
peak_counter = peak_counter + 1;
R_peak_segm(peak_counter) = peaks(idx);
% Adjust RR interval
RR_int_segm(peak_counter-1) = 1000*(R_peak_segm(peak_counter)-R_peak_segm(peak_counter-1))/fs; % In ms
elseif peak <= noise_threshold % Peak is a noise peak
% Adjust the noise average
noise_avg = 0.125*peak + 0.875*noise_avg;
else % searchback procedure
if peak_counter > 1
% Check if the candidate RR-interval is within
% the boundaries of the lower and upper
% RR-limits with the previous R-peak
if time(peaks(idx)) >= RR_lower_limit/1000+time(R_peak_segm(peak_counter))...
&& time(peaks(idx)) <= RR_upper_limit/1000+time(R_peak_segm(peak_counter))
% Check if we are at the end, if the next peak is a
% signal peak, if it is a signal peak, check the time
% interval
if idx == length(peaks) || signal(peaks(idx+1)) < peak_threshold || (signal(peaks(idx+1)) >= peak_threshold && time(peaks(idx+1))-time(peaks(idx)) > 0.2)
% Adjust the peak average
peak_avg = 0.25*peak + 0.75*peak_avg;
% Add R-peak
peak_counter = peak_counter + 1;
R_peak_segm(peak_counter) = peaks(idx);
% Adjust RR interval
RR_int_segm(peak_counter-1) = 1000*(R_peak_segm(peak_counter)-R_peak_segm(peak_counter-1))/fs; % In ms
else % Peak is a noise peak
% Adjust the noise average
noise_avg = 0.125*peak+0.875*noise_avg;
end
else % Peak is a noise peak
% Adjust the noise average
noise_avg = 0.125*peak+0.875*noise_avg;
end
else % Peak is a noise peak
% Adjust the noise average
noise_avg = 0.125*peak+0.875*noise_avg;
end
end
% Adjust the RR-average
if peak_counter > 9
RR_avg(idx) = mean(RR_int_segm(peak_counter-8:peak_counter-1));
elseif peak_counter > 1
RR_avg(idx) = 0.5*RR_avg(idx-1)+0.5*median(RR_int_segm(1:peak_counter-1));
end
% Adjust thresholds
peak_threshold = noise_avg + 0.25*(peak_avg-noise_avg);
noise_threshold = peak_threshold/2;
end
% Get the actual R-peaks
R_peak_segm = R_peak_segm(1:peak_counter);
% Get the actual RR-intervals, in ms
RR_int_segm = RR_int_segm(1:peak_counter-1);
% figure;
% plot(signal)
% hold on;
% scatter(R_peak_segm,signal(R_peak_segm))
end
function R_peak_segm = post_processing(all_peaks, R_peak_segm, RR_int_segm, signal, fs)
% To check whether RRI is correct. This will be done by comparing
% the RR-interval with the mean of the 3 past RR-intervals. A past
% RR-interval should not be a 'problem' interval.
% Set basic variables
a = 0.3; % 30% difference
d = 0.5; % 50% difference
max_k = 6; % Maximum ammount of previous RR-intervals
% Get the amount of RR-intervals
len = length(RR_int_segm);
% Pre-allocate
large_prob = zeros(1,len); % Index of large problems
RRref_all = zeros(1,len); % Reference RR intervals
% Set first RR-reference
RR_ref = trimmean(RR_int_segm(1:5),50);
RRref_all(1) = RR_ref;
% Set loop start
counter = 1;
% Loop over the different RR-intervals
while counter < len-1
% Define the RR-ref if you are further than the first RR-interval
if counter > 2
% Set variables
k = 1; % Number of previous RR-intervals
sum_ref = 0; % Sum of the good previous RRI's
num = 0; % Number of good previous RRI's
wght = [0.5 0.3 0.2]; % Weights: the farther away, the lower the weight
while counter-k > 0 && num < 3 && k < max_k
% Check if the previous RRI is a big problem
if large_prob(counter-k) == 1
prob_check = 1;
elseif large_prob(counter-k) == 0
prob_check = 0;
end
% If this is not the case, use it for reference
if ~prob_check
num = num + 1;
sum_ref = sum_ref + wght(num)*RR_int_segm(counter-k);
end
k = k+1;
end
% If no good previous RR-intervals are found, this probably means that
% this is the new normal, so just take the three previous
if num <= 1
k = 1;
while counter-k > 0 && num < 3
num = num + 1;
sum_ref = sum_ref + wght(num)*RR_int_segm(counter-k);
k = k+1;
end
end
% Define the reference variables
RR_ref = sum_ref/sum(wght(1:num));
no_RR_ref = nnz(RRref_all);
RRref_all(no_RR_ref+1) = RR_ref;
end
% Check for too small RR-intervals
if RR_int_segm(counter) < (1-a)*RR_ref || RR_int_segm(counter) < 200
% Set looping variables
counter1 = 1;
counter2 = 0;
test = 1;
% Get the sum with the previous RR-interval
try
RRnew1 = RR_int_segm(counter) + RR_int_segm(counter-1);
catch
RRnew1 = [];
end
% Get the sum with the next RR-interval
sumRR = RR_int_segm(counter) + RR_int_segm(counter + counter1);
RRnew2 = sumRR;
% Loop until a good summation is found
while test == 1
% Only proceed if we are not at the end of the signal
if counter + counter1 < len
% If the new RR-interval is still too small
if sumRR < (1-a)*RR_ref || sumRR < 200
% Adjust counter
counter1 = counter1 + 1;
% Add the next RR-interval
sumRR = sumRR + RR_int_segm(counter+counter1);
% Good summation
elseif abs(sumRR-RR_ref) < a*RR_ref
% Set new looping variable
test2 = 1;
% Adjust the second counter
counter2 = counter2 + 1;
% Loop until the best summation is found
while test2 == 1
% Only proceed if we are not at the end of the
% signal
if counter + counter1 + counter2 < len
% Add the next RR-interval
sumRR2 = sumRR + RR_int_segm(counter + counter1 + counter2);
% Compute the difference of both sums with the
% RR-reference
diff1 = abs(RR_ref-sumRR) / RR_ref;
diff2 = abs(RR_ref-sumRR2) / RR_ref;
% If the initial sum is best
if diff1 < diff2
% Stop both loops
test = 0;
test2 = 0;
% Store the initial sum as new RR-interval
RRnew2 = sumRR;
% Adjust the second counter (necessary for
% later)
counter2 = counter2-1;
% If the second sum is best
else
% Redefine the initial sum and go through
% the loop again
sumRR = sumRR2;
% Adjust the counter
counter2 = counter2+1;
% Stop the loop if we are at the end of the
% signal
if counter + counter1 + counter2 > len
test = 0;
test2 = 0;
RRnew2 = sumRR2;
end
end
else
% Stop the loop
test = 0;
test2 = 0;
RRnew2 = sumRR;
end
end
% Too big
else
% Stop the loop
test = 0;
% Select the previous sum as correct
RRnew2 = sumRR - RR_int_segm(counter + counter1);
% Adjust the first counter (necessary for later)
counter1 = counter1 - 1;
end
else
% Stop loop
test = 0;
RRnew2 = sumRR;
end
end
% If no better option has been found,
% or if the new option is better than the
% combination of the previous RR-intervals
if ~isempty(RRnew1) && abs(RRnew1 - RR_ref) < abs(RRnew2 - RR_ref) && abs(RRnew1-RR_ref) < a*RR_ref
% Set RRnew
RRnew = RRnew1;
% Replace RR-interval
RR_int_segm(counter - 1) = RRnew1;
% Change RR
RR_int_segm(counter) = [];
% Change large prob vector (has equal length as RR)
large_prob(counter) = [];
% Change RRref_all (is one smaller than RR)
if counter + 1 < len-1
RRref_all(counter) = [];
end
% Get new length
len = length(RR_int_segm);
% Make sure that the new interval is checked
counter = counter-1;
else
% Set RRnew
RRnew = RRnew2;
% Replace RR-interval
RR_int_segm(counter) = RRnew;
% Change the variables depending on circumstances
if counter1 >= 1
% Change RR
RR_int_segm(counter + 1 : counter + counter1 + counter2) = [];
% Change large prob vector (has equal length as RR)
large_prob(counter + 1 : counter + counter1 + counter2) = [];
% Change RRref_all (is one smaller than RR)
if counter+1 < len-1
RRref_all(counter+1:min([counter+counter1+counter2, len-1])) = [];
end
% Get new length
len = length(RR_int_segm);
end
end
% Check if the interval is a big problem
if abs(RRnew-RR_ref)/RR_ref > d || RRnew < 200 % Check for big problems
large_prob(counter) = 1;
end
% Check for too big RR-intervals
elseif RR_int_segm(counter) > (1+a)*RR_ref || RR_int_segm(counter) > 1600
% Convert RR-intervals to samples
RR_samples = RR_int_segm*fs/1000;
% Define R-peak positions
R_peak_positions = cumsum(RR_samples(1:counter)) + R_peak_segm(1);
% Define beginning and end of the interval
if counter > 1
begin_int = R_peak_positions(end-1);
else
begin_int = R_peak_segm(1);
end
end_int = R_peak_positions(end);
% Get the first peak that is bigger than the beginning of the
% interval
test_peak = all_peaks(all_peaks > begin_int);
test_peak = test_peak(test_peak < end_int);
if length(test_peak) > 1
% Get the test RR interval
RR_test = 1000*(test_peak-begin_int)/fs;
% Find the smallest difference with the
% reference RR interval
[~,I] = min(abs(RR_test-RR_ref));
test_peak = test_peak(I);
end
% See if that peak is before the end of the
% interval and not too small
if ~isempty(test_peak) &&...
signal(test_peak) > prctile(signal,50) && ...
(((test_peak-begin_int)/fs)*1000) > (1-a)*RR_ref &&...
(((test_peak-begin_int)/fs)*1000) > 200 &&...
(((end_int-test_peak)/fs)*1000) > (1-a)*RR_ref && ...
(((end_int-test_peak)/fs)*1000) > 200
% Add an RR interval
if counter > 1
RR_int_segm = [RR_int_segm(1:counter-1) (((test_peak-begin_int)/fs)*1000) (((end_int-test_peak)/fs)*1000) RR_int_segm(counter+1:end)];
elseif counter+1 > len
RR_int_segm = [RR_int_segm(1:counter-1) (((test_peak-begin_int)/fs)*1000) (((end_int-test_peak)/fs)*1000)];
else
RR_int_segm = [(((test_peak-begin_int)/fs)*1000) (((end_int-test_peak)/fs)*1000) RR_int_segm(counter+1:end)];
end
% Get new length
len = length(RR_int_segm);
% Adjust the large problem variable
large_prob = [large_prob 0]; %#ok
% Adjust the RR-ref all variable
RRref_all = [RRref_all 0]; %#ok
% Make sure that the new interval is checked
counter = counter-1;
else
% State that the interval is a large problem and should not
% be included for the reference intervals
large_prob(counter) = 1;
end
end
% Go one peak further
counter = counter + 1;
end
% Define the R-peak positions
RR_int_segm = RR_int_segm*fs/1000; % Go back to samples
R_peak_segm(2:length(RR_int_segm)+1) = round(cumsum(RR_int_segm) + R_peak_segm(1));
end
function R_peak_temp = peaks_in_ecg(signal,R_peak_temp,window)
% % Get the percentage of positive R-peaks
% perc_pos = sum(sign(signal(R_peak_temp)))/length(R_peak_temp);
for idx = 1:length(R_peak_temp)
% Get the samples from the R-peak minus the envelope length
% to the R-peak
p = signal(max(1, R_peak_temp(idx) - window) : R_peak_temp(idx));
% Do the same but for the time locations
t = max(1,R_peak_temp(idx)-window) : R_peak_temp(idx);
% Search for the maximum in p
[~,ip] = max(p);
% % Search for the extremum in p
% if perc_pos >= 0.5
% [~,ip] = max(p);
% else
% [~,ip] = min(p);
% end
% Adjust the R-peak
try
if signal(t(ip(end))) >= signal(t(ip(end))-1) && signal(t(ip(end))) >= signal(t(ip(end))+1)
R_peak_temp(idx) = t(ip(end));
end
catch
R_peak_temp(idx) = t(ip(end));
end
end
end