R-DECO: An open-source Matlab based graphical user interface for the detection and correction of R-peaks 1.0.0

File: <base>/R_peak_detection/Algorithm/peak_detection.m (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