ECG-Kit 1.0

File: <base>/common/a2hbc/scripts/a2hbc_main.m (44,105 bytes)
function [ Labels, ConfusionMatrix, LabelList ] = a2hbc_main(varargin)

% A2HBC main script
% -----------------
% See a2hbc.m in the parent directory or the documentation for help.
% 
% Author: Mariano Llamedo Soria (llamedom at {electron.frba.utn.edu.ar; unizar.es}
% Birthdate  : 16/8/2011
% Last update: 9/2/2012

%% Constants and definitions
persistent userNoAnswers

if( isempty(userNoAnswers) )
    %first use
    userNoAnswers = 1;
end

a2hbc_header;

% some variable declarations;
Labels = [];
ConfusionMatrix = [];
LabelList = [];
true_labels = [];
bArgCheckPassed = false;
CantSamplesClose = 10;
CantSamplesFar = 10;
bUserExit = false;
bRecordingChanged = true;
bLabelingChanged = false;
repeat_idx = 1;

%% Argument parsing

%argument definition
p = inputParser;   % Create instance of inputParser class.
p.addParamValue('recording_name', [], @(x)( ischar(x)));
p.addParamValue('recording_format', [], @(x)( ischar(x) && any(strcmpi(x,cKnownFormats))) );
p.addParamValue('ECG', [], @(x)(isnumeric(x)) );
p.addParamValue('ECG_header', [], @(x)(isstruct(x)) );
p.addParamValue('ECG_annotations', [], @(x)( isstruct(x) ) );
p.addParamValue('op_mode', 'auto', @(x)( (isnumeric(x) && x >= 1  && x <= maxOperMode) || any(strcmpi(x,cKnownModesOfOperation))) );
p.addParamValue('cant_pids', 1, @(x)(isnumeric(x) && x > 0 ) );
p.addParamValue('this_pid', 1, @(x)(isnumeric(x) && x > 0 ) );
p.addParamValue('CacheData', true, @(x)(islogical(x)) );
p.addParamValue('InteractiveMode', false, @(x)(islogical(x)) );
p.addParamValue('SimulateExpert', false, @(x)(islogical(x)) );
p.addParamValue('tmp_path', [], @(x)(ischar(x)) );
p.addParamValue('NumOfClusters', 12, @(x)(isnumeric(x) && x > 1 ) );
p.addParamValue('ClusteringRepetitions', 1, @(x)(isnumeric(x) && x > 0 && x <= 10 ) );
p.addParamValue('ClusterPresence', 75, @(x)(isnumeric(x) && x >= 0 && x <= 100 ) );
p.addParamValue('Repetitions', 1, @(x)(isnumeric(x) && x > 0 ) );
%additions
p.addParamValue('class_labeling', 'AAMI', @(x)( ischar(x) && any(strcmpi(x,cKnownLabelings))) );
p.addParamValue('class_filter', char({'Normal', 'Supraventricular', 'Ventricular'}), @(x)( ischar(x) || iscell(x) ) );


try
    p.parse( varargin{:} );
catch MyError
    rethrow(MyError);    
end

recording_name = p.Results.recording_name;
recording_format = p.Results.recording_format;
ECG_total = p.Results.ECG;
ECG_header = p.Results.ECG_header;
ECG_annotations = p.Results.ECG_annotations;
bCache = p.Results.CacheData;
op_mode = p.Results.op_mode;
cant_pids = p.Results.cant_pids;
this_pid = p.Results.this_pid;
tmp_path = p.Results.tmp_path;
bInteractiveMode = p.Results.InteractiveMode;
bSimulateExpert = p.Results.SimulateExpert;
CantClusters = p.Results.NumOfClusters;
iter_times = p.Results.ClusteringRepetitions;
cluster_presence = p.Results.ClusterPresence;
Repetitions = p.Results.Repetitions;
class_labeling = p.Results.class_labeling;
class_filter = p.Results.class_filter;

% Dont know why this variable uses a lot of bytes to store at disk.
clear p

%% Start of the algorithm

if( bHaveUserInterface )
    if( isempty(varargin) )
        bInteractiveMode = true;
    end
else
    bInteractiveMode = false;
end

typical_lablist = char(typical_lablists{find(strcmpi(class_labeling, cKnownLabelings))});
typical_cant_labels = size(typical_lablist,1);
this_iter_cm = nan(typical_cant_labels);
ConfusionMatrix = zeros(typical_cant_labels, typical_cant_labels,Repetitions);

while ( ~bUserExit && repeat_idx <= Repetitions )

    try

        %% work starts here
        
        if( bRecordingChanged )
            % new recording, needs processing
            bRecordingChanged = false;
            
            %% Argument check

            % op_mode parsing
            if( isnumeric(op_mode) )
                op_mode = cKnownModesOfOperation{op_mode};
            end

            % class filtering parsing
            class_filter = char(intersect( cellstr(class_filter), cellstr(typical_lablist)));
            if( isempty(class_filter) )
                strAux = [ repmat(' + ', size(typical_lablist,1), 1) char(typical_lablist) repmat('\n', size(typical_lablist,1), 1 ) ];
                error( 'a2hbc:ArgCheck:InvalidClassFilter', ['Invalid class-filter. Please provide one of these classes:\n' rowvec(strAux')] );
            end

            bECG_sample_provided = false;
            
            %ECG parsing
            if( ~isempty(ECG_total) )
                % ECG already read
                
                bECG_sample_provided = true;

                
            elseif( ~isempty(recording_name) )

                [~, rec_filename] = fileparts(recording_name);

                % ECG to be read, create a wrapper object, later assign tasks to it.
                ECGw = ECGwrapper( ... 
                                    'recording_name', recording_name, ...
                                    'recording_format', recording_format, ...
                                    'tmp_path', tmp_path, ...
                                    'this_pid', sprintf('%d/%d', this_pid, cant_pids) ...
                                    );
                                
                ECG_header = ECGw.ECG_header;
                ECG_annotations = ECGw.ECG_annotations;
                
                % annotations are already AAMI, this is in order not to
                % convert again.
                recording_format = '';
            else
            %     strAux = help('a2hbc'); %#ok<MCHLP>
                error( 'a2hbc:ArgCheck:InvalidECGarg', 'Please provide an ECG recording as described in the documentation, help(''a2hbc'') maybe could help you.\n' );
            end
            
            if( isempty(ECG_header))
               error( 'a2hbc:ArgCheck:InvalidHeader', 'Please provide the ECG header.\n\n' );
            else
                if( ~isfield(ECG_header, cHeaderFieldNamesRequired ) )
                    strAux = [ repmat(' + ', length(cHeaderFieldNamesRequired), 1) char(cHeaderFieldNamesRequired) repmat('\n', length(cHeaderFieldNamesRequired), 1 ) ];
                    error( 'a2hbc:ArgCheck:InvalidHeader', ['Please provide the following fields in the header struct:\n ' rowvec(strAux') ] );
                end
            end

            if( isempty(ECG_annotations))
               error( 'a2hbc:ArgCheck:InvalidAnnotations', 'Please provide the ECG annotations.\n\n' );
            else
                if( ~isfield(ECG_annotations, cAnnotationsFieldNamesRequired ) )
                    strAux = [ repmat(' + ', length(cAnnotationsFieldNamesRequired), 1) char(cAnnotationsFieldNamesRequired) repmat('\n', length(cAnnotationsFieldNamesRequired), 1 ) ];
                    error( 'a2hbc:ArgCheck:InvalidAnnotations', ['Please provide the following fields in the annotations struct:\n ' rowvec(strAux') ] );
                end
            end

            [QRS_locations, aux_val ] = Annotation_process(ECG_annotations, recording_format, class_labeling);

            if( isempty(QRS_locations))
               error( 'a2hbc:ArgCheck:InvalidAnnotations', 'No QRS annotations available, please provide valid annotations or perform QRS detection.\n\n' );
            end
            
            true_labels = nan(size(aux_val));
            for ii = 1:length(typical_lablists_anntyp)
                bAux = aux_val == typical_lablists_anntyp{ii};
                true_labels(bAux) = ii;
            end

            rec_filename = ECG_header.recname;
                
            bLabelingChanged = false;

            lablist_idx = find(strcmpi(class_labeling, cKnownLabelings));

            %Update the automatic classifier.
            global_classifier = global_classifiers{lablist_idx};

            if( isempty(tmp_path) )
                tmp_path = tempdir;
            end

            %check path integrity.
            if(~exist(tmp_path, 'dir'))
                %try to create it
                if( ~mkdir(tmp_path) )
                    error('a2hbc:ArgCheck:InvalidPath', 'Invalid tmp_path. Please provide a valid path.\n' );
                end
            end

            bArgCheckPassed = true;

            

            %% Calculate accesories signals and coefficients
            
            if( this_pid > cant_pids )
                %% Classification only PIDs
                % Wait other PIDS sync here after Master build the featmat
                % file.

                %last pid
                bContinue = true;
                
                CachedFeatMatFiles = dir([tmp_path 'tmpfile_a2hbc_' ECGtask_class_fc_hdl.name '_' rec_filename '*.mat']);
                
                %Wait for Time2WaitPIDs seconds the finalization of all PIDs. Otherwise exit
                %with error.
                Start2Wait = tic();

                while(bContinue)

                    try

                        if( isempty( CachedFeatMatFiles ) )
                            error('a2hbc:PIDnotFinished', 'Handled error');
                        else                            
                            % exit the waiting loop.
                            bContinue = false;
                        end

                    catch ME


                        if( strfind(ME.identifier, 'a2hbc') )
                            if( toc(Start2Wait) > 2*Time2WaitPIDs )
                                error('a2hbc:PIDnotFinished', 'Timeout. Classification-only Slave give up waitng Master.');
                            end
                            pause(60);
                        else
                            rethrow(ME)
                        end
                    end


                end
                
            end

            % one task for calculating the PCA basis over the entire
            % recording
            ECGtask_PCA_proj_basis_hdl = ECGtask_PCA_proj_basis();

            % new task for calculating the features
            ECGtask_class_fc_hdl = ECGtask_classification_features_calc();
            
            %% ECG processing
            
            CachedFeatMatFiles = dir([tmp_path 'tmpfile_a2hbc_' ECGtask_class_fc_hdl.name '_' rec_filename '*.mat']);

            if( isempty( CachedFeatMatFiles ) )
                
                if( bECG_sample_provided )

                    aux_struct.time = QRS_locations;
                    aux_struct.anntyp = true_labels;
                    
                    if( ECG_header.nsig > 1  )
                        % As no wrapper is needed, I manually execute the task
                        ECGtask_PCA_proj_basis_hdl.Start(ECG_header, aux_struct);
                        ECGtask_PCA_proj_basis_hdl.Process(ECG_total, 1, [1 ECG_header.nsig], ECG_header, aux_struct, [1 length(QRS_locations) ] );
                        ECGtask_PCA_proj_basis_hdl.Finish([], ECG_header);
                    else
                        % PCA not need.
                        ECGtask_PCA_proj_basis_hdl.autovec = 1;
                    end

                    % Activate the progress_struct bar.
                    pb = progress_bar(rec_filename);
                    
                    ECGtask_class_fc_hdl.progress_handle = pb;
                    ECGtask_class_fc_hdl.autovec = ECGtask_PCA_proj_basis_hdl.autovec;
                    ECGtask_class_fc_hdl.Start(ECG_header, aux_struct);
                    payload = ECGtask_class_fc_hdl.Process(ECG_total, 1, [1 ECG_header.nsamp], ECG_header, aux_struct, [1 length(QRS_locations) ] ); %#ok<NASGU>

                    CachedFeatMatFiles = [tmp_path 'tmpfile_a2hbc_' ECGtask_class_fc_hdl.name '_' rec_filename '.mat'];
                    save(CachedFeatMatFiles, '-struct', 'payload');
                    clear payload
                    CachedFeatMatFiles = dir(CachedFeatMatFiles);
                    
                    delete(pb)
                    
                else

                    aux_struct.time = QRS_locations;
                    aux_struct.anntyp = true_labels;
                    ECGw.ECG_annotations = aux_struct;
                    
                    if( ECGw.ECG_header.nsig > 1  )
    %                     if( isempty(ECGw.ECG_annotations) )
    %                         ECGtask_PCA_proj_basis_hdl.cant_QRS_locations = 0;
    %                     else
    %                         ECGtask_PCA_proj_basis_hdl.cant_QRS_locations = length(ECGw.ECG_annotations.time);
    %                     end

                        ECGw.partition_mode =  'QRS';
                        ECGw.ECGtaskHandle = ECGtask_PCA_proj_basis_hdl;

                        %calculate the basis
                        ECGw.Run();
                    else
                        % PCA not need.
                        ECGtask_PCA_proj_basis_hdl.autovec = 1;
                    end

                    ECGw.class_labeling = class_labeling;
                    ECGtask_class_fc_hdl.autovec = ECGtask_PCA_proj_basis_hdl.autovec;

                    % assign the task to the wrapper.
                    ECGw.ECGtaskHandle = ECGtask_class_fc_hdl;
                    ECGw.partition_mode =  'QRS';

                    %% Iterations over the whole ECG recording
                    ECGw.Run;

                    CachedFeatMatFiles = [];
                    [tmp_path, CachedFeatMatFiles.name] = fileparts(ECGw.Result_files{1});
                    tmp_path = [tmp_path filesep];
                end
                
            end
            

            if( isempty( CachedFeatMatFiles ) )
                error( 'a2hbc:FeaturesNotCalc', 'Cached features files not found. Check files in the temporary dir.\n' );
            else
                
                % Activate the progress_struct bar.
                pb = progress_bar(rec_filename);
                
                % Update point
                pb.checkpoint('Restoring cached feature matrix');
                
                if( length(CachedFeatMatFiles) > 1 )
                    warning('TODO: generalizar para featmatrices arb grandes. Leyendo solo una parte de la feat matrix');
                    CachedFeatMatFileName = CachedFeatMatFiles(1).name;
                else
                    CachedFeatMatFileName = CachedFeatMatFiles.name;
                end
                
                % restore cached data.
                load( [ tmp_path CachedFeatMatFileName]);
                
            end

            if( bECG_sample_provided )
                ann = ECG_annotations;
            else
                ann = ECGw.ECG_annotations;
                aux_val = ann.anntyp;
                
                true_labels = nan(size(aux_val));
                for ii = 1:length(typical_lablists_anntyp)
                    bAux = aux_val == typical_lablists_anntyp{ii};
                    true_labels(bAux) = ii;
                end
                
            end
            
            % clear unused objects before continue
%             clear ECGw
%             clear ECGtask*
            
%           recorrer ECGw.Result_files y armar las matrices de features.

            %% Feature Matrices building


            %clean featureMatrix from NAN values.
%             AnyNaN_idx = find(any(isnan(featMat_clust),2));
%             iAllMedian = nanmedian(featMat_clust);
%             if(any(isnan(iAllMedian)))
%                error( 'a2hbc:AllNanFeatures', 'Features not calculated in this recording. Check recording or ask help.\n' );
%             end
%             for ii = rowvec(AnyNaN_idx)
%                bIdx2 = find(isnan(featMat_clust(ii,:)));
%                featMat_clust(ii, bIdx2) = iAllMedian(bIdx2);
%             end
% 
%             AnyNaN_idx = find(any(isnan(featMat_ldc),2));
%             iAllMedian = nanmedian(featMat_ldc);
%             if(any(isnan(iAllMedian)))
%                error( 'a2hbc:AllNanFeatures', 'Features not calculated in this recording. Check recording or ask help.\n' );
%             end
%             for ii = rowvec(AnyNaN_idx)
%                bIdx2 = find(isnan(featMat_ldc(ii,:)));
%                featMat_ldc(ii, bIdx2) = iAllMedian(bIdx2);
%             end

            % por el momento sigo usando el PRtools.
            % armo datasets.
            featMat_clust = prdataset(featMat_clust);
            featMat_ldc = prdataset(featMat_ldc);

            % Update point
            %clean featureMatrix from NAN values.
            pb.checkpoint('Clean featureMatrix from NAN values');
            
            featMat_ldc = deNaN_dataset(featMat_ldc, 'change');
            featMat_clust = deNaN_dataset(featMat_clust, 'change');
            
            featMat_ldc = deNaN_dataset(featMat_ldc, 'change', @isinf);
            featMat_clust = deNaN_dataset(featMat_clust, 'change', @isinf);
            

            if( bHaveUserInterface && (~exist('UCP_struct', 'var') || ~ishandle(UCP_struct.fig_hdl)) )
                UserControlPanel;
                ParseUserControlPanelInput;
            end

        else
            %% Only labeling changed ?
            
            if( bLabelingChanged )
                
                bLabelingChanged = false;
                
                % labeling changed
                [QRS_locations, aux_val ] = Annotation_process(ann, recording_format, class_labeling);

                true_labels = nan(size(aux_val));
                for ii = 1:length(typical_lablists_anntyp)
                    bAux = aux_val == typical_lablists_anntyp{ii};
                    true_labels(bAux) = ii;
                end
                
                %change lablists
                lablist_idx = find(strcmpi(class_labeling, cKnownLabelings));
                typical_lablist = char(typical_lablists{lablist_idx});
                typical_cant_labels = size(typical_lablist,1);
                this_iter_cm = nan(typical_cant_labels);
                ConfusionMatrix = zeros(typical_cant_labels, typical_cant_labels,Repetitions);
                
                %Update the automatic classifier.
                global_classifier = global_classifiers{lablist_idx};
                
            end
            
        end

        
        %% Classification

        cant_QRS_locations = size( featMat_ldc,1);
        
        already_labeled_idx = [];
        pending_hb_idx = setdiff((1:cant_QRS_locations)', already_labeled_idx );
        bCancel = false;

        % nasty addons
        RR_intervals = diff(QRS_locations, 1);
        RR_intervals = colvec([ RR_intervals(1); RR_intervals ]);
        
        if( ~bECG_sample_provided )
            ECG_total = ECGw.read_signal(1, ECGw.ECG_header.nsamp);
        end
        
        if( bHaveUserInterface )
            DisplayConfiguration;
        end
        
        while( ~bCancel && ~isempty(pending_hb_idx) )

            bDataSkiped = false;
            this_loop_already_labeled_idx = [];
            this_loop_cant_pending = length(pending_hb_idx);

            % Update point
            pb.checkpoint('Clustering data.');

            %skip warnings here
            warning off all; prwarning(0);
            Clust_Labels = cluster_data_with_EM_clust(featMat_clust(pending_hb_idx,:), qdc_new([],1e-6,1e-6, []), CantClusters, iter_times);
            warning on all; prwarning(1);

            [~, sort_idx] = sort( Clust_Labels );
            [all_clusters, aux_location] = unique(Clust_Labels(sort_idx,:), 'first');

            aux_location = [colvec(aux_location); cant_QRS_locations+1];
            cluster_sizes = diff(aux_location);
            cant_clusters = size(all_clusters,1);

            if( bHaveUserInterface)
                set(UCP_struct.Axes_hdl, 'Visible','on' );
%                 waitbar(0, UCP_struct.fig_hdl, 'Classification progress 0%')
                hb_percent_done = (length(already_labeled_idx) + length(this_loop_already_labeled_idx))/cant_QRS_locations;
                waitbar( hb_percent_done, UCP_struct.fig_hdl, [ 'Classification progress ' num2str(round(hb_percent_done*100)) '%' ])
            end
            
            %% Intento de clasificacion autom�tica.
            % Aqu� trabaja el GC.

            % Update point
            pb.checkpoint('Automatic classification');

            if( strcmpi(op_mode, 'slightly-assisted') || strcmpi(op_mode, 'auto') )

                dsThisPatient_classification = featMat_ldc * global_classifier;

                dsThisPatient_classification = labeld(dsThisPatient_classification);
                dsThisPatient_classification = renumlab(dsThisPatient_classification, typical_lablist); % try to fit on original lablist

                for ii = 1:cant_clusters

                    %los latidos de este cluster
                    aux_idx = sort_idx(aux_location(ii):(aux_location(ii+1)-1));

                    %busco las clasificaciones no rechazadas.
                    aux_idx1 = find(dsThisPatient_classification(aux_idx) ~= 0);

                    cantXclase = histc( dsThisPatient_classification(aux_idx(aux_idx1)), 1:typical_cant_labels );

                    %busco la clase mas predecida
                    [~, class_idx] = max(cantXclase);

                    if( cantXclase(class_idx) > ( cluster_presence  * 0.01 * cluster_sizes(ii) ) )
                        % La mayoria impone su clase.
                        Clust_Labels(aux_idx) = class_idx;
                        
                        this_loop_already_labeled_idx = [ this_loop_already_labeled_idx; colvec(aux_idx) ];

                        if( bHaveUserInterface)                            
                            hb_percent_done = (length(already_labeled_idx) + length(this_loop_already_labeled_idx))/cant_QRS_locations;
                            waitbar( hb_percent_done, UCP_struct.fig_hdl, [ 'Classification progress ' num2str(round(hb_percent_done*100)) '%' ])
                        end
                        
                    else
                        %for the slightly assisted, in the next section the
                        %expert will do the labeling.
                        if( strcmpi(op_mode, 'auto') )
                            %se deja la clasificacion autom�tica.
                            Clust_Labels(aux_idx) = dsThisPatient_classification(aux_idx);
                            
                            this_loop_already_labeled_idx = [ this_loop_already_labeled_idx; colvec(aux_idx) ];

                            if( bHaveUserInterface)                            
                                hb_percent_done = (length(already_labeled_idx) + length(this_loop_already_labeled_idx))/cant_QRS_locations;
                                waitbar( hb_percent_done, UCP_struct.fig_hdl, [ 'Classification progress ' num2str(round(hb_percent_done*100)) '%' ])
                            end                                
                            
                        end
                    end
                end                    
            end

            %% Clasificacion manual.
            % Aca consultamos al ORACLE.

            % Update point
            pb.checkpoint('Expert assistance');

            if( any(strcmpi(op_mode, {'assisted' 'slightly-assisted'}) ) )
                %Clustering + global classifier (GC) + oracle
                %Clustering + oracle
                %busco los centroides o algun elemento
                %representativo de cada cluster
                not_labeled_idx = setdiff(1:this_loop_cant_pending, this_loop_already_labeled_idx );
                [~, sort_idx] = sort( Clust_Labels(not_labeled_idx) );
                [all_clusters, aux_location] = unique(Clust_Labels(not_labeled_idx(sort_idx)), 'first');

                m_not_labeled = length(not_labeled_idx);

                aux_location = [colvec(aux_location); m_not_labeled+1];
                cluster_sizes = diff(aux_location);
                cant_clusters = size(all_clusters,1);

                ii = 1;
                while( ii <= cant_clusters )

                    %los latidos de este cluster
                    aux_idx = sort_idx(aux_location(ii):(aux_location(ii+1)-1));

                    %si es muy grande lo sampleo uniformemente para
                    %no generar mucho costo computacional.
                    laux_idx = length(aux_idx);
                    if( laux_idx > 5000 )
                        aux_idx2 = randsample(laux_idx, 5000);
                    else
                        aux_idx2 = 1:laux_idx;
                    end

                    %busco el centroide del cluster para
                    %etuiquetarlo.
                    clust_data_m = length(aux_idx2);
                    
                    if( clust_data_m > 1 )
                        
                        clust_data = [ featMat_clust(pending_hb_idx(not_labeled_idx(aux_idx(aux_idx2))),:); featMat_clust(pending_hb_idx(not_labeled_idx(aux_idx(aux_idx2))),:) ];
                        clust_data = setlabels(clust_data,[ones(clust_data_m,1);2*ones(clust_data_m,1)] );
                        clust_data = setprior(clust_data,[1 1]./2 );
                        clust_data_model = qdc(clust_data, 1e-6, 1e-6);
                        clust_data_posterior = clust_data * clust_data_model;
                        clust_dist2mu = +clust_data_posterior(1:clust_data_m,1);

                        [~, clust_dist2mu_sorted_idx ] = sort(clust_dist2mu, 'descend');

                    else
                        clust_dist2mu_sorted_idx = 1;
                    end
                    
                    if( ~bHaveUserInterface || bSimulateExpert )

                        clust_sample_centroid_idx = clust_dist2mu_sorted_idx(1);
                        %a cada cluster le asignamos la etiqueta
                        %VERDADERA del elemento mas cercano al centroide.
                        Clust_Labels(not_labeled_idx(aux_idx)) = true_labels(not_labeled_idx(aux_idx(aux_idx2(clust_sample_centroid_idx))));
                        
                        this_loop_already_labeled_idx = [ this_loop_already_labeled_idx; colvec(not_labeled_idx(aux_idx)) ];

                        if( bHaveUserInterface )
                            hb_percent_done = (length(already_labeled_idx) + length(this_loop_already_labeled_idx))/cant_QRS_locations;
                            waitbar( hb_percent_done, UCP_struct.fig_hdl, [ 'Classification progress ' num2str(round(hb_percent_done*100)) '%' ])
                        end
                        
                        ii = ii + 1;
                        
                    else
                        clust_sample_centroid_idx = clust_dist2mu_sorted_idx(1);
                        fprintf(1, ['Suggestion: ' typical_lablist(true_labels(not_labeled_idx(aux_idx(aux_idx2(clust_sample_centroid_idx)))),:) '\n']);
                        
                        %find heartbeat samples to show to the expert.
                        FindClusterExamples;

                        % Ask for expert opinion
                        [ Label bRefresh bCancel ] = ExpertUserInterface(cCentroid, cCloserExamples, cDistantExamples);

                        if(bCancel)
                            fprintf(2, 'Canceling current classification.\n');
                            if( bHaveUserInterface )                            
                                if( ishandle(UCP_struct.fig_hdl) )
                                    waitbar(0, UCP_struct.fig_hdl, 'Classification progress 0%')
                                    set(UCP_struct.Axes_hdl, 'Visible','off' );
                                end
                            end
                            break;
                        end

                        if( ~bRefresh  )

                            if( isempty(Label) )
                                bDataSkiped = true;
                                disp(['Skipping ' num2str(clust_data_m) ' heartbeats to re-cluster.'])
                            else
                                Clust_Labels(not_labeled_idx(aux_idx)) = find( strcmpi(cellstr(typical_lablist), Label) );
                                this_loop_already_labeled_idx = [ this_loop_already_labeled_idx; colvec(not_labeled_idx(aux_idx)) ];

                                hb_percent_done = (length(already_labeled_idx) + length(this_loop_already_labeled_idx))/cant_QRS_locations;
                                waitbar( hb_percent_done, UCP_struct.fig_hdl, [ 'Classification progress ' num2str(round(hb_percent_done*100)) '%' ])
                            end
                            ii = ii + 1;
                        end
                    end
                end
            end

            if( ~bCancel )

                %save the labeling done
                Labels(pending_hb_idx) = Clust_Labels;
                already_labeled_idx = [already_labeled_idx; colvec(pending_hb_idx(this_loop_already_labeled_idx))];
                pending_hb_idx = setdiff(1:cant_QRS_locations, already_labeled_idx );

                if(bHaveUserInterface)
                    if( bDataSkiped  )
                        %Maybe some fine tuning is necesary ...
                        if( ~exist('UCP_struct', 'var') || ~ishandle(UCP_struct.fig_hdl) )
                            UserControlPanel;
                        end

                        cant_hb_pending = length(pending_hb_idx);
                        fprintf(1, [num2str(cant_hb_pending) ' heartbeats remaining. Any fine tuning needed ? Change controls freely\n' ] );

                        if( cant_hb_pending < 90 ) % 10 * (k+1) * clusters
                            recommended_clusters = max(2, round(cant_hb_pending/90));
                            fprintf(2, [ 'Too few heartbeats. Recommended clusters: '  num2str(recommended_clusters) ]);
                            set(UCP_struct.CantClusters, 'Value', recommended_clusters);
                        end
                        
%                         set(UCP_struct.fig_hdl, 'Visible','on' );
                        EnableControPanel;

                        uiwait(UCP_struct.fig_hdl);
                        
                        if( ishandle(UCP_struct.fig_hdl) )
                            DisableControPanel;
%                             set(UCP_struct.fig_hdl, 'Visible','off' );
                        else
                            bCancel = true;
                            fprintf(2, 'Canceling current classification.\n');
                        end
                    end

                    %to update current user interaction.
                    ParseUserControlPanelInput;
                end
                
            end

        end

        %% Results generation

        if( bCancel )
            %Cancelation of expert input ends here.
            bCancel = false;
        else

            % Update point
            pb.checkpoint('Results generation');

            if( ~isempty(true_labels) )
                [~, this_iter_cm] = DisplayResults( 'TrueLabels', typical_lablist(true_labels,:), ...
                                                    'class_filter', class_filter, ...
                                                    'ClassifiedLabels', typical_lablist(Labels,:), ...
                                                    'ClassLabels', typical_lablist);
            else
                %not possible to build a confusion matrix without the true labels.
                this_iter_cm = nan(typical_cant_labels);
            end   
        end

        % All done
        if( bHaveUserInterface )
            if( ishandle(pb) )
                pb.checkpoint('Saving error report');
            end
        end
        
    catch MException
        %% Error handling

        if( bHaveUserInterface)
            %% with UI
            if( isempty(MException.identifier) || ~isempty(strfind(MException.identifier, 'a2hbc')) || ~isempty(strfind(MException.identifier, 'ECGwrapper'))  )
                %Our errors
                if( strfind(MException.identifier, 'ArgCheck') )
                    %Argument error, try other settings if user interface is
                    %available
                    fprintf(2, '%s', MException.message);
                    fprintf(2, '\nTry a different set of arguments with the user interface.\n');
                else
                    %other home-made errors. Make an educated exit ...
                    rethrow(MException)
                end

            else
                %% Other unknown errors -> Report it to me

                if( userNoAnswers <= maxNoAnswers && strcmpi( 'Yes', questdlg('Oh no! An unexpected error ocurred. Please take a minute to report it so I can fix it and improve A2HBC. Report error information through Internet ?', 'Unhandled error', 'Yes', 'No', 'Yes')) )
                    %report an error
                    if( ~exist('pb', 'var') )
                        % Activate the progress_struct bar.
                        pb = progress_bar(rec_filename);
                    end
                    pb.checkpoint('Saving error report');

                    timestamp = datestr(now, 'HH-MM-SS_dd-mm-yy');
                    report_filename = [tmp_path 'error_report_' timestamp '_' rec_filename '.mat' ];

                    %clean heavy variables
                    variables = whos();
                    [var_size, var_idx] = sort(cell2mat({variables(:).bytes}));
                    cum_var_size = cumsum(var_size);
                    last_var_idx = find( (cum_var_size/typical_compression_ratio) <= max_report_filesize, 1, 'last');
                    %save does not accept cell arrays as input -> workaround
                    var_names = {variables(var_idx(1:last_var_idx)).name};
                    feval( @save, report_filename, var_names{:} )

                    pb.checkpoint('Compressing');
                    
                    report_filename_gz = gzip( report_filename );
                    delete( report_filename );

                    pb.checkpoint('Reporting');

                    setpref('Internet','E_mail', 'a2hbc.errors@gmail.com');
                    setpref('Internet','SMTP_Server','smtp.gmail.com');
                    setpref('Internet','SMTP_Username','a2hbc.errors');
                    setpref('Internet','SMTP_Password', 'Any_password');
                    props = java.lang.System.getProperties;
                    props.setProperty('mail.smtp.auth','true');
                    props.setProperty('mail.smtp.socketFactory.class', 'javax.net.ssl.SSLSocketFactory');
                    props.setProperty('mail.smtp.socketFactory.port','465');   

%                     local_host = 'unknown';
%                     ip_address = 'unknown';
%                     location = 'unknown';
                    
                    %just to identify reports
                    local_host = java.net.InetAddress.getLocalHost;
                    
                    try
                        ip_address = urlread('http://automation.whatismyip.com/n09230945.asp');
                    catch dummyME
                        ip_address = '';
                    end
    
                    try
                        location = urlread('http://www.ipaddresslocation.org/my-ip-address.php');
                    catch dummyME
                        location = '';
                    end

                    computer_arch = computer();
                    computer_installation = ver();
                    lcomputer_installation = length(computer_installation);
                    tab_aux = cellstr(repmat('\t\t', lcomputer_installation, 1));
                    crlf_aux = repmat('\n', lcomputer_installation, 1);
                    computer_installation = strcat( colvec({computer_installation(:).Name}), tab_aux, colvec({computer_installation(:).Version}), tab_aux, colvec({computer_installation(:).Release}) );
                    computer_installation = [char(computer_installation) crlf_aux];
                    computer_installation = cellstr(sprintf(colvec(computer_installation')));

                    sendmail({'llamedom@unizar.es' 'llamedom@electron.frba.utn.edu.ar'}, ... %recipients
                            ['[A2HBC Error reporting] ' char(local_host)], ... %subject
                            [ cellstr(location) ; cellstr(ip_address) ; cellstr(computer_arch) ; computer_installation], ... %body
                            report_filename_gz); %attach

                    pb.checkpoint('Report sent successfully');

                    delete( char(report_filename_gz) );
                    
                    fprintf(1, '\n------------------------\nReport sent successfully\n------------------------\n\nThis is the error information:\n\n');
                    
                else                
                    %try to convince user for the next time.
                    fprintf( 2, '\nOk, thank you anyway. Please consider sending a report in case this error happens again.\n\n')
                    
                    if(userNoAnswers <= maxNoAnswers)
                        userNoAnswers = userNoAnswers + 1;
                    end
                end

                rethrow(MException);
            end
        else
            %% No User interface
            
% this is obsolete now since ECGwrapper takes care about error reporting
% 
%             fprintf(2,'\n\n')
%             fprintf(2,'###########\n')
%             fprintf(2,'## ERROR ##\n')
%             fprintf(2,'###########\n')
% 
%             fprintf(2,'Recording: %s (%d/%d) \n', recording_name, this_pid, cant_pids);
% 
%             strAux = GetFunctionInvocation(mfilename, varargin);
%             
%             local_host = getenv('HOSTNAME');
%             computer_arch = computer();
% 
%             fprintf(2,'Computer: %s (%s) \n', local_host, computer_arch);
%             
%             fprintf(2, '%s\n\n', strAux);
%             
%             report = getReport(MException);
%             fprintf(2, '%s', report);
%             fprintf(2,'###########\n')
%             fprintf(2,'## ERROR ##\n')
%             fprintf(2,'###########\n')
            
            rethrow(MException)

        end
        

    end

    ConfusionMatrix(:,:,repeat_idx) = this_iter_cm;
    
    if( bHaveUserInterface && ( ~bArgCheckPassed || bInteractiveMode ) )
        %% Ask User interaction
        % only if: the arguments were incorrect or I want to use a2hbc
        % in interactive mode.
        
        UserInteraction;

    else
        %If not user interface available -> unatended mode, just exit the loop.
        %bUserExit = true;
        repeat_idx = repeat_idx + 1;
    end

end

LabelList = typical_lablist;

if( bECG_sample_provided )
    delete([ tmp_path CachedFeatMatFileName])
end

%save results when running without user interface.
if( ~bHaveUserInterface )
    
    save([tmp_path 'tmpfile_' rec_filename '_results_' op_mode '_' num2str(CantClusters) '_' num2str(iter_times) '_' num2str(cluster_presence) '_' num2str(this_pid) '.mat' ], 'Labels', 'ConfusionMatrix', 'LabelList');
    
    % flag that the program ended correctly
    setenv('A2HBC_ES', '0');
end


%%%%%%%%%%%%%%%%%%
% Other functions
%%%%%%%%%%%%%%%%%%

function posterior = CalcLDCposterior(data, ldc_struct)

[ cant_classes k ] = size(ldc_struct.mean);

posterior = nan(size(data,1), cant_classes);

for ii = 1:cant_classes
    
    centered_data = bsxfun( @minus, data, ldc_struct.mean(ii,:));

    posterior(:,ii) = exp(-0.5*sum(centered_data'.*(ldc_struct.cov(:,:,ii)*centered_data'),1)' - (ldc_struct.det(ii) + k*log(2*pi))*0.5);

end

function RunNow(obj, event_obj)

fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
uiresume(fig_hnd); 

function SliderFunc(obj, event_obj)
fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
NewVal = round(get(obj , 'Value'));
set(obj , 'Value', NewVal );
set(user_data.SliderLabel , 'String', [ num2str(NewVal) ' Clusters'] );

function SliderFunc2(obj, event_obj)
fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
NewVal = round(get(obj , 'Value'));
set(obj , 'Value', NewVal );
strAux = [ num2str(NewVal) ' repetition'];
if(NewVal > 1)
    strAux= [strAux 's'];
end
set(user_data.SliderLabel2 , 'String', strAux );

function SliderFunc3(obj, event_obj)
fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
NewVal = round(get(obj , 'Value'));
set(obj , 'Value', NewVal );
set(user_data.SliderLabel3 , 'String', [ 'Cluster majority ' num2str(NewVal) ' %'] );

function UpdateClasses(obj, event_obj)
fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
Selection = get(obj , 'Value');
aux_val = get(user_data.class_filter , 'Value');
aux_str = get(user_data.class_filter , 'String');

class_filter = cellstr(aux_str(aux_val,:));

[~, aux_val] = intersect(user_data.typical_lablists{Selection}, class_filter );

set(user_data.class_filter , 'String', char(user_data.typical_lablists{Selection}) );
set(user_data.class_filter , 'Value', aux_val );


function OpModeSelect(obj, event_obj)
fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
Selection = get(obj , 'Value');

if( Selection > 2 )
    set(user_data.cluster_presence , 'Enable', 'off' );
else
    set(user_data.cluster_presence , 'Enable', 'on' );
end

function FormatCheck(obj, event_obj)
fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
NewVal = round(get(obj , 'Value'));
set(obj , 'Value', NewVal );
set(user_data.SliderLabel3 , 'String', [ 'Cluster majority ' num2str(NewVal) ' %'] );

function BrowseRecording(obj, event_obj)
fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
recording_name = get(user_data.recording_name , 'String');
[recording_path, ~, recording_ext] = fileparts(recording_name);
[recording_name, recording_path] = uigetfile([recording_path filesep '*.*'], 'Select a recording');
if( recording_name ~= 0 )
    set(user_data.recording_name , 'String', [ recording_path recording_name]);
end

function BrowseTMPpath(obj, event_obj)
fig_hnd = gcf();
user_data = get(fig_hnd , 'User');
tmp_path = get(user_data.tmp_path , 'String');
tmp_path = uigetdir(tmp_path, 'Select temporal directory');
if( tmp_path ~= 0 )
    set(user_data.tmp_path , 'String', tmp_path);
end

function PathCheck(obj, event_obj)

tmp_path = get(obj , 'String');

if( exist( tmp_path, 'dir' ) )
    set(obj , 'String', tmp_path)
    set(obj , 'Tag', tmp_path)
else
    fprintf(2, ['Path not found. ' tmp_path '\n'] )
    set(obj , 'String', get(obj , 'Tag') )
end

function RecordingCheck(obj, event_obj)

recording = get(obj , 'String');

if( exist( recording, 'file' ) )
    set(obj , 'String', recording)
    set(obj , 'Tag', recording)
else
    fprintf(2, ['File not found. ' recording '\n'] )
    set(obj , 'String', get(obj , 'Tag') )
end

function CheckNumeric(obj, event_obj)

number = get(obj , 'String');

if( isnan(str2double(number)) )
    fprintf(2, ['Not a valid numeric value: ' number '\n'] )
    set(obj , 'String', get(obj , 'Tag') )
else
    set(obj , 'String', number)
    set(obj , 'Tag', number)
end