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

    % ImageWithROIsDisplay encapsulates an axes containing an image with
% overlayed ROIs

% Copyright 2016 The MathWorks, Inc.

classdef ImageWithROIsDisplay < handle
    properties        
        ImageAxes = [];
        Parent = [];
        ImageMode = 'ROImode';
        CurrentROIs = {};        
    end
    
    properties(Dependent)
        IsValid;
    end
    
    properties(Access=private)
        AxesTag = 'TrainingImageAxes';
        InsideNudge = false;
        CopiedROIs = [];     
    end
    
    events
        ImageClick
        ParentMouseClick
        ParentMouseRelease
        ImageModeChanged
        AddFullImageROI
        NextImage
        PreviousImage
        ROIsChanged
        RotateClockwise
        RotateCounterClockwise
    end
    
    methods
        %------------------------------------------------------------------
        function this = ImageWithROIsDisplay(parent)
            this.Parent = parent;
            set(this.Parent, 'Visible', 'on');
            iptaddcallback(this.Parent,'KeyPressFcn',@this.doFigKeyPress);
        end
        
        %------------------------------------------------------------------
        function tf = get.IsValid(this)
            tf = isvalid(this) && ~isempty(this.Parent) && isvalid(this.Parent) && ishandle(this.Parent);
        end
        
        %------------------------------------------------------------------
        function delete(this)
            if ishandle(this.Parent)
                set(this.Parent,'HandleVisibility','on');
                close(this.Parent);
            end
        end
        
        %------------------------------------------------------------------
        function setCategoriesVisible(this, tf)
            for i = 1:numel(this.CurrentROIs)
                this.CurrentROIs{i}.setTextLabelVisible(tf);
            end
        end
        
        %------------------------------------------------------------------
        function drawImage(this, imageMatrix, imageLabel)       
            set(this.Parent, 'HandleVisibility', 'on');
            if isempty(this.ImageAxes) || ~ishandle(this.ImageAxes) % add an axes if needed
                hAxes = axes('Parent', this.Parent,...
                    'Tag', this.AxesTag);
                this.ImageAxes = hAxes;
            else
                hAxes = this.ImageAxes;
            end
            
            if isa(imageMatrix, 'single')
                imageMatrix = ...
                    vision.internal.cascadeTrainer.tool.adjustAndConvertSingleImage(imageMatrix);
            end
            
            hImage = imshow(imageMatrix,'InitialMagnification', 'fit',...
                'Parent', hAxes, 'Border', 'tight');
            
            set(hImage, 'buttondownfcn', @(~,~) notify(this, 'ImageClick'));
            
            % Install context menu again. Have to do it twice because the
            % pointer behavior code prevents the context menus from
            % showing up immediately after images are loaded.
            this.installContextMenu('ROI');
            
            % add title
            title(hAxes, imageLabel, 'Interpreter', 'none');
            
            % set pointer behavior to cross when hovering over the image
            this.setPointerToCross();
            
            % Disable overwriting to put on the ROIs
            set(hAxes, 'NextPlot', 'add');
            
            % resets all axes properties to default values
            set(hAxes, 'NextPlot', 'replace');
            set(hAxes, 'Tag', this.AxesTag); % add tag after reset
            
            set(this.Parent, 'HandleVisibility', 'callback');
            
        end
        
        %------------------------------------------------------------------
        % selection can be 'ROI', 'ZoomIn', 'ZoomOut', or 'Pan'
        %------------------------------------------------------------------
        function installContextMenu(this, selection)
            
            choices = {'off','on'};
            
            hFig = this.Parent;
            
            hCmenu = uicontextmenu('Parent', hFig);
            
            % Paste
            pasteUIMenu = uimenu(hCmenu, 'Label', ...
                getString(message('vision:trainingtool:PastePopup')),...
                'Callback', @this.pasteSelectedROIs, 'Accelerator', 'V');
            if isempty(this.CopiedROIs)
                set(pasteUIMenu,'Enable','off');
            end
            
            % Add full image ROI            
            uimenu(hCmenu, 'Label', ...
                getString(message('vision:trainingtool:FullROIPopup')),...
                'Callback', @(~, ~)notify(this, 'AddFullImageROI'));
            
            % ROI mode
            uimenu(hCmenu, 'Label', ...
                getString(message('vision:trainingtool:ROImodePopup')), ...
                'Checked', choices{strcmp(selection,'ROI')+1}, ...
                'Callback', @(~, ~)setImageMode(this, 'ROImode'));
            
            % Zoom in
            uimenu(hCmenu, 'Label', ...
                getString(message('vision:trainingtool:ZoomInPopup')),...
                'Checked', choices{strcmp(selection,'ZoomIn')+1}, ...
                'Callback', @(~, ~)setImageMode(this, 'ZoomInMode'));
            
            % Zoom out
            uimenu(hCmenu, 'Label', ...
                getString(message('vision:trainingtool:ZoomOutPopup')),...
                'Checked', choices{strcmp(selection,'ZoomOut')+1},....
                'Callback', @(~, ~)setImageMode(this, 'ZoomOutMode'));
            
            % Pan
            uimenu(hCmenu, 'Label', ...
                getString(message('vision:trainingtool:PanPopup')),...
                'Checked', choices{strcmp(selection,'Pan')+1}, ...
                'Callback', @(~,~)setImageMode(this, 'PanMode'));
            
            % Rotate
            rotateMenu = uimenu(hCmenu, 'Label', ...
                getString(message('vision:trainingtool:RotateImage')));
            
            uimenu(rotateMenu, 'Label', ...
                getString(message('vision:trainingtool:RotateClockwise')), ...
                'Callback', @(~,~)notify(this, 'RotateClockwise'), ...
                'Accelerator', 'R');                
            
            uimenu(rotateMenu, 'Label', ...
                getString(message('vision:trainingtool:RotateCounterClockwise')), ...
                'Callback', @(~,~)notify(this, 'RotateCounterClockwise'));
            
            hImage = findobj(this.ImageAxes, 'Type', 'image');
            set(hImage, 'UIContextMenu', hCmenu);
        end
        
        
        %------------------------------------------------------------------
        % Pressing a key in the main figure, will send you through this
        % code
        %------------------------------------------------------------------
        function doFigKeyPress(this,~,src)
            
            if isempty(this.ImageAxes) || ~ishandle(this.ImageAxes)
                % Nothing to do if none of the images are loaded.
                return;
            end
            
            modifierKeys = {'control', 'command'};
            
            if any(strcmp(src.Modifier, modifierKeys{ismac()+1}))
                switch src.Key
                    case 'a'
                        this.selectAllROIs();
                    case 'c'
                        this.copySelectedROIs();
                    case 'v'
                        if ~isempty(this.CopiedROIs)
                            this.pasteSelectedROIs;
                        end
                    case 'x'
                        this.cutSelectedROIs();
                    case 'r'
                        if any(strcmp(src.Modifier, 'shift'))
                            notify(this, 'RotateCounterClockwise');
                        else
                            notify(this, 'RotateClockwise');
                        end  
                    otherwise
                        this.directionKeyPress(src.Key, src.Modifier);
                        
                end
            else
                switch src.Key
                    case 'escape'
                        this.setImageMode('ROImode');
                    case 'delete'
                        this.deleteSelectedROIs();
                    case 'pagedown'
                        notify(this, 'NextImage');
                    case 'pageup'
                        notify(this, 'PreviousImage');
                    otherwise
                        this.directionKeyPress(src.Key, src.Modifier);
                end
            end
        end
        
        %------------------------------------------------------------------        
        function directionKeyPress(this, keyPressed, modifierPressed)
            
            directionKeys = {'uparrow', 'downarrow', 'rightarrow',...
                'leftarrow'};
            keyIndex = find(strcmp(keyPressed, directionKeys), 1);
            
            if ~isempty(keyIndex)
                hAxes = this.ImageAxes;
                currentROIs = findall(hAxes, 'tag', 'imrect',...
                    'Selected','on');
                if isempty(currentROIs)
                    if keyIndex==1      
                        notify(this, 'PreviousImage');
                    elseif keyIndex==2
                        notify(this, 'NextImage');
                    end
                else
                    nudgeROI();
                end
            end
            
            %--------------------------------------------------------------
            function nudgeROI()
                
                hImage = findobj(hAxes, 'Type', 'image');
                [y_extent, x_extent, ~] = size(get(hImage,'CData'));
                constraint_fcn = makeConstrainToRectFcn('imrect',...
                    [1 x_extent+1], [1 y_extent+1]);
                
                roiObjects = arrayfun(@iptgetapi, currentROIs);
                roiPositions = cell2mat(arrayfun(@(x) x.getPosition(),...
                    roiObjects,'UniformOutput',false));
                
                % Get number of pixels to nudge (based on whether CTRL is
                % pressed), and the index of the ROI that is closest to the
                % image boundary
                [nudgePixels, index] = nudgeDirection();
                
                % If the ROI closest to the image boundary is within bounds
                % after nudging, then nudge all ROIs
                newPos = roiPositions(index,:) + nudgePixels + [0.5 0.5 0 0];
                
                this.InsideNudge = 1;
                if isequal(newPos, constraint_fcn(newPos))
                    newRoiPositions = cell2mat(arrayfun(@(x)...
                        roiPositions(x,:) + nudgePixels,...
                        (1:numel(currentROIs))','UniformOutput',false));
                    arrayfun(@(x,y) x.setPosition(newRoiPositions(y,:)),...
                        roiObjects,(1:numel(currentROIs))');
                end
                this.InsideNudge = 0;
                notify(this, 'ROIsChanged');
                drawnow();
                
                %----------------------------------------------------------
                function [nudgePixels, index] = nudgeDirection()
                    
                    switch keyIndex
                        case 1
                            nudgePixels = [0 -5 0 0];
                            [~,index] = min(roiPositions(:,2));
                        case 2
                            nudgePixels = [0 5 0 0];
                            [~,index] = max(roiPositions(:,2)+...
                                roiPositions(:,4));
                        case 3
                            nudgePixels = [5 0 0 0];
                            [~,index] = max(roiPositions(:,1)+...
                                roiPositions(:,3));
                        case 4
                            nudgePixels = [-5 0 0 0];
                            [~,index] = min(roiPositions(:,1));
                    end
                    
                    if ~isempty(modifierPressed)
                        if any(strcmp(modifierPressed, {'control',...
                                'command'}))
                            nudgePixels = nudgePixels./5;
                        end
                    end
                    
                end    % end of nudgeDirection()
                
            end    % end of nudgeROI()
            
        end
        
        %------------------------------------------------------------------
        % Select all ROIs
        function selectAllROIs(this, varargin)
            for i = 1:numel(this.CurrentROIs)
                this.CurrentROIs{i}.IsSelected = true;
            end
        end
        
        %------------------------------------------------------------------
        function setImageMode(this, mode)                        
            this.ImageMode = mode;
            if isequal(this.ImageMode, 'ROImode')
                this.setPointerToCross();
            end
            notify(this, 'ImageModeChanged');
        end
        
        %------------------------------------------------------------------
        function drawBoundingBoxes(this, bboxs, catIDs, colors, catNames, showROILabels)
            % Restore selection of ROIs across multiple images
            for i = 1:numel(this.CurrentROIs)
                delete(this.CurrentROIs{i});
            end
            this.CurrentROIs = {};
            
            for i = 1:size(bboxs, 1)
                catID = catIDs(i);
                drawROI(this, bboxs(i,:), catID, colors{i}, catNames{i}, ...
                    showROILabels);                                
            end
        end
        
        %------------------------------------------------------------------
        function deselectAllROIs(this)
            for i = 1:numel(this.CurrentROIs)
                this.CurrentROIs{i}.IsSelected = false;
            end
        end
        
        %------------------------------------------------------------------
        function drawROI(this, bbox, catID, color, catName, showROILabel, ...
                isSelected)
            if nargin < 7
                isSelected = false;
            end
            
            hImage = findobj(this.ImageAxes, 'Type', 'image');
            this.CurrentROIs{end+1} = ...
                vision.internal.cascadeTrainer.tool.EnhancedROI(...
                bbox, this.ImageAxes,...
                hImage, catID, color, catName, showROILabel);
            
            addlistener(this.CurrentROIs{end}, 'Delete', ...
                @(~,~)notify(this, 'ROIsChanged'));
            
            addlistener(this.CurrentROIs{end}, 'Move', ...
                @this.moveIfNotNudging);
            
            addlistener(this.CurrentROIs{end}, 'Copy', ...
                @this.copySelectedROIs);
            
            addlistener(this.CurrentROIs{end}, 'Cut', ...
                @this.cutSelectedROIs);
            
            this.CurrentROIs{end}.IsSelected = isSelected;
        end
        
        %--------------------------------------------------------------
        function moveIfNotNudging(this, varargin)
            if ~this.InsideNudge
                notify(this, 'ROIsChanged');
            end
        end
        
        %--------------------------------------------------------------
        % Copy selected ROIs
        function copySelectedROIs(this, varargin)
            this.CopiedROIs = {};
            
            for i = numel(this.CurrentROIs):-1:1
                if this.CurrentROIs{i}.IsValid && this.CurrentROIs{i}.IsSelected
                    this.CopiedROIs{end+1} = this.CurrentROIs{i}.CopiedData;
                end
            end
            
            if ~isempty(this.CopiedROIs)
                this.enablePaste();
            end
        end
        
        %--------------------------------------------------------------
        % Cut selected ROIs
        function cutSelectedROIs(this, varargin)
            
            % Copy selected ROIs
            this.copySelectedROIs();
            
            % Delete selected ROIs
            this.deleteSelectedROIs();
            
        end
        
        %--------------------------------------------------------------
        % Delete selected ROIs
        function deleteSelectedROIs(this, varargin)
            numROIs = numel(this.CurrentROIs);
            isDeleted = false(1, numROIs);
            for i = 1:numROIs
                if this.CurrentROIs{i}.IsSelected
                    delete(this.CurrentROIs{i});
                    isDeleted(i) = true;
                end
            end
            this.CurrentROIs = this.CurrentROIs(~isDeleted);
            notify(this, 'ROIsChanged');
        end
        
        
        %------------------------------------------------------------------
        % Paste selected ROI co-ordinates
        function pasteSelectedROIs(this, varargin)
            hAxes = this.ImageAxes;
            hImage = findobj(hAxes, 'Type', 'image');
            
            [y_extent, x_extent, ~] = size(get(hImage,'CData'));
            constraint_fcn = makeConstrainToRectFcn('imrect',...
                [1 x_extent+1], [1 y_extent+1]);
            
            % Offsets needed for pasting in case of overlap
            xOffset = round(x_extent/100 + 1);
            yOffset = round(y_extent/100 + 1);
            offset(1,:) = [ xOffset,  yOffset, 0, 0];
            offset(2,:) = [-xOffset, -yOffset, 0, 0];
            offset(3,:) = [ xOffset, -yOffset, 0, 0];
            offset(4,:) = [-xOffset,  yOffset, 0, 0];
            
            numROIs = numel(this.CopiedROIs);
            
            currentROIs = zeros(0, 4);
            if numROIs > 0
                for i = 1:numel(this.CurrentROIs)
                    if this.CurrentROIs{i}.IsValid
                        this.CurrentROIs{i}.IsSelected = false;
                        currentROIs(end+1, :) = ...
                            this.CurrentROIs{i}.BBox + [0.5 0.5 0 0]; %#ok<AGROW>
                    end
                end
            else
                return;
            end
            
            
            numPastedROIs = 0;
            for i = 1:numROIs
                boxPoints = this.CopiedROIs{i}.bbox;
                
                % Pasting between images of different sizes
                if (boxPoints(1) > x_extent) || (boxPoints(2) > y_extent)
                    continue;
                end
                
                boxPoints(3) = min(boxPoints(3), x_extent+1-boxPoints(1));
                boxPoints(4) = min(boxPoints(4), y_extent+1-boxPoints(2));
                
                % Offset code
                offsetIndex = 1;
                if ~isempty(currentROIs)
                    % Check if pasted ROI overlaps with existing ROIs
                    if ~isempty(intersect(currentROIs,boxPoints,'rows'))
                        lastToLastPoints = [NaN NaN NaN NaN];
                        lastPoints = boxPoints;
                        try
                            % Find a place to paste since there is overlap
                            findPlaceToPaste();
                        catch
                            % No place found, do not paste ROI
                            boxPoints = [];
                        end
                    end
                end
                
                if ~isempty(boxPoints)
                    catID = this.CopiedROIs{i}.categoryID;
                    color = this.CopiedROIs{i}.color;
                    catName = this.CopiedROIs{i}.categoryName;
                    isSelected = true;
                    this.drawROI(boxPoints, catID, ...
                        color, catName, true, isSelected);
                    numPastedROIs = numPastedROIs + 1;
                end
            end
            
            if numPastedROIs > 0
                % Update the session
                notify(this, 'ROIsChanged');
            end
            
            %TODO: set status text.
            % Notify user if all ROIs are not pasted
%             if numROIs ~= numPastedROIs
%                 this.setStatusText('paste', [numROIs numPastedROIs]);
%             end
%             
            % Flush the event queue before new paste callback
            % This is required to ensure ROIs are pasted only as long as
            % CTRL+V is held down
            drawnow;
            
            %-------------------------------------------------------
            % Recursive function to look for a place to paste
            function findPlaceToPaste()
                
                % Add offset and check if within bounds
                newBoxPoints = boxPoints + offset(offsetIndex,:);
                if ~isequal(constraint_fcn(newBoxPoints), newBoxPoints)
                    
                    % Outside bounds, so change offset direction and keep
                    % looking for a place
                    offsetIndex = offsetIndex + 1;
                    if offsetIndex==5
                        offsetIndex = 1;
                    end
                    findPlaceToPaste();
                else
                    
                    % Check if we are have already been at the current
                    % location before (this check is needed to avoid
                    % getting stuck in an infinite loop)
                    if ~isequal(lastToLastPoints,newBoxPoints)
                        
                        % If there is no overlap, then we have found a
                        % place to paste. This is the only point of return
                        % from this recursive function
                        if ~isempty(intersect(currentROIs,boxPoints,'rows'))
                            
                            % We still have overlap, update previous
                            % locations, and keep looking for a place
                            boxPoints = newBoxPoints;
                            lastToLastPoints = lastPoints;
                            lastPoints = boxPoints;
                            findPlaceToPaste();
                        end
                    else
                        
                        % We have been here before, so change direction
                        % and keep looking for a place
                        offsetIndex = offsetIndex + 1;
                        if offsetIndex==5
                            offsetIndex = 1;
                        end
                        findPlaceToPaste();
                    end
                end
            end
            
        end
        
        %--------------------------------------------------------------
        % Function to enable 'Paste' context menu at the first copy
        function enablePaste(this, varargin)
            hAxes = this.ImageAxes;
            hImage     = findobj(hAxes, 'Type', 'image');
            foundPaste = findall(get(hImage,'UIContextMenu'),...
                'Label','Paste');
            set(foundPaste, 'Enable','on');            
        end
        
        %------------------------------------------------------------------
        function setPointerToCross(this)
            
            hFig = this.Parent;
            hImage = findobj(hFig, 'Type', 'image');
            
            enterFcn = @(figHandle, currentPoint)...
                set(figHandle, 'Pointer', 'cross');
            iptSetPointerBehavior(hImage, enterFcn);
            iptPointerManager(hFig);
        end
        
        %------------------------------------------------------------------
        function resetPointerBehavior(this)
            
            hFig = this.Parent;
            hImage = findobj(hFig, 'Type', 'image');
            
            iptSetPointerBehavior(hImage, []);
            iptPointerManager(hFig);
        end
        
        %------------------------------------------------------------------
        function wipeFigure(this)
            fig = this.Parent;
            if ishandle(fig)
                set(fig,'HandleVisibility','on');
                
                clf(fig); % clean out figure content
                
                zoom(fig, 'off');
                pan(fig, 'off');
                resetCallbacks();
                
                % turn off the visibility
                set(fig,'HandleVisibility','off');
            end
            
            %----------------------------------------
            function resetCallbacks
                % remove any hanging callbacks
                set(fig,'WindowButtonMotionFcn',[])
                set(fig,'WindowButtonUpFcn',[])
                set(fig,'WindowButtonDownFcn',[])
                set(fig,'WindowKeyPressFcn',[])
                set(fig,'WindowKeyReleaseFcn',[])
                
                % install listeners that temporarily block use of the
                % image browser
                iptaddcallback(fig, 'WindowButtonDownFcn',...
                    @(~,~)notify(this, 'ParentMouseClick'));
                iptaddcallback(fig, 'WindowButtonUpFcn',...
                    @(~,~)notify(this, 'ParentMouseRelease'));                
            end % end of resetCallbacks
            
        end % end of wipeFigure
        
        %------------------------------------------------------------------
        % Zoom In Button callback
        %------------------------------------------------------------------
        function doZoomIn(this, varargin)
            
            hFig = this.Parent;
            
            set(hFig,'HandleVisibility','on');
            
            if ~isempty(hFig) % if axes is in place
                this.resetPointerBehavior();
                
                % Disable zoom if enabled, to set context menus (context
                % menus cannot be set when zoom is on)
                if strcmp(this.ImageMode, 'Zoom')
                    zoom(hFig, 'off');
                end
                h = zoom(hFig);
                set(h, 'Direction', 'in');
                % create context menus
                this.installContextMenu('ZoomIn');
                zoom(hFig, 'inmode');
            end
            drawnow();
            set(hFig, 'HandleVisibility', 'off');
            this.ImageMode = 'Zoom';
        end
        
        %------------------------------------------------------------------
        % Zoom Out buttons callback
        %------------------------------------------------------------------
        function doZoomOut(this, varargin)
            
            hFig = this.Parent;
            
            set(hFig,'HandleVisibility','on');
            
            if ~isempty(hFig) % if axes is in place
                this.resetPointerBehavior();
                
                % Disable zoom if enabled, to set context menus (context
                % menus cannot be set when zoom is on)
                if strcmp(this.ImageMode, 'Zoom')
                    zoom(hFig, 'off');
                end
                h = zoom(hFig);
                set(h, 'Direction', 'out');
                % create context menus
                this.installContextMenu('ZoomOut');
                zoom(hFig, 'outmode');
            end
            drawnow();
            set(hFig,'HandleVisibility','off');
            this.ImageMode = 'Zoom';            
        end
        
        %------------------------------------------------------------------
        % Pan Button callback
        %------------------------------------------------------------------
        function doPan(this, varargin)
            
            hFig = this.Parent;
            set(hFig,'HandleVisibility','on');
            hAxes = this.ImageAxes;
            
            if ~isempty(hAxes) % if axes is in place
                drawnow();
                
                this.resetPointerBehavior();
                
                h = pan(hFig);
                this.installContextMenu('Pan');
                set(h, 'Enable', 'on');
            end
            drawnow();
            set(hFig,'HandleVisibility','off');
            this.ImageMode = 'Pan';            
        end
        
        %------------------------------------------------------------------
        % Add ROIs callback
        %------------------------------------------------------------------
        function doAddROI(this, varargin)
            
            hFig = this.Parent;
            
            set(hFig,'HandleVisibility','on');
            hAxes = this.ImageAxes;
            
            if ~isempty(hAxes) % if axes is in place
                switch this.ImageMode
                    case 'Zoom'
                        zoom(hFig, 'off');
                    case 'Pan'
                        pan(hFig, 'off');
                end
                
                this.installContextMenu('ROI');                
                this.setPointerToCross();
            end
            
            this.ImageMode = 'ROImode';
            drawnow();
            set(hFig,'HandleVisibility','off');            
        end                
    end
end