EPHNOGRAM: A Simultaneous Electrocardiogram and Phonocardiogram Database 1.0.0

File: <base>/Code/TestHeartRateCalculation.m (9,260 bytes)
% EPHNOGRAM: A Simultaneous Electrocardiogram and Phonocardiogram Database
% A sample MATLAB script for reading the ECG-PCG data files and extracting the
% R-peaks from the ECG and the S1/S2 peaks of the PCG channels
%
% Dependencies: This script uses some functions from the Open-Source
% Electrophysiological Toolbox (OSET): https://github.com/alphanumericslab/OSET.git
%
% NOTE: This code has not been optimized for any specific dataset and is
% only provided as proof of concept and a starting point to work with the
% EPHNOGRAM database
%
% CITE:
% 1- The EPHNOGRAM on PhysioNet
% 2- A. Kazemnejad, P. Gordany, and R. Sameni. An open-access simultaneous electrocardiogram and phonocardiogram database. bioRxiv, 2021. DOI: https://doi.org/10.1101/2021.05.17.444563
% 3- R. Sameni, The Open-Source Electrophysiological Toolbox (OSET), v 3.14, URL: https://github.com/alphanumericslab/OSET
%
% By: Reza Sameni
% Email: reza.sameni@gmail.com
% May 2021
%

clear;
close all;
clc;

in_folder = '../MATfiles'; % .mat files folder
D = dir([in_folder '/*.mat']); % list of all mat files

% pre-processing parameters
fs = 1000.0; % post-decimation sampling frequency
for m = 1 : length(D) % NOTE: limit the index range to only analyze a subset of the records
    % LOAD DATA
    in_fname = D(m).name;
    in_fname_full = [in_folder '/' in_fname];
    dat = load(in_fname_full); % load the data file
    
    % DECIMATION
    % decimate the ECG and PCG channel to fs
    ECG = decimate(dat.ECG, round(dat.fs/fs));
    PCG = decimate(dat.PCG, round(dat.fs/fs));
    
    % NOTCH FILTERING THE ECG
    fc = 50.0; % powerline frequency
    Qfactor = 45; % Q-factor of the notch filter
    Wo = fc/(fs/2);  BW = Wo/Qfactor; % nothc filter parameters
    [b,a] = iirnotch(Wo, BW); % design the notch filter
    ECG = filtfilt(b, a, ECG); % zero-phase non-causal filtering
    
    % ECG PRE-PROCESSING
    fl_ECG = 10.0; % lower bandpass cut-off frequency for R-peaks
    fh_ECG = 40.0; % upper bandpass cut-off frequency for R-peaks
    ECG_hp = ECG - LPFilter(ECG, fl_ECG/fs);
    ECG_bp = LPFilter(ECG_hp, fh_ECG/fs);
    sigma = 5.0 * std(ECG_bp); % saturate peaks above k-sigma of the ECG
    ECG_saturated = sigma * tanh(ECG_bp / sigma);
    
    if(skew(ECG_saturated) > 0) % find the ECG polarity based on skewness sign
        ecg_polarity = 1;
    else
        ecg_polarity = 0;
    end
    
    % ECG R-PEAK DETECTION
    % First round of R-peak detection (fixed window length peak search)
    ff0 = 1.4; % approximate heart rate in Hz (just a rough estimate; will be fine-tuned by the algorithm)
    peaks0 = PeakDetection(ECG_saturated, ff0/fs, ecg_polarity);
    I0 = find(peaks0);
    ff1 = median(fs ./ diff(I0));
    
    % Second round of R-peak detection (adaptive window length peak search)
    peaks_ECG = PeakDetectionAdaptiveHR(ECG_saturated, ff1, fs, ecg_polarity);
    I_ECG_peaks = find(peaks_ECG);
    RR_intervals_ecg = diff(I_ECG_peaks); % RR-interval time series in samples
    ff_ecg = fs ./ RR_intervals_ecg;
    HR_ecg = 60.* ff_ecg; % ECG-based heart rate
    
    % PCG PRE-PROCESSING
    fl_PCG = 10.0; % lower bandpass cut-off frequency for PCG peaks
    fh_PCG = 100.0; % upper bandpass cut-off frequency for PCG peaks
    PCG_hp = PCG - LPFilter(PCG, fl_PCG/fs);
    PCG_bp = LPFilter(PCG_hp, fh_PCG/fs);
    wlen = round(0.015 * fs); % PCG envelope detector window length
    PCG_envelope = sqrt(filtfilt(ones(1, wlen), wlen, PCG_bp.^2));
    
    % PCG PEAK DETECTION
    search_wlen = round(0.15 * fs);
    % find two dominant local peaks in the PCG envelope between successive
    % ECG R-peaks (gives both S1 and S2)
    num_peaks = 2;
    peaks_S1S2_PCG = IntermediatePeakDetection(PCG_envelope, peaks_ECG, search_wlen, num_peaks, 1, 1);
    % find the first dominant local peak in the PCG envelope between
    % successive ECG R-peaks (gives S1 only)
    num_peaks = 1;
    peaks_S1_PCG = IntermediatePeakDetection(PCG_envelope, peaks_ECG, search_wlen, num_peaks, 1, 2);
    
    I_PCG_S1_peaks = find(peaks_S1_PCG);
    S1S1_intervals_pcg = diff(I_PCG_S1_peaks); % S1S1-interval time series in samples
    ff_pcg = fs ./ S1S1_intervals_pcg;
    HR_pcg_S1 = 60.* ff_pcg; % PCG-based heart rate based on S1S1 time intervals
    
    peaks_S2_PCG = abs(peaks_S1S2_PCG - peaks_S1_PCG); % find the S2 peaks (exclude S1 from the two-peak time sequence)
    I_PCG_S2_peaks = find(peaks_S2_PCG);
    S2S2_intervals_pcg = diff(I_PCG_S2_peaks);  % S2S2-interval time series in samples
    ff_pcg_S2 = fs ./ S2S2_intervals_pcg;
    HR_pcg_S2 = 60.* ff_pcg_S2; % PCG-based heart rate based on S2S2 time intervals
    
    
    % INTERPOLATE THE HEART RATE SEQUENCES
    t = (0 : length(ECG)-1)/fs;
    % Add start and end points to the HR time sequences before interpolation
    tt_ecg = t;
    if(t(I_ECG_peaks(1)) > t(1))
        tt_ecg = cat(2, t(1), tt_ecg(I_ECG_peaks));
        RR_intervals_ecg = cat(2, RR_intervals_ecg(1), RR_intervals_ecg(1), RR_intervals_ecg);
    end
    if(t(I_ECG_peaks(end)) < t(end))
        tt_ecg = cat(2, tt_ecg, t(end));
        RR_intervals_ecg = cat(2, RR_intervals_ecg, RR_intervals_ecg(end));
    end
    tt_pcg_S1 = t;
    if(t(I_PCG_S1_peaks(1)) > t(1))
        tt_pcg_S1 = cat(2, t(1), tt_pcg_S1(I_PCG_S1_peaks));
        S1S1_intervals_pcg = cat(2, S1S1_intervals_pcg(1), S1S1_intervals_pcg(1), S1S1_intervals_pcg);
    end
    if(t(I_PCG_S1_peaks(end)) < t(end))
        tt_pcg_S1 = cat(2, tt_pcg_S1, t(end));
        S1S1_intervals_pcg = cat(2, S1S1_intervals_pcg, S1S1_intervals_pcg(end));
    end
    tt_pcg_S2 = t;
    if(t(I_PCG_S2_peaks(1)) > t(1))
        tt_pcg_S2 = cat(2, t(1), tt_pcg_S2(I_PCG_S2_peaks));
        S2S2_intervals_pcg = cat(2, S2S2_intervals_pcg(1), S2S2_intervals_pcg(1), S2S2_intervals_pcg);
    end
    if(t(I_PCG_S2_peaks(end)) < t(end))
        tt_pcg_S2 = cat(2, tt_pcg_S2, t(end));
        S2S2_intervals_pcg = cat(2, S2S2_intervals_pcg, S2S2_intervals_pcg(end));
    end
    
    % interpolate the HRs
    RR_interval_ecg_interpolated = interp1(tt_ecg, RR_intervals_ecg, t);
    S1S1_interval_pcg_interpolated = interp1(tt_pcg_S1, S1S1_intervals_pcg, t);
    S2S2_interval_pcg_interpolated = interp1(tt_pcg_S2, S2S2_intervals_pcg, t);
    RR_S1S1_diff = (RR_interval_ecg_interpolated - S1S1_interval_pcg_interpolated) / fs;
    RR_S2S2_diff = (RR_interval_ecg_interpolated - S2S2_interval_pcg_interpolated) / fs;
    beta = 0.05; % saturate differences above this amound of time (s)
    RR_S1S1_diff_sat = beta * tanh(RR_S1S1_diff / beta);
    RR_S2S2_diff_sat = beta * tanh(RR_S2S2_diff / beta);
    
    HR_ecg_interpolated = interp1(t(I_ECG_peaks), [HR_ecg(1), HR_ecg], t);
    HR_pcg_S1_interpolated = interp1(t(I_PCG_S1_peaks), [HR_pcg_S1(1), HR_pcg_S1], t);
    HR_pcg_S2_interpolated = interp1(t(I_PCG_S2_peaks), [HR_pcg_S2(1), HR_pcg_S2], t);
    HR_ecg_pcg_S1_diff = HR_ecg_interpolated - HR_pcg_S1_interpolated;
    HR_ecg_pcg_S2_diff = HR_ecg_interpolated - HR_pcg_S2_interpolated;
    alpha = 10.0; % saturate differences above this number of BPMs
    HR_ecg_pcg_S1_diff_sat = alpha * tanh(HR_ecg_pcg_S1_diff / alpha);
    HR_ecg_pcg_S2_diff_sat = alpha * tanh(HR_ecg_pcg_S2_diff / alpha);
    
    % PLOT THE RESULTS
    figure
    lg = {};
    subplot(511);
    plot(t, ECG); lg = cat(2, lg, {'ECG'});
    hold on
    plot(t, ECG_saturated); lg = cat(2, lg, {'saturated ECG'});
    plot(t, PCG); lg = cat(2, lg, {'PCG'});
    plot(t, PCG_envelope); lg = cat(2, lg, {'PCG envelope'});
    plot(t(I_ECG_peaks), ECG(I_ECG_peaks), 'ro'); lg = cat(2, lg, {'R-peaks'});
    plot(t(I_PCG_S1_peaks), PCG_envelope(I_PCG_S1_peaks), 'gx'); lg = cat(2, lg, {'S1-peaks'});
    plot(t(I_PCG_S2_peaks), PCG_envelope(I_PCG_S2_peaks), 'kx'); lg = cat(2, lg, {'S2-peaks'});
    legend(lg);
    grid
    
    subplot(512);
    lg = {};
    plot(t(I_ECG_peaks), [HR_ecg(1), HR_ecg]); lg = cat(2, lg, {'ECG-based HR'});
    hold on
    plot(t(I_PCG_S1_peaks), [HR_pcg_S1(1), HR_pcg_S1]); lg = cat(2, lg, {'S1-based HR'});
    plot(t(I_PCG_S2_peaks), [HR_pcg_S2(1), HR_pcg_S2]); lg = cat(2, lg, {'S2-based HR'});
    legend(lg);
    ylabel('BPM');
    grid
    
    subplot(513);
    lg = {};
    plot(t, HR_ecg_interpolated); lg = cat(2, lg, {'ECG-based HR'});
    hold on
    plot(t, HR_pcg_S1_interpolated); lg = cat(2, lg, {'S1-based HR'});
    plot(t, HR_pcg_S2_interpolated); lg = cat(2, lg, {'S2-based HR'});
    legend(lg)
    grid
    ylabel('Interpolated HRs (BPM)');
    
    subplot(514);
    lg = {};
    plot(t, HR_ecg_pcg_S1_diff_sat); lg = cat(2, lg, {'RR-S1S1'});
    hold on
    plot(t, HR_ecg_pcg_S2_diff_sat); lg = cat(2, lg, {'RR-S2S2'});
    legend(lg)
    grid
    ylabel('RR-SS HR diff saturated (BPM)');
    
    subplot(515);
    lg = {};
    plot(t, 1000 * RR_S1S1_diff_sat); lg = cat(2, lg, {'RR-S1S1'});
    hold on
    plot(t, 1000 * RR_S2S2_diff_sat); lg = cat(2, lg, {'RR-S1S1'});
    legend(lg)
    grid
    ylabel('RR-SS diff saturated (ms)');
    
    % NOTE: Add a keyboard hit or pause here to check the results!
end