gusucode.com > vision工具箱matlab源码程序 > vision/+vision/+internal/+cascadeTrainer/+tool/TrainingDataLabelerTool.m

    % TrainingDataLabelerTool Main class for the trainingImageLabeler App
%
%    This object implements the core routines in trainingImageLabeler App.
%    All the callbacks that you see in the UI are implemented below.

classdef TrainingDataLabelerTool < vision.internal.uitools.ToolStripApp
    
    properties
        ShowROILabels = true;
    end
    
    properties(Access = 'private')
        
        SplitPanel;
        
        % Tool group management
        LabelingTab
        
        % UI state
        
        ImageDisplay = [];
        MaxNumIcons     % Takes a maximum number of image icons that can be shown in the full sized UI
        
        % The item below is just a dummy cache for storing Java related
        % items that would otherwise break by going out of scope
        Misc
        
        DataBrowser
        % Handle to the Java's JList which holds all the boards. It must
        % be available throughout the class so that one can obtain the
        % currently selected board
        JImageList
        ImageStrip
        
        MCategoryStrip
        
        OpenSessionPath;
        
        % variables needed to control HG/Java synchronization
        IsInteractionDisabledByScrollCallback = false;
        IsBrowserInteractionEnabled = true;
    end
    
    %======================================================================
    
    %----------------------------------------------------------------------
    % Public methods
    %----------------------------------------------------------------------
    methods (Access = 'public')
        
        %------------------------------------------------------------------
        function this = TrainingDataLabelerTool()
            import vision.internal.cascadeTrainer.*;
            
            % generate a name for this tool; we need a unique string for
            % each instance
            [~, name] = fileparts(tempname);
            title = vision.getMessage('vision:trainingtool:ToolTitle');
            this.ToolGroup = toolpack.desktop.ToolGroup(name, title);
            
            this.LabelingTab = tool.LabelingTab(this);
            add(this.ToolGroup, getToolTab(this.LabelingTab), 1);
            
            this.SessionManager = ...
                vision.internal.cascadeTrainer.tool.TrainingImageLabelerSessionManager;
            
            this.Session = vision.internal.cascadeTrainer.tool.Session; % initialize the session object
            
            this.MaxNumIcons = 13;
            
            this.setupDataBrowser();
            
            % handle closing of the tool group
            this.setClosingApprovalNeeded(true);
            addlistener(this.ToolGroup, 'GroupAction',...
                @(es,ed)doClosingSession(this, es, ed));
            
            % manageToolInstances
            this.addToolInstance();
            
            % set the path for opening sessions to the current directory
            this.OpenSessionPath = pwd;

            this.updateCategoryStrip();
            
            this.setSelectedCategoryIndex(1);
            
            this.ToolGroup.Title = getString(message(...
                'vision:trainingtool:ToolTitleWithSession', ...
                this.SessionManager.DefaultSessionFileName));            
        end
        
        %------------------------------------------------------------------
        function show(this)
            
            this.removeViewTab();
            this.removeDocumentTabs();
            
            % open the tool
            this.ToolGroup.open();
            
            % create figures and lay them out the way we want them
            imageslib.internal.apputil.ScreenUtilities.setInitialToolPosition(this.getGroupName());
                        
            this.setupImageDisplay();
            
            % update button states to indicate the tool's current state
            this.updateButtonStates();
            
            drawnow();
        end
        
        
    end % public methods
    
    %======================================================================
    
    methods (Access = 'public', Hidden)
        function conditionallyEnableBrowserIneraction(this, tf)
            if ~this.IsInteractionDisabledByScrollCallback
                this.setBrowserInteractionEnabled(tf);
            end
        end
        
        %------------------------------------------------------------------
        % New session button callback
        %------------------------------------------------------------------
        function newSession(this)
            
            % First check if we need to save anything before wiping the
            % existing data
            isCanceled = this.processSessionSaving();
            if isCanceled
                return;
            end
            
            % Wipe the UI clean
            this.resetAll();
            
            % Update Button states
            this.updateButtonStates();
            
            % Update Status text
            this.setStatusText();
            
            this.ToolGroup.Title = getString(message(...
                'vision:trainingtool:ToolTitleWithSession', ...
                this.SessionManager.DefaultSessionFileName));
        end
        
        
        %------------------------------------------------------------------
        % Open session button callback
        %------------------------------------------------------------------
        function openSession(this)
            
            % First check if we need to save anything before we wipe
            % existing data
            isCanceled = this.processSessionSaving();
            if isCanceled
                return;
            end
            
            trainingFilesString = vision.getMessage...
                ('vision:trainingtool:LabelingSessionFiles');
            allFilesString = vision.getMessage('vision:uitools:AllFiles');
            selectFileTitle = vision.getMessage('vision:uitools:SelectFileTitle');
            
            [filename, pathname] = uigetfile( ...
                {'*.mat', [trainingFilesString,' (*.mat)']; ...
                '*.*', [allFilesString, ' (*.*)']}, ...
                selectFileTitle, this.OpenSessionPath);
            
            wasCanceled = isequal(filename,0) || isequal(pathname,0);
            if wasCanceled
                return;
            else
                % preserve the last path for next time
                this.OpenSessionPath = pathname;
            end
            
            % Indicate that this is going to take some time
            setWaiting(this.ToolGroup, true);
            
            preserveExistingSession = false;
            
            this.processOpenSession(pathname, filename, preserveExistingSession);
            
            this.setStatusText();
            
            setWaiting(this.ToolGroup, false);
            
            this.ToolGroup.Title = getString(message(...
                'vision:trainingtool:ToolTitleWithSession', this.Session.FileName));
        end
        
        
        %------------------------------------------------------------------
        function importROIsFromWorkspace(this)
            % First check if we need to save anything before we wipe
            % existing data
            isCanceled = this.processSessionSaving();
            if isCanceled
                return;
            end
            
            
            [ROIs, varName, isCanceled] = vision.internal.cascadeTrainer.tool.roigetvar();
            if isCanceled
                return;
            end
            
            try
                session = vision.internal.cascadeTrainer.tool.Session(...
                    ROIs, '', '');                
            catch
                errordlg(...
                    getString(message('vision:trainingtool:UnableLoadROIMsg', varName)),...
                    getString(message('vision:trainingtool:UnableLoadROITitle')),...
                    'modal');
                return;
            end
            session.ExportVariableName = varName;
            preserveExistingSession = false;
            this.loadSession(session, preserveExistingSession);            
            
            this.ToolGroup.Title = getString(message(...
                'vision:trainingtool:ToolTitle'));            
        end
        
        %------------------------------------------------------------------
        % Add multiple sessions to current session button callback
        % Not yet implemented fully (wait to see if this is a needed
        % feature
        %------------------------------------------------------------------
        function addToCurrentSession(this)
            
            trainingFilesString = vision.getMessage...
                ('vision:trainingtool:LabelingSessionFiles');
            allFilesString = vision.getMessage('vision:uitools:AllFiles');
            selectFileTitle = vision.getMessage('vision:uitools:SelectFileTitle');
            
            [filename, pathname] = uigetfile( ...
                {'*.mat', [trainingFilesString,' (*.mat)']; ...
                '*.*', [allFilesString, ' (*.*)']}, ...
                selectFileTitle, this.OpenSessionPath);
            
            wasCanceled = isequal(filename,0) || isequal(pathname,0);
            if wasCanceled
                return;
            else
                % preserve the last path for next time
                this.OpenSessionPath = pathname;
            end
            
            setWaiting(this.ToolGroup, true);
            
            preserveExistingSession = true;
            
            this.processOpenSession(pathname, filename, preserveExistingSession);
            
            this.setStatusText();
            
            setWaiting(this.ToolGroup, false);
            
        end
        
        %------------------------------------------------------------------
        % Save session button callback
        %------------------------------------------------------------------
        function saveSession(this, fileName)
            % If we didn't save the session before, ask for the filename
            if nargin < 2
                if isempty(this.Session.FileName)
                    fileName = vision.internal.uitools.getSessionFilename(...
                        this.SessionManager.DefaultSessionFileName);
                    if isempty(fileName)
                        return;
                    end
                else
                    fileName = this.Session.FileName;
                end
            end
            
            this.SessionManager.saveSession(this.Session, fileName);
            
            this.ToolGroup.Title = getString(message(...
                'vision:trainingtool:ToolTitleWithSession', this.Session.FileName));
        end
        
        %------------------------------------------------------------------
        function saveSessionAs(this)
            fileName = vision.internal.uitools.getSessionFilename(...
                this.SessionManager.DefaultSessionFileName);
            if ~isempty(fileName)
                this.saveSession(fileName);
            end
            
            this.ToolGroup.Title = getString(message(...
                'vision:trainingtool:ToolTitleWithSession', this.Session.FileName));
        end
        
        %------------------------------------------------------------------
        % Add images button callback
        %------------------------------------------------------------------
        function addImages(this)
            persistent imageDir;
            
            if isempty(imageDir) || ~exist(imageDir, 'dir')
                imageDir = pwd();
            end
            
            % Get image file names
            [files, isUserCanceled] = imgetfile('MultiSelect', true, ...
                'InitialPath', imageDir);
            if isUserCanceled || isempty(files)
                return;
            end
            
            imageDir = fileparts(files{1});
            addImagesToSession(this, files);            
        end
        
        %------------------------------------------------------------------
        % Add Category button callback
        %------------------------------------------------------------------
        
        function addCategory(this)            
            categoryDlg = vision.internal.cascadeTrainer.tool.CategoryDlg(...
                this.getGroupName(), 'add');
            wait(categoryDlg);
            
            % Close up the modal dialog
            if ~categoryDlg.IsCanceled
                varName = categoryDlg.VarName;
                startingIndex = this.Session.CategorySet.addCategoryToSession(varName);
                this.Session.IsChanged = true;
                
                this.updateCategoryStrip();
                
                this.setSelectedCategoryIndex(startingIndex);
                this.makeCategorySelectionVisible(startingIndex);
                
            end
            
            drawnow;
        end
        
        
        %------------------------------------------------------------------
        % Add images to a session
        %------------------------------------------------------------------
        function addImagesToSession(this, files)
            
            % If a single file is selected convert the string into a cell
            % array before passing it to the Session object
            if ~isa(files, 'cell')
                files = {files};
            end
            
            try
                setWaiting(this.ToolGroup, true);
                
                % Add images to the session object
                startingIndex = this.Session.ImageSet.addImagesToSession(files);
                if isempty(startingIndex)
                    setWaiting(this.ToolGroup, false);
                    dlg = warndlg(...
                        getString(message('vision:uitools:NoImagesAddedMessage')),...
                        getString(message('vision:uitools:NoImagesAddedTitle')),...
                        'modal');
                    uiwait(dlg);
                    return; % This would indicate presence of duplicates
                end
                
                this.Session.IsChanged = true;
                
                if ~this.Session.hasAnyImages()
                    setWaiting(this.ToolGroup, false);
                    errordlg(...
                        getString(message('vision:trainingtool:NoValidImagesFoundMsg')),...
                        getString(message('vision:uitools:LoadingImagesFailedTitle')),...
                        'modal');
                    drawnow;
                    return;
                end
                
                this.updateImageStrip();
                
                % Update selection in the list
                this.setSelectedImageIndex(startingIndex);
                this.makeSelectionVisible(startingIndex);                
                
                % Manage the image strip
                this.updateImageStrip();
                
                drawnow();                
                
                % Update session state
                this.Session.IsChanged = true;
                this.Session.CanExport = this.getExportStatus();
                this.updateButtonStates();
                
                % Update displays
                this.drawImages();
                this.setStatusText();
                
                setWaiting(this.ToolGroup, false);
                
            catch loadingEx
                
                if ~isvalid(this)
                    % we already went through delete sequence; this can
                    % happen if the images did not yet load and someone
                    % already closed the tool
                    return;
                end
                
                setWaiting(this.ToolGroup, false); % if it errors out set the toolgroup busy to false
                
                errordlg(loadingEx.message,...
                    vision.getMessage('vision:uitools:LoadingImagesFailedTitle'),...
                    'modal');
                return;
            end
            
        end
        
        
        
        
        %------------------------------------------------------------------
        % Zoom In Button callback
        %------------------------------------------------------------------
        function doZoomIn(this, varargin)
            this.ImageDisplay.doZoomIn(varargin{:});
             this.setFocusOnImages();
        end
        
        %------------------------------------------------------------------
        % Zoom Out buttons callback
        %------------------------------------------------------------------
        function doZoomOut(this, varargin)
            this.ImageDisplay.doZoomOut(varargin{:});
            this.setFocusOnImages();
        end
        
        %------------------------------------------------------------------
        % Pan Button callback
        %------------------------------------------------------------------
        function doPan(this, varargin)
            this.ImageDisplay.doPan(varargin{:});
            this.setFocusOnImages();
        end
        
        %------------------------------------------------------------------
        % Add ROIs callback
        %------------------------------------------------------------------
        function doAddROI(this, varargin)
            this.ImageDisplay.doAddROI(varargin{:});
            this.setFocusOnImages();
        end
        
        %------------------------------------------------------------------
        % Export button callback
        %------------------------------------------------------------------
        function export(this)
            this.updateSessionObject();
            if ~this.Session.ImageSet.areAllImagesLabeled()
                choice = questdlg(vision.getMessage('vision:trainingtool:UnlabeledImagesPrompt'),...
                    vision.getMessage('vision:trainingtool:UnlabeledImagesTitle'),...
                    vision.getMessage('vision:trainingtool:ExportROIs'),...
                    vision.getMessage('vision:trainingtool:ContinueLabeling'),...
                    vision.getMessage('vision:trainingtool:ContinueLabeling'));
                % Handle of the dialog is destroyed by the user
                % closing the dialog or the user pressed cancel
                if isempty(choice) || ...
                        strcmp(choice, vision.getMessage('vision:trainingtool:ContinueLabeling'))
                    return;
                end
            end
            
            varName = this.Session.ExportVariableName;
            exportDlg = vision.internal.cascadeTrainer.tool.ExportDlg(...
                this.getGroupName, varName);
            
            if this.Session.NumCategories > 1
                exportDlg.disableFormat();
            end
            
            wait(exportDlg);
            if ~exportDlg.IsCanceled
                varName = exportDlg.VarName;
                format = exportDlg.VarFormat;
                if isequal(format, getString(message('vision:trainingtool:StructFormat')))
                    catID   = this.Session.CategorySet.CategoryStruct(1).categoryID;
                    this.Session.ImageSet.setROIBoundingBoxesForCategory(catID);
                    outputVar = this.Session.ImageSet.ROIBoundingBoxes;
                    for i = 1:numel(outputVar)
                        outputVar(i).objectBoundingBoxes = round(outputVar(i).objectBoundingBoxes);
                    end
                    assignin('base', varName, outputVar);
                else
                    labelTable = this.Session.getLabelTable();
                    assignin('base', varName, labelTable);
                    % display the parameters at the command prompt
                end
                evalin('base', varName);
            end
            drawnow;
        end
        
        %------------------------------------------------------------------
        % Help button callback
        %------------------------------------------------------------------
        function help(~)
            
            mapfile_location = fullfile(docroot,'toolbox',...
                'vision','vision.map');
            doc_tag = 'visionTrainingImageLabeler';
            
            helpview(mapfile_location, doc_tag);
        end
        
        %------------------------------------------------------------------
        function deleteToolInstance(this)
            imageslib.internal.apputil.manageToolInstances('remove',...
                'trainingImageLabeler', this);
            delete(this);
        end
        
        %------------------------------------------------------------------
        function addToolInstance(this)
            imageslib.internal.apputil.manageToolInstances('add',...
                'trainingImageLabeler', this);
        end
        
        %------------------------------------------------------------------
        % This method is used for testing
        %------------------------------------------------------------------
        function setClosingApprovalNeeded(this, in)
            this.ToolGroup.setClosingApprovalNeeded(in);
        end
        
        %------------------------------------------------------------------
        function processOpenSession(this, pathname, filename,...
                preserveExistingSession)
            

            session = this.SessionManager.loadSession(pathname, filename);
            if isempty(session)
                return;
            end
            
            loadSession(this, session, preserveExistingSession);
        end

        %------------------------------------------------------------------
        function loadSession(this, session, preserveExistingSession)
            
            isNewSession = false;
                     
            if ~preserveExistingSession
                this.resetAll();  % Start fresh
                this.Session.FileName = session.FileName;
                isNewSession = true;
            end
            
            % use the loaded session
            if ~this.Session.ImageSet.hasAnyImages()
                this.Session = session;
                selectedIndex = 1;
            else
                % index of the first element of the added session
                selectedIndex = numel(this.Session.ImageSet.ImageStruct)+1; % matlab based index
                categoryStruct = session.CategorySet.CategoryStruct;
                count = this.Session.CategorySet.compareCategoryStruct(categoryStruct);
                
                % count = index means the new category gets mapped to the
                % existing category, count = 0 means there is a conflict in
                % the color and the color gets changed for the new session
                % category (warn), count = -1, add this category to the new
                % list
                
                if any(count==0)
                    dlg = warndlg(...
                        getString(message('vision:trainingtool:DuplicateColorPromptMessage')),...
                        getString(message('vision:trainingtool:DuplicateColorPromptTitle')),...
                        'modal');
                    uiwait(dlg);
                end
                
                imagesCatID = {session.ImageSet.ImageStruct.catID};
                imagesCatID = cellfun(@(x) zeros(size(x)), imagesCatID, 'UniformOutput', false);
                for i = 1:numel(count)
                    origIndex = categoryStruct(i).categoryID;
                    varName = categoryStruct(i).categoryName;
                    if(count(i)==0)
                        [~,newIndex] = this.Session.CategorySet.addCategoryToSession(varName);
                        imagesCatID = session.replaceCategoryIndex(origIndex,...
                            newIndex, imagesCatID);
                        continue;
                    end
                    
                    if(count(i)==-1)
                        color = categoryStruct(i).categoryColor;
                        [~,newIndex] = this.Session.CategorySet.addCategoryToSession(varName,color);
                    else
                        newIndex = count(i);
                    end
                    imagesCatID = session.replaceCategoryIndex(origIndex, ...
                        newIndex, imagesCatID);
                end
                
                s = session.ImageSet;
                [s.ImageStruct.catID] = imagesCatID{:};
                session.ImageSet = s;
                
                addedImages = this.Session.ImageSet.addImageStructToCurrentSession(...
                    session.ImageSet.ImageStruct, ...
                    session.ImageSet.AreThumbnailsGenerated);
                % Return if no new images are added
                if ~addedImages
                    return;
                end
                
                this.Session.IsChanged = true;
            end
            
            if ~isempty(this.Session.ImageSet.ImageStruct)
                this.updateImageStrip(); % Restore image strip
                this.updateCategoryStrip();
                
                idx = 1;
                this.MCategoryStrip.setSelectedCategoryIndex(idx);
                this.setSelectedImageIndex(selectedIndex);
                this.makeSelectionVisible(selectedIndex);
                
                this.drawImages(); % Display the first image on the list
                this.updateImageStrip(); % Restore image stripclear classes
                
            end
            
            this.Session.CanExport = this.getExportStatus();
            this.updateButtonStates();
            if isNewSession
                this.Session.IsChanged = false;
            end
        end
        
        %------------------------------------------------------------------
        function doClosingSession(this, group, event)
            if strcmp(event.EventData.EventType, 'CLOSING') && ...
                    group.isClosingApprovalNeeded
                this.closingSession(group)
            end
        end
        
        %------------------------------------------------------------------
        function closingSession(this, group)
            
            sessionChanged = this.Session.IsChanged;
            
            yes    = vision.getMessage('MATLAB:uistring:popupdialogs:Yes');
            no     = vision.getMessage('MATLAB:uistring:popupdialogs:No');
            cancel = vision.getMessage('MATLAB:uistring:popupdialogs:Cancel');
            
            if sessionChanged
                selection = this.askForSavingOfSession();
            else
                selection = no;
            end
            
            switch selection
                case yes
                    this.saveSession();
                    group.approveClose
                    this.deleteToolInstance();
                case no
                    group.approveClose
                    this.deleteToolInstance();
                case cancel
                    group.vetoClose
                otherwise
                    group.vetoClose
            end
            
        end
        
        %------------------------------------------------------------------
        function closeAllFigures(this)
            delete(this.ImageDisplay);            
        end
        
        %------------------------------------------------------------------
        function setupImageDisplay(this)
            
            % create all the required figures
            this.ImageDisplay = ...
                vision.internal.cascadeTrainer.tool.ImageWithROIsDisplay(...
                makeFig());
                        
            this.addFigure(this.ImageDisplay.Parent);
            
            drawnow();
            
            % Prevent the user from being able to close the main image figure
            % using a keyboard shortcut.
            md = com.mathworks.mlservices.MatlabDesktopServices.getDesktop;
            client = md.getClient('MainImageFigure', this.ToolGroup.Name);
            client.putClientProperty(...
                com.mathworks.widgets.desk.DTClientProperty.PERMIT_USER_CLOSE,...
                java.lang.Boolean.FALSE);
            
            % Add listeners.
            
            addlistener(this.ImageDisplay, 'ImageClick', ...
                @this.imageClick);
            
            addlistener(this.ImageDisplay, 'ParentMouseClick', ...
                @(~,~)this.conditionallyEnableBrowserIneraction(false));
            
            addlistener(this.ImageDisplay, 'ParentMouseRelease', ...
                @(~,~)this.conditionallyEnableBrowserIneraction(true));
            
            addlistener(this.ImageDisplay, 'ImageModeChanged', ...
                @this.respondToImageModeChange);        
            
            addlistener(this.ImageDisplay, 'AddFullImageROI', ...
                @this.addFullImageROI);
            
            addlistener(this.ImageDisplay, 'NextImage', ...
                @(~, ~)this.changeImage(1));
            
            addlistener(this.ImageDisplay, 'PreviousImage', ...
                @(~, ~)this.changeImage(-1));     
            
            addlistener(this.ImageDisplay, 'ROIsChanged', ...
                @this.updateInternalROIs);
            
            addlistener(this.ImageDisplay, 'RotateClockwise', ...
                @(~, ~)rotateClockwise(this, this.getSelectedImageIndices()));
            
            addlistener(this.ImageDisplay, 'RotateCounterClockwise', ...
                @(~, ~)rotateCounterClockwise(this, this.getSelectedImageIndices()));
        end 
        
        %------------------------------------------------------------------        
        function resetDataBrowserLocation(this)
            
            % restore data browser to its original location
            md = com.mathworks.mlservices.MatlabDesktopServices.getDesktop;
            md.setClientLocation('DataBrowserContainer', this.getGroupName(), ...
                com.mathworks.widgets.desk.DTLocation.create('W'))
        end
        
    end
    
    %=======================================================================
    
    %----------------------------------------------------------------------
    % Static public methods
    %----------------------------------------------------------------------
    methods (Static)
        
        %------------------------------------------------------------------
        function deleteAllTools
            imageslib.internal.apputil.manageToolInstances('deleteAll',...
                'trainingImageLabeler');
        end
        
    end
    
    %======================================================================
    
    %----------------------------------------------------------------------
    % Private methods
    %----------------------------------------------------------------------
    methods (Access = 'private')
        
        %------------------------------------------------------------------
        function ret = getExportStatus(this)
            ret = this.Session.getExportStatus();
        end
        
        %------------------------------------------------------------------
        %  Gets the UI to the starting point, as if nothing has been loaded
        %------------------------------------------------------------------
        function resetAll(this)            
            % reset the message in the data browser
            this.setupDataBrowser();
            
            % wipe the visible figures
            wipeFigure(this.ImageDisplay);
            
            % reset the session
            this.Session.reset();
            
            this.updateButtonStates();
            this.updateCategoryStrip();
        end
                    
        %------------------------------------------------------------------
        function setupDataBrowser(this)
            msg = getString(message('vision:trainingtool:LoadImagesFirstMsg'));
            this.DataBrowser = javaObject('com.mathworks.toolbox.vision.TCDataBrowser');
            
            % Use Java list to display the message
            label = javaObjectEDT('javax.swing.JLabel', {msg});
            label.setName('InitialDataBrowser');
            this.ImageStrip = javaObject('com.mathworks.toolbox.vision.ImageStrip');
            this.JImageList = javaMethod('getImageList', this.ImageStrip);
            add(this.ImageStrip.getImagePanel(),label,java.awt.BorderLayout.NORTH);
            
            this.DataBrowser.addPanel('Images', ...
                getString(message('vision:trainingtool:DataBrowserTitleImages')), this.ImageStrip.getImagePanel());
            
            this.MCategoryStrip = vision.internal.cascadeTrainer.tool.MCategoryStrip; % initialize the category strip object;
            
            this.DataBrowser.addPanel('Categories', ...
                getString(message('vision:trainingtool:DataBrowserTitleCategories')),this.MCategoryStrip.getPanel());
            
            this.ToolGroup.setDataBrowser(this.DataBrowser.getPanel());
            
            drawnow();
        end
        
        %------------------------------------------------------------------
        % Training Data Labeler app requires at least one image with
        % an ROI. This routine grays out the export button and all the
        % buttons in the mode section if there are no images / images
        % without any ROIs.
        %------------------------------------------------------------------
        function updateButtonStates(this)
            
            this.Session.CanExport = this.getExportStatus();
            this.LabelingTab.updateButtonStates(this.Session);
            
        end
                
        %------------------------------------------------------------------
        function isCanceled = processSessionSaving(this)
            
            isCanceled = false;
            
            sessionChanged = this.Session.IsChanged;
            
            yes    = vision.getMessage('MATLAB:uistring:popupdialogs:Yes');
            no     = vision.getMessage('MATLAB:uistring:popupdialogs:No');
            cancel = vision.getMessage('MATLAB:uistring:popupdialogs:Cancel');
            
            if sessionChanged
                selection = this.askForSavingOfSession();
            else
                selection = no;
            end
            
            switch selection
                case yes
                    this.saveSession();
                case no
                    
                case cancel
                    isCanceled = true;
            end
        end
        
        %------------------------------------------------------------------
        % This function can suspend and resume interaction with the
        % image browser.  This is particularly useful for synchronizing
        % Java UI with MATLAB's functions.
        %------------------------------------------------------------------
        function setBrowserInteractionEnabled(this, isEnabled)
            
            if isempty(this.Session.ImageSet.ImageStruct)
                return;
            end
            
            scrollPane = this.ImageStrip.getImageScrollPane();
            this.IsBrowserInteractionEnabled = isEnabled;
            
            if isEnabled
                javaMethod('enableImageScrolling', this.ImageStrip);
            else
                javaMethod('disableImageScrolling', this.ImageStrip);
            end
            
            javaMethodEDT('setEnabled', scrollPane, isEnabled);
            javaMethodEDT('setWheelScrollingEnabled', scrollPane, isEnabled);
        end
        
        %------------------------------------------------------------------
        function [menuItemRemove, removeActionListener] = ...
                createImageStripRemovePopupMenu(this, idxMultiselect)
            item = vision.getMessage('vision:uitools:Remove');
            itemName = 'removeItem';
            
            menuItemRemove = javaObjectEDT('javax.swing.JMenuItem',...
                item);
            menuItemRemove.setName(itemName);
            
            % Added Accelerators
            jRemoveKeyStroke = javaMethodEDT('getKeyStroke', 'javax.swing.KeyStroke', 'DELETE');
            menuItemRemove.setAccelerator(jRemoveKeyStroke);
            
            removeActionListener = addlistener(menuItemRemove,'Action',...
                @remove); % main popup callback
            
            %----------------------------------------------------------
            function remove(~,~)
                this.processRemove(idxMultiselect)
            end %remove
        end
        %------------------------------------------------------------------
        function [menuRotate, rotateClockwiseListener,...
                rotateCounterClockwiseListener] = ...
                createImageStripRotatePopupMenu(this, idxMultiselect)
            
            % Main rotate menu item
            item1 = vision.getMessage('vision:trainingtool:RotateImage');
            itemName1 = 'rotateImage';
            
            menuRotate = javaObjectEDT('javax.swing.JMenu', ...
                item1);
            menuRotate.setName(itemName1);
            
            item2 = vision.getMessage('vision:trainingtool:RotateClockwise');
            itemName2 = 'rotateClockWise';
            
            % Clockwise sub-menu
            subMenuRotateClockwise = javaObjectEDT('javax.swing.JMenuItem', ...
                item2);
            subMenuRotateClockwise.setName(itemName2);
            jRotateClockwiseKeyStroke = javaMethodEDT(...
                'getKeyStroke', 'javax.swing.KeyStroke', 'control R');
            subMenuRotateClockwise.setAccelerator(jRotateClockwiseKeyStroke);
            menuRotate.add(subMenuRotateClockwise);
            
            % Counter-clockwise sub-menu
            item3 = vision.getMessage('vision:trainingtool:RotateCounterClockwise');
            itemName3 = 'rotateCounterClockWise';
            
            subMenuRotateCounterClockwise = javaObjectEDT('javax.swing.JMenuItem', ...
                item3);
            subMenuRotateCounterClockwise.setName(itemName3);
            jRotateCounterClockwiseKeyStroke = javaMethodEDT(...
                'getKeyStroke', 'javax.swing.KeyStroke', 'control shift R');
            subMenuRotateCounterClockwise.setAccelerator(jRotateCounterClockwiseKeyStroke);
            menuRotate.add(subMenuRotateCounterClockwise);
            
            % Add listeners
            rotateClockwiseListener = addlistener(subMenuRotateClockwise, ...
                'Action', @rotClockwise);
            rotateCounterClockwiseListener = addlistener(...
                subMenuRotateCounterClockwise, 'Action', @rotCounterClockwise);
            
            %----------------------------------------------------------
            function rotClockwise(~,~)
                this.rotateClockwise(idxMultiselect);
            end
            %----------------------------------------------------------
            function rotCounterClockwise(~,~)
                this.rotateCounterClockwise(idxMultiselect);
            end
        end
        
        %------------------------------------------------------------------
        function [menuSortByROIs, sortByROIsActionListener] = ...
                createImageStripSortByROIsPopupMenu(this)
            item = vision.getMessage('vision:trainingtool:SortListByNumROIs');
            itemName = 'SortByNumROIs';
            
            menuSortByROIs = javaObjectEDT('javax.swing.JMenuItem',...
                item);
            menuSortByROIs.setName(itemName);
            
            sortByROIsActionListener = addlistener(menuSortByROIs, 'Action', ...
                @sortByNumROIs);
            
            %----------------------------------------------------------
            function sortByNumROIs(~,~)
                boundingBoxes = {this.Session.ImageSet.ImageStruct.objectBoundingBoxes};
                [rows, ~] = cellfun(@size, boundingBoxes);
                [~, indices] = sortrows(rows');
                this.Session.ImageSet.AreThumbnailsGenerated = ...
                    this.Session.ImageSet.AreThumbnailsGenerated(indices);
                this.Session.ImageSet.ImageStruct = ...
                    this.Session.ImageSet.ImageStruct(indices);
                
                % edit: The following lines are being done in many
                % places. Consider refactoring it into a function.
                
                jfirstVisibleIndex = this.JImageList.getFirstVisibleIndex();
                jlastVisibleIndex = this.JImageList.getLastVisibleIndex();
                for index = jfirstVisibleIndex:jlastVisibleIndex
                    this.Session.ImageSet.updateImageListEntry(index);
                    javaMethodEDT('setListData', this.JImageList, ...
                        {this.Session.ImageSet.ImageStruct.ImageIcon});
                end
                
                % Scroll to the first unlabeled image after sorting and
                % select it.
                this.setSelectedImageIndex(1);
                this.makeSelectionVisible(1);
                drawnow;
            end
        end
        
        %------------------------------------------------------------------
        % Set up management of the image strip
        %------------------------------------------------------------------
        function updateImageStrip(this)
            if(numel(this.Session.ImageSet.ImageStruct)==0)
                return;
            end
            
            jfirstVisibleIndex = this.JImageList.getFirstVisibleIndex();
            jlastVisibleIndex  = this.JImageList.getLastVisibleIndex();
            
            if jfirstVisibleIndex == -1
                jfirstVisibleIndex = 0;
            end
            
            if jlastVisibleIndex == -1
                % By default, a full sized viewport can hold approx 13
                % icons, if the number of images selected is lesser than
                % that set it as the last visible index.
                jlastVisibleIndex = min(numel(this.Session.ImageSet.ImageStruct)-1,...
                    this.MaxNumIcons);
            end
            
            for ind = jfirstVisibleIndex:jlastVisibleIndex
                this.Session.ImageSet.updateImageListEntry(ind);
            end
                        
            selectedIdx = this.getSelectedImageIndex();
            javaMethodEDT('setListData', this.JImageList, ...
                {this.Session.ImageSet.ImageStruct(:).ImageIcon});
            this.setSelectedImageIndex(selectedIdx);
            % Add a listener for handling file selections
            this.addSelectionListener();
            
            
            popupListener = addlistener(this.JImageList, 'MousePressed', ...
                @doPopup);
            
            keyListener = addlistener(this.JImageList, 'KeyPressed', ...
                @doInterceptKeyPress);
            
            % Use the handle command to convert the Java object to a
            % handle object.
            scrollCallback = handle(this.ImageStrip.getScrollCallback);
            
            % Connect the callback to a nested function. The callback
            % class requires 'delayed' as the listener type.
            scrollListener = handle.listener(scrollCallback, 'delayed', @doScroll);
            
            mouseMotionListener = addlistener(this.JImageList, 'MouseMoved', ...
                @this.setStatusText);
            
            % Store handles to prevent going out of scope
            this.Misc.PopupListener       = popupListener;
            this.Misc.KeyListener         = keyListener;
            this.Misc.ScrollListener      = scrollListener;
            this.Misc.MouseMotionListener = mouseMotionListener;
            
            this.setStatusText();
            
            
            %--------------------------------------------------------------
            function doScroll(~, ~)
                if this.Session.ImageSet.areAllIconsGenerated()
                    return;
                end
                
                drawnow(); % update Java UI components before proceeding
                this.setBrowserInteractionEnabled(false);
                this.IsInteractionDisabledByScrollCallback = true;
                
                jfirstVisibleIndex = this.JImageList.getFirstVisibleIndex();
                jlastVisibleIndex = this.JImageList.getLastVisibleIndex();
                
                for index = jfirstVisibleIndex:jlastVisibleIndex
                    doUpdate = this.Session.ImageSet.updateImageListEntry(index);

                    if doUpdate
                        
                        % Do not move the "setWaiting" outside of the for loop!
                        % It will cause the down/up arrows on the scrollbar
                        % to misbehave
                        setWaiting(this.ToolGroup, true); % turn on waiting pointer
                        
                        selectedIndex = this.getSelectedImageIndex();
                        
                        javaMethodEDT('setListData', this.JImageList, ...
                            {this.Session.ImageSet.ImageStruct.ImageIcon});
                        
                        this.setSelectedImageIndex(selectedIndex);
                        drawnow();
                    end
                end
                
                this.setBrowserInteractionEnabled(true);
                this.IsInteractionDisabledByScrollCallback = false;
                setWaiting(this.ToolGroup, false); % turn off waiting pointer
                drawnow();
            end
            
            %--------------------------------------------------------------
            function doInterceptKeyPress(~, hData)
                
                if this.IsBrowserInteractionEnabled
                    if hData.getKeyCode == hData.VK_DELETE
                        doDeleteKey();
                    elseif hData.isControlDown() && hData.isShiftDown() && hData.getKeyCode() == hData.VK_R
                        doCounterClockwiseRotationKey();
                    elseif hData.isControlDown() && hData.getKeyCode() == hData.VK_R
                        doClockwiseRotationKey();
                    elseif hData.getKeyCode == hData.VK_ESCAPE
                        % Hitting escape takes you back to ROI drawing mode
                        this.selectROIMode();
                    end
                end
            end
            
            %--------------------------------------------------------------
            function doDeleteKey()
                % CTRL-DEL will also end up here
                idxMultiselect = this.getSelectedImageIndices();
                this.processRemove(idxMultiselect);
            end
            
            %--------------------------------------------------------------
            function doClockwiseRotationKey()
                idxMultiselect = this.getSelectedImageIndices();
                this.rotateClockwise(idxMultiselect);
                
            end
            
            %--------------------------------------------------------------
            function doCounterClockwiseRotationKey()
                idxMultiselect = this.getSelectedImageIndices();
                this.rotateCounterClockwise(idxMultiselect);
            end
            %--------------------------------------------------------------
            function doPopup(~, hData)
                
                if hData.getButton == 3 % right-click
                    
                    % Get the list widget
                    list = hData.getSource;
                    
                    % Get current mouse location
                    point = hData.getPoint();
                    
                    % Figure out the index of the board immediately under
                    % the mouse button
                    jIdx = list.locationToIndex(point); % 0-based java idx
                    
                    idx = jIdx + 1;
                    
                    % Figure out the index list in the case of multi-select
                    idxMultiselect = this.getSelectedImageIndices();
                    
                    if ~any(idx == idxMultiselect)
                        % If the mouse is not over the selected area;
                        % select whatever is under the mouse and override
                        % the multi-selection index
                        this.setSelectedImageIndex(idx);
                        idxMultiselect = idx;
                    end
                    
                    % Create a popup
                    
                    % Removing Images
                    [menuItemRemove, removeActionListener] = ...
                        this.createImageStripRemovePopupMenu(idxMultiselect);
                    
                    % Rotating an Image
                    [menuRotate, rotateClockwiseListener, ...
                        rotateCounterClockwiseListener] = ...
                        this.createImageStripRotatePopupMenu(idxMultiselect);
                    
                    % Sorting by Number of ROIs
                    [menuSortByROIs, sortByROIsActionListener] = ...
                        this.createImageStripSortByROIsPopupMenu();
                    
                    % Prevent listeners from going out of scope
                    this.Misc.PopupActionListener = [removeActionListener sortByROIsActionListener ...
                        rotateClockwiseListener rotateCounterClockwiseListener];
                    
                    jmenu = javaObjectEDT('javax.swing.JPopupMenu');
                    
                    jmenu.add(menuItemRemove);
                    jmenu.add(menuRotate);
                    
                    jmenu.addSeparator();
                    jmenu.add(menuSortByROIs);
                    
                    % Display the popup
                    jmenu.show(list, point.x, point.y);
                    jmenu.repaint;
                end
            end % doPopup
            
        end % updateImageStrip
        
        %------------------------------------------------------------------
        % Set up management of the category strip
        %------------------------------------------------------------------
        function updateCategoryStrip(this)
            
            this.updateCategoryPanel();
            this.MCategoryStrip.addPopupListener(@doPopup);
            this.MCategoryStrip.addKeyListener(@doInterceptCategoryStripKeyPress);
                            
            %--------------------------------------------------------------
            function doInterceptCategoryStripKeyPress(~, hData)
                
                if this.IsBrowserInteractionEnabled
                    if hData.getKeyCode == hData.VK_DELETE
                        % Figure out the index list in the case of multi-select
                        idxMultiselect = this.MCategoryStrip.getSelectedCategoryIndices();
                        this.processCategoryRemove(idxMultiselect);
                    elseif hData.getKeyCode == hData.VK_ESCAPE
                        % Hitting escape takes you back to ROI drawing mode
                        this.selectROIMode();
                    end
                end
            end
            
            %--------------------------------------------------------------
            function doPopup(~, hData)
                
                if hData.getButton == 3 % right-click
                    
                    % Get the list widget
                    list = hData.getSource;
                    
                    % Get current mouse location
                    point = hData.getPoint();
                    
                    % Figure out the index of the board immediately under
                    % the mouse button
                    jIdx = list.locationToIndex(point); % 0-based java idx
                    
                    idx = jIdx + 1;
                    
                    % Figure out the index list in the case of multi-select
                    idxMultiselect = this.MCategoryStrip.getSelectedCategoryIndices();
                    
                    if ~any(idx == idxMultiselect)
                        % If the mouse is not over the selected area;
                        % select whatever is under the mouse and override
                        % the multi-selection index
                        this.MCategoryStrip.setSelectedCategoryIndex(idx);
                        idxMultiselect = idx;
                    end
                    
                    % Create a popup
                    item = vision.getMessage('vision:trainingtool:Color');
                    itemName = 'colorItem';
                    
                    menuItemColor = javaObjectEDT('javax.swing.JMenuItem',...
                        item);
                    menuItemColor.setName(itemName);
                    
                    % Rename Category
                    item = vision.getMessage('vision:trainingtool:Rename');
                    itemName = 'renameItem';
                    
                    menuItemRename = javaObjectEDT('javax.swing.JMenuItem',...
                        item);
                    menuItemRename.setName(itemName);
                    
                    
                    % Removing Category
                    item = vision.getMessage('vision:uitools:Remove');
                    itemName = 'removeItem';
                    
                    menuItemRemove = javaObjectEDT('javax.swing.JMenuItem',...
                        item);
                    menuItemRemove.setName(itemName);
                    
                    % Added Accelerators
                    jRemoveKeyStroke = javaMethodEDT('getKeyStroke', ...
                        'javax.swing.KeyStroke', 'DELETE');
                    menuItemRemove.setAccelerator(jRemoveKeyStroke);
                    
                    this.MCategoryStrip.removeActionListener = addlistener(...
                        menuItemRemove,'Action', @removeCategory);
                    
                    this.MCategoryStrip.changeColorListener = addlistener(menuItemColor, ...
                        'Action', @changeColor);
                    this.MCategoryStrip.renameCategoryListener = addlistener(menuItemRename, ...
                        'Action', @renameCategory);
                    
                    jmenu = javaObjectEDT('javax.swing.JPopupMenu');
                    
                    jmenu.add(menuItemColor);
                    jmenu.add(menuItemRename);
                    if(list.getModel().getSize() > 1)
                        jmenu.add(menuItemRemove);
                    end
                    
                    % Display the popup
                    jmenu.show(list, point.x, point.y);
                    jmenu.repaint;
                    
                end
                
                %----------------------------------------------------------
                % Nested Functions
                %----------------------------------------------------------
                function changeColor(~,~)
                    idx = this.MCategoryStrip.getSelectedCategoryIndex();
                    c = uisetcolor;
                    if isscalar(c)
                        return;
                    end
                    this.Session.changeCategoryColor(c,idx);
                    this.updateCategoryPanel();
                    this.MCategoryStrip.setSelectedCategoryIndex(idx);     
                    this.redrawBoundingBoxes();                        
                end
                
                %----------------------------------------------------------
                function removeCategory(~,~)
                    this.processCategoryRemove(idxMultiselect);
                end 
                
                %----------------------------------------------------------
                function renameCategory(~,~)
                    idx = this.MCategoryStrip.getSelectedCategoryIndex();
                    
                    categoryDlg = vision.internal.cascadeTrainer.tool.CategoryDlg(...
                        this.getGroupName(), 'rename');
                    wait(categoryDlg);
                    
                    % Close up the modal dialog
                    if ~categoryDlg.IsCanceled
                        varName = categoryDlg.VarName;
                        this.Session.renameCategory(varName,idx);
                        this.updateCategoryPanel();
                        this.MCategoryStrip.setSelectedCategoryIndex(idx);
                        this.redrawBoundingBoxes();                        
                    end
                end
                %----------------------------------------------------------
            end % doPopup
            
        end % updateCategoryStrip
        
        %------------------------------------------------------------------
        function processCategoryRemove(this, idxMultiselect)
            
            if this.Session.NumCategories==1
                return;
            end
            
            if this.Session.NumCategories == numel(idxMultiselect)
                errordlg(vision.getMessage('vision:trainingtool:RemoveAllCategories'));
                return;
            end
            
            if numel(idxMultiselect) > 1
                choice = questdlg(vision.getMessage('vision:trainingtool:RemoveCategoriesPrompt'),...
                    vision.getMessage('vision:trainingtool:RemoveCategoriesTitle'),...
                    vision.getMessage('vision:uitools:Remove'),...
                    vision.getMessage('vision:uitools:Cancel'),...
                    vision.getMessage('vision:uitools:Cancel'));
            elseif (numel(idxMultiselect) == 1)
                choice = questdlg(vision.getMessage('vision:trainingtool:RemoveCategoryPrompt'),...
                    vision.getMessage('vision:trainingtool:RemoveCategoryTitle'),...
                    vision.getMessage('vision:uitools:Remove'),...
                    vision.getMessage('vision:uitools:Cancel'),...
                    vision.getMessage('vision:uitools:Cancel'));
                
            end
            
            % Handle of the dialog is destroyed by the user
            % closing the dialog or the user pressed cancel
            if isempty(choice) || ...
                    strcmp(choice, 'Cancel')
                return;
            end
            
            this.Session.removeCategory(idxMultiselect);
            
            this.updateCategoryPanel();
            idx = 1;
            this.MCategoryStrip.setSelectedCategoryIndex(idx);
            this.redrawBoundingBoxes();
            this.updateImageStrip();
        end
        
        %------------------------------------------------------------------
        function updateCategoryPanel(this)
            num = numel(this.Session.CategorySet.CategoryStruct)-1;
            [jfirstVisibleIndex, jlastVisibleIndex]  = ...
                this.MCategoryStrip.getFirstLastIndex(num);
            
            for ind = jfirstVisibleIndex:jlastVisibleIndex
                
                this.Session.CategorySet.updateCategoryListEntry(ind);
                icon = {this.Session.CategorySet.CategoryStruct.categoryIcon};
                this.MCategoryStrip.updateCategoryStrip(icon);
                
            end
        end
        
        
        %------------------------------------------------------------------
        % Add selection listener to the image browser to handle the update of
        % the image display.
        %------------------------------------------------------------------
        function addSelectionListener(this)
            
            selectionCallback = handle(this.ImageStrip.getSelectionCallback);
            
            % Connect the callback to a class function. The callback
            % class requires 'delayed' as the listener type.
            selectionListener = handle.listener(selectionCallback, ...
                'delayed', @this.doSelection);
            
            this.Misc.SelectionListener = selectionListener;
        end
        
        % File selection handler
        %----------------------------------------
        function doSelection(this, ~, ~)
            if ~this.Session.hasAnyImages()
                return
            else
                if ~isempty(this.ImageDisplay) && this.ImageDisplay.IsValid %ishandle(this.FigureHandles.MainImage)
                    
                    if ~this.IsInteractionDisabledByScrollCallback
                        drawnow();
                        setWaiting(this.ToolGroup, true);
                        this.setBrowserInteractionEnabled(false);
                    end
                    
                    this.selectROIMode();
                    this.ImageDisplay.wipeFigure();
                    this.drawImages();
                    this.setStatusText();
                    
                    if ~this.IsInteractionDisabledByScrollCallback
                        drawnow();
                        this.setBrowserInteractionEnabled(true);
                        setWaiting(this.ToolGroup, false);
                    end
                    
                end
            end
        end
    end
     
    %======================================================================
    
    methods (Access = 'public', Hidden)
        %------------------------------------------------------------------
        function drawImages(this)
            if ~this.ImageDisplay.IsValid
                return; % figure was destroyed
            end
            
            % Handle the case of wiping the data out
            if ~hasAnyImages(this.Session)
                return; % this can happen in rapid testing
            end
            
            currentIdx = this.getSelectedImageIndex();            
            
            try % image can disappear from the disk
                [imageMatrix, imageLabel] = this.Session.getImages(currentIdx);
            catch missingFileEx
                errordlg(missingFileEx.message,...
                    vision.getMessage...
                    ('vision:uitools:LoadingImagesFailedTitle'), 'modal');
                return;
            end
            
            this.ImageDisplay.drawImage(imageMatrix, imageLabel);            
            this.redrawBoundingBoxes();
        end % drawImages
        
        %------------------------------------------------------------------
        function redrawBoundingBoxes(this)                        
            index = this.getSelectedImageIndex();
            if ~hasROIs(this.Session, index)
                return;
            end
            
            boundingBoxes = this.Session.ImageSet.ImageStruct...
                (index).objectBoundingBoxes;
            categoryID = this.Session.ImageSet.ImageStruct(index).catID;
            in = arrayfun(@(x) find(x == [this.Session.CategorySet.CategoryStruct.categoryID]),...
                categoryID,'UniformOutput', false);
            colors = {this.Session.CategorySet.CategoryStruct([in{:}]).categoryColor};

            catNames = cell(1, numel(categoryID));
            for i = 1:numel(categoryID)
                catNames{i} = this.Session.getCategoryName(categoryID(i));
            end
            
            this.ImageDisplay.drawBoundingBoxes(boundingBoxes, categoryID, ...
                colors, catNames, this.ShowROILabels);            
        end
        
        %------------------------------------------------------------------
        function imageClick(this, varargin)
            mouseClickType = get(this.ImageDisplay.Parent, 'SelectionType');
            this.ImageDisplay.deselectAllROIs();
            switch mouseClickType
                case 'normal'
                    % Draw ROI interactively
                    this.drawROI();
                    
                case 'open' % triple-click
                    this.addFullImageROI();
            end
            
        end
        
        %---------------------------------------------------------------
        function addFullImageROI(this, ~, ~)
            % Create an ROI to fill the entire image
            imageIdx = getSelectedImageIndex(this);
            catIdx = getSelectedCategoryIndex(this);
            hAxes = this.ImageDisplay.ImageAxes;
            hImage = findobj(hAxes, 'Type', 'image');
            imSize = size(hImage.CData);
            roi = [1, 1, imSize([2, 1])];
            addROI(this.Session, imageIdx, catIdx, roi);
            this.drawImages();
            this.updateImageStrip();
        end
                    
        %---------------------------------------------------------------
        function drawROI(this, varargin)
            hAxes = this.ImageDisplay.ImageAxes;
            hImage = findobj(hAxes, 'Type', 'image');
            
            roi = vision.internal.uitools.imrectButtonDown.drawROI(hImage);
            
            if isempty(roi)
                % no-op
            elseif ~vision.internal.uitools.imrectButtonDown.isValidROI(roi)
                this.updateSessionObject();
            else
                this.setROI(roi);
            end
            
            drawnow(); % Finish all the drawing before moving on
            
        end
        
        %------------------------------------------------------------------
        function setROI(this, roi)
            currentIndex = this.getSelectedCategoryIndex();
            if currentIndex < 1
                currentIndex = 1;
            end
            catID = this.Session.CategorySet.CategoryStruct(currentIndex).categoryID;
            color = this.Session.CategorySet.CategoryStruct(currentIndex).categoryColor;
            catName = this.Session.getCategoryName(catID);
            
            this.ImageDisplay.deselectAllROIs();
            this.ImageDisplay.drawROI(roi, catID, color, catName, this.ShowROILabels);

            this.updateInternalROIs();
        end
        
        %------------------------------------------------------------------
        function setCategoriesVisible(this)
            if ~isempty(this.ImageDisplay)
                this.ImageDisplay.setCategoriesVisible(this.ShowROILabels);
            end
        end
                        
        %------------------------------------------------------------------
        function updateInternalROIs(this, varargin)
            this.updateSessionObject();
            this.Session.CanExport = this.getExportStatus();
            this.updateButtonStates();
        end
    end
    
    %=======================================================================
    
    methods (Access = 'private')        
        %-------------------------------------------------------------------
        function respondToImageModeChange(this, ~, ~)
            switch this.ImageDisplay.ImageMode
                case 'ROImode'
                    selectROIMode(this);
                case 'ZoomInMode'
                    selectZoomInMode(this);
                case 'ZoomOutMode'
                    selectZoomOutMode(this);
                case 'PanMode'
                    selectPanMode(this);
            end
        end
        
        %-------------------------------------------------------------------
        function selectROIMode(this, varargin)
            this.LabelingTab.selectROIMode();
            drawnow();
        end
        
        %-------------------------------------------------------------------
        function selectZoomInMode(this, varargin)
            this.LabelingTab.selectZoomInMode();
        end
        
        %-------------------------------------------------------------------
        function selectZoomOutMode(this, varargin)
            this.LabelingTab.selectZoomOutMode();
        end
        
        %-------------------------------------------------------------------
        function selectPanMode(this, varargin)
            this.LabelingTab.selectPanMode();
        end
        
        %-------------------------------------------------------------------
        function setStatusText(this, varargin)
            % Set the status bar to indicate the number of images labeled
            % by the user.
            md = com.mathworks.mlservices.MatlabDesktopServices.getDesktop;
            f = md.getFrameContainingGroup(this.getGroupName());
            
            if isempty(f)
                % Bail out if we can't grab the frame.  Apparently, this
                % can happen when testing on the MAC g1130360.
                return;
            end
            
            if this.Session.hasAnyImages()
                totalNumImages = getNumImages(this.Session);
                numImagesLabeled = getNumLabeledImages(this.Session);
                totalNumROIs = getNumROIs(this.Session);
                
                statusText{1} = vision.getMessage...
                    ('vision:trainingtool:NumImagesLabeled',...
                    numImagesLabeled, totalNumImages);
                
                statusText{2} = vision.getMessage...
                    ('vision:trainingtool:NumTotalROIs',totalNumROIs);
                
                % setStatusText called by paste operation
                if ~isempty(varargin) && strcmp(varargin{1},'paste')
                    pasteRoiNums = varargin{2};
                    statusText{3} = vision.getMessage...
                        ('vision:trainingtool:LastPasteOp',...
                        pasteRoiNums(2), pasteRoiNums(1));
                end
                
                statusText = strjoin(statusText, '    ');
                
                javaMethodEDT('setStatusText', f, statusText);
            else
                % clear the status text
                javaMethodEDT('setStatusText', f, '');
            end
        end        
        
        %------------------------------------------------------------------
        function updateSessionObject(this)
            
            currentIndex = this.getSelectedImageIndex;
            hAxes = this.ImageDisplay.ImageAxes;
            if isempty(hAxes)
                return;
            end
            currentROIs = findall(hAxes, 'tag', 'imrect');
            
            % Return if no ROIs
            if isempty(currentROIs)
                this.Session.CanExport = false;
            end
            boundingBoxes = zeros(numel(currentROIs),4);
            colors = zeros(numel(currentROIs),3);
            categoryID = zeros(numel(currentROIs),1);

            for i = 1:numel(currentROIs)
                boundingBoxes(i,:) = round(getPos(currentROIs(i)));
                userData = get(currentROIs(i),'UserData');
                colors(i,:) = this.Session.getCategoryColor(userData.catID);
                categoryID(i,1) = userData.catID;
            end
            
            needsUpdate = ...
                this.Session.ImageSet.updateBoundingBoxes(currentIndex,...
                boundingBoxes, categoryID);
            
            this.Session.IsChanged = needsUpdate;
            
            % Increment the ROI count on the icon description
            this.Session.ImageSet.updateIconDescription(currentIndex);
            % update the status bar to refresh the progress in labeling
            this.setStatusText();
            javaMethodEDT('updateUI', this.JImageList); % Force an update of the list
            
            this.Session.CanExport = true;
            
            %--------------------------------------------------------------
            function bbox = getPos(ROI)
                bbox = iptgetapi(ROI);
                bbox = round(feval(bbox.getPosition));
            end
        end
        
        %------------------------------------------------------------------
        % returns index of the selected Image
        %------------------------------------------------------------------
        function idx = getSelectedImageIndex(this)
            idx = double(javaMethodEDT('getSelectedIndex', this.JImageList));
            idx = idx+1; % make it one based
            if idx < 1
                idx = 1;
            end
        end
        
        %------------------------------------------------------------------
        function setSelectedImageIndex(this, index) % assumes 1-based index
            javaMethodEDT('setSelectedIndex', this.JImageList, index-1);
        end
        
        %------------------------------------------------------------------
        function makeSelectionVisible(this, index)
            javaMethodEDT('ensureIndexIsVisible', this.JImageList, index-1);
        end
        
        %------------------------------------------------------------------
        function [idx, jIdx] = getSelectedImageIndices(this)
            idx = double(this.JImageList.getSelectedIndices);
            jIdx = idx; % 0-based java index
            idx = idx+1; % make it one based
        end
        
        
        %------------------------------------------------------------------
        % returns index of the selected Category
        %------------------------------------------------------------------
        function idx = getSelectedCategoryIndex(this)
            idx = this.MCategoryStrip.getSelectedCategoryIndex;
        end
        
        %------------------------------------------------------------------
        function setSelectedCategoryIndex(this, index) % assumes 1-based index
            this.MCategoryStrip.setSelectedCategoryIndex(index);
        end
        
        %------------------------------------------------------------------
        function makeCategorySelectionVisible(this, index)
            this.MCategoryStrip.makeCategorySelectionVisible(index);
        end
        
        %------------------------------------------------------------------
        function [idx, jIdx] = getSelectedCategoryIndices(this)
            [idx, jIdx] = this.MCategoryStrip.getSelectedCategoryIndices;
        end
        
        %------------------------------------------------------------------
        function processRemove(this, idxMultiselect)
            
            % create a warning that asks if you're sure to remove
            
            % Display different warnings based on whether multiple images
            % are selected or just a single image is selected.
            
            cancel = getString(message('MATLAB:uistring:popupdialogs:Cancel'));
            if numel(idxMultiselect) > 1
                choice = questdlg(vision.getMessage('vision:trainingtool:RemoveImagesPrompt'),...
                    vision.getMessage('vision:trainingtool:RemoveImagesTitle'),...
                    vision.getMessage('vision:uitools:Remove'), cancel, cancel);
            elseif (numel(idxMultiselect) == 1)
                choice = questdlg(vision.getMessage('vision:trainingtool:RemoveImagePrompt'),...
                    vision.getMessage('vision:trainingtool:RemoveImageTitle'),...
                    vision.getMessage('vision:uitools:Remove'), cancel, cancel);
                
            end
            
            % Handle of the dialog is destroyed by the user
            % closing the dialog or the user pressed cancel
            if isempty(choice) || ...
                    strcmp(choice, cancel)
                return;
            end
            
            this.Session.ImageSet.removeImage(idxMultiselect);
            
            this.Session.IsChanged = true;
            
            jLowestIdx = idxMultiselect(1)-1;
            
            if this.Session.hasAnyImages()
                javaMethodEDT('setListData', this.JImageList, {this.Session.ImageSet.ImageStruct.ImageIcon});
                
                if jLowestIdx ~= 0
                    newIdx = jLowestIdx;
                else
                    newIdx = 1;
                end
                
                this.setSelectedImageIndex(newIdx);
            else
                % Remove all items from the image list
                if ishandle(this.ImageStrip)
                    javaMethodEDT('resetImageList', this.ImageStrip);
                end
                this.Session.resetImages();
                wipeFigure(this.ImageDisplay);
                this.updateImageStrip();
            end
                        
            % Update the UI before proceeding further
            drawnow;
            
        end
        
        %------------------------------------------------------------------
        function rotateClockwise(this, idxMultiselect)
            rotationType = 'Clockwise';
            rotateImage(this, idxMultiselect, rotationType);
        end
        
        %------------------------------------------------------------------
        function rotateCounterClockwise(this, idxMultiselect)
            rotationType = 'CounterClockwise';
            rotateImage(this, idxMultiselect, rotationType);
        end
        
        %------------------------------------------------------------------
        function rotateImage(this, idxMultiselect, rotationType)
            % create a warning that asks if you're sure to overwrite
            % rotated image
            
            % Display different warnings based on whether multiple images
            % are selected or just a single image is selected.
            
            cancel = getString(message('MATLAB:uistring:popupdialogs:Cancel'));
            if numel(idxMultiselect) > 1
                choice = questdlg(vision.getMessage('vision:trainingtool:RotateImagesPrompt'),...
                    vision.getMessage('vision:trainingtool:RotateImagesTitle'),...
                    vision.getMessage('vision:trainingtool:Continue'), cancel, cancel);
            elseif (numel(idxMultiselect) == 1)
                choice = questdlg(vision.getMessage('vision:trainingtool:RotateImagePrompt'),...
                    vision.getMessage('vision:trainingtool:RotateImageTitle'),...
                    vision.getMessage('vision:trainingtool:Continue'), cancel, cancel);
            end
            
            % Handle of the dialog is destroyed by the user
            % closing the dialog or the user pressed cancel
            if isempty(choice) || ...
                    strcmp(choice, cancel)
                return;
            end
            this.Session.ImageSet.rotateImages(idxMultiselect, rotationType);
            
            this.Session.IsChanged = true;
            
            javaMethodEDT('setListData', this.JImageList, ...
                {this.Session.ImageSet.ImageStruct.ImageIcon});
            
            this.setFocusOnImages();
            this.setSelectedImageIndex(idxMultiselect(1)); % pick the first index
            drawnow;
            this.drawImages();
            
        end
        
        %------------------------------------------------------------------
        % Puts the image strip in focus
        %------------------------------------------------------------------
        function setFocusOnImages(this)
            %drawnow;
            if ishandle(this.JImageList)
                javaMethodEDT('requestFocus', this.JImageList);
            end
        end
        
        %--------------------------------------------------------------
        function changeImage(this, direction)
            
            currentIndex = this.getSelectedImageIndex();
            this.setSelectedImageIndex(currentIndex+direction);
            this.makeSelectionVisible(currentIndex+direction);
            
        end
    end
end

%------------------------------------------------------------------
% Creates a figure with properties desired by the Training Data labeler
% tool  UI
%------------------------------------------------------------------
function fig = makeFig()
% suppress docked-window warning
warning('off', 'MATLAB:figure:SetResize');
c = onCleanup(@()warning('on', 'MATLAB:figure:SetResize'));

fig = figure('Resize', 'off', 'Visible','off', ...
    'NumberTitle', 'off', 'Name', 'MainImageFigure', 'HandleVisibility',...
    'callback', 'Color','white','IntegerHandle','off',...
    'BusyAction', 'cancel', 'Interruptible', 'off');

end