Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
executable file 466 lines (409 sloc) 19.2 KB
classdef CROIEditor < handle
%% CROIEditor
%
% A Class with a convenient user interface to define multiple region of
% interest (ROI) on a given image.
% All the imroi tools (freehand, circle, rectangle, polygon) can be used
% to define these region(s).
%
% Loading and storing of previously defined ROI masks to and from files
% is easily done via the UI toolbar.
%
% Multiple regions are labeled using the connected component labeling
% method (using the MATLAB built-in bwlabel function).
%
% Generated Information: - Binary Mask (obj.roi)
% - Label Mask (obj.labels)
% - number of independent regions (obj.number)
%
% You can listen to the object's "MaskDefined" event to retrieve the
% ROI information generated (obj.getROIData) or get them directly from
% the objects public properties.
%
% Example usage:
% myimage = imread('eight.tif');
% roiwindow = CROIEditor(myimage);
% ...
% addlistener(roiwindow,'MaskDefined',@your_roi_defined_callback)
% ...
% function your_roi_defined_callback(h,e)
% [mask, labels, n] = roiwindow.getROIData;
% delete(roiwindow);
% end
%
% Notes:
% - if you assign an new image to the class, the window gets
% resized according to the image dimensions to have a smooth looking
% UI. Initial height can be defined (set figh).
% - if you don't like that the window is centered, remove it in
% resizeWindow function
% - you can enable/disable the ROI preview by handing over a 'nopreview'
% to the applyclick function
% - Image Processing Toolbox is needed
% - please report bugs and suggestions for improvement
%
% autor. Jonas Reber
% date. Mai 6th 2011
% email. jonas.reber at gmail dot com
% web. desperate-engineers.com
%
%%
% Copyright (c) 2011, Jonas Reber
% All rights reserved.
%
% Redistribution and use in source and binary forms, with or without
% modification, are permitted provided that the following conditions are
% met:
%
% * Redistributions of source code must retain the above copyright
% notice, this list of conditions and the following disclaimer.
% * Redistributions in binary form must reproduce the above copyright
% notice, this list of conditions and the following disclaimer in
% the documentation and/or other materials provided with the distribution
%
% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
% POSSIBILITY OF SUCH DAMAGE.
events
MaskDefined % thrown when "apply" button is hit, listen to this event
% to get the ROI information (obj.getROIData)
end
properties
image % image to work on, obj.image = theImageToWorkOn
roi % the generated ROI mask (logical)
labels % Connected component labens (multi ROI)
number % how many ROIs there are
figh = 300; % initial figure height - your image is scaled to fit.
% On change of this the window gets resized
end
properties(Access=private)
% UI stuff
guifig % mainwindow
imax % holds working area
roiax % holds roid preview image
imag % image to work on
roifig % roi image
tl % userinfo bar
figw % initial window height, this is calculated on load
hwar = 2.1; % aspect ratio
% Class stuff
loadmask % mask loaded from file
mask % mask defined by shapes
current % which shape is selected
shapes = {}; % holds all the shapes to define the mask
% load/save information
filename
pathname
end
%% Public Methods
methods
function this = CROIEditor(theImage)
% constructor
% make sure the window appears "nice" (was hard to find this
% aspect ratio to show a well aligned UI ;)
this.figw = this.figh*this.hwar;
% invoke the UI window
this.createWindow;
% load the image
if nargin > 0
this.image = theImage;
else
this.image = ones(100,100);
end
% predefine class variables
this.current = 1;
this.shapes = {}; % no shapes at start
this.filename = 'mymask'; % default filename
this.pathname = pwd; % current directory
end
function delete(this)
% destructor
delete(this.guifig);
end
function set.image(this,theImage)
% set method for image. uses grayscale images for region selection
if size(theImage,3) == 3
this.image = im2double(rgb2gray(theImage));
elseif size(theImage,3) == 1
this.image = im2double(theImage);
else
error('Unknown Image size?');
end
this.resetImages;
this.resizeWindow;
end
function set.figh(this,height)
this.figh = height;
this.figw = this.figh*this.hwar;
this.resizeWindow;
end
function [roi, labels, number] = getROIData(this,varargin)
% retrieve ROI Data
roi = this.roi;
labels = this.labels;
number = this.number;
end
end
%% private used methods
methods(Access=private)
% general functions -----------------------------------------------
function resetImages(this)
this.newROI;
% load images
this.imag = imshow(this.image,'parent',this.imax);
this.roifig = imshow(this.image,'parent',this.roiax);
% set masks to blank
this.loadmask = zeros(size(this.image));
end
function updateROI(this, a)
set(this.tl,'String','ROI not saved/applied','Visible','on','BackgroundColor',[255 182 193]./256);
this.mask = this.loadmask | zeros(size(this.image)) ;
for i=1:numel(this.shapes)
BWadd = this.shapes{i}.createMask(this.imag);
this.mask = this.mask | BWadd;
end
set(this.roifig,'CData',this.image.*this.mask);
end
function newShapeCreated(this)
set(this.shapes{end},'Tag',sprintf('imsel_%.f',numel(this.shapes)));
this.shapes{end}.addNewPositionCallback(@this.updateROI);
this.updateROI;
end
% CALLBACK FUNCTIONS
% window/figure
function winpressed(this,h,e,type)
SelObj = get(gco,'Parent');
Tag = get(SelObj,'Tag');
if and(~isempty(SelObj),strfind(Tag,'imsel_'))
this.current = str2double(regexp(Tag,'\d','match'));
for i=1:numel(this.shapes)
if i==this.current
setColor(this.shapes{i},'red');
else
setColor(this.shapes{i},'blue');
end
end
end
end
function closefig(this,h,e)
delete(this);
end;
% button callbacks ------------------------------------------------
function polyclick(this, h,e)
this.shapes{end+1} = impoly(this.imax);
this.newShapeCreated; % add tag, and callback to new shape
end
function elliclick(this, h,e)
this.shapes{end+1} = imellipse(this.imax);
this.newShapeCreated; % add tag, and callback to new shape
end
function freeclick(this,h,e)
this.shapes{end+1} = imfreehand(this.imax);
this.newShapeCreated; % add tag, and callback to new shape
end
function rectclick(this,h,e)
this.shapes{end+1} = imrect(this.imax);
this.newShapeCreated; % add tag, and callback to new shape
end
function deleteclick(this,h,e)
% delete currently selected shape
if ~isempty(this.current) && this.current > 0
n = findobj(this.imax, 'Tag',['imsel_', num2str(this.current)]);
delete(n);
% renumbering of this.shapes: (e.g. if 3 deleted: 4=>3, 5=>4,...
for i=this.current+1:numel(this.shapes)
set(this.shapes{i},'Tag',['imsel_', num2str(i-1)]);
end
this.shapes(this.current)=[];
this.current = numel(this.shapes);
this.updateROI;
else
disp('first select a shape to remove');
end
end
function applyclick(this, h, e, varargin)
set(this.tl,'String','ROI applied','Visible','on','BackgroundColor','g');
this.roi = this.mask;
[this.labels, this.number] = bwlabel(this.mask);
if~(nargin > 3 && strcmp(varargin{1},'nopreview'))
% preview window
preview = figure('MenuBar','none','Resize','off',...
'Toolbar','none','Name','Created ROI', ...
'NumberTitle','off','Color','white',...
'position',[0 0 300 300]);
movegui(preview,'center');
imshow(label2rgb(this.labels),'InitialMagnification','fit');
title({'This is your labeled ROI', ...
['you have ', num2str(this.number), ' independent region(s)']});
uicontrol('style','pushbutton',...
'string','OK!','Callback','close(gcf)');
end
notify(this, 'MaskDefined');
end
function saveROI(this, h,e)
% save Mask to File
try
[this.filename, this.pathname] = uiputfile('*.mask','Save Mask as',this.filename);
logicmask = this.mask;
save([this.pathname, this.filename],'logicmask','-mat');
set(this.tl,'String',['ROI saved: ' this.filename],'Visible','on','BackgroundColor','g');
catch
% aborted
end
end
function openROI(this, h,e)
% load Mask from File
this.newROI; % delete stuff
[this.filename,this.pathname,~] = uigetfile('*.mask');
try
b = load([this.pathname,this.filename],'-mat');
if size(b.logicmask)~=size(this.image)
set(this.tl,'String',['Size not matching! ' this.filename],'Visible','on','BackgroundColor','r');
else
this.loadmask = b.logicmask;
this.updateROI;
set(this.tl,'String',['Current: ' this.filename],'Visible','on','BackgroundColor','g');
end
catch
% aborted
end
end
function newROI(this, h,e)
this.mask = zeros(size(this.image));
this.loadmask = zeros(size(this.image));
% remove all the this.shapes
for i=1:numel(this.shapes)
delete(this.shapes{i});
end
this.current = 1; % defines the currently selected shape - start with 1
this.shapes = {}; % reset shape holder
this.updateROI;
end
% UI FUNCTIONS ----------------------------------------------------
function createWindow(this, w, h)
this.guifig=figure('MenuBar','none','Resize','on','Toolbar','none','Name','Analyzer - ROI Editor', ...
'NumberTitle','off','Color','white', 'units','pixels','position',[0 0 this.figw this.figh],...
'CloseRequestFcn',@this.closefig, 'visible','off');
% buttons
buttons = [];
buttons(end+1) = uicontrol('Parent',this.guifig,'String','Polygon',...
'units','normalized',...
'Position',[0.01 0.8 0.15 0.15], ...
'Callback',@(h,e)this.polyclick(h,e));
buttons(end+1) = uicontrol('Parent',this.guifig,'String','Ellipse',...
'units','normalized',...
'Position',[0.01 0.65 0.15 0.15],...
'Callback',@(h,e)this.elliclick(h,e));
buttons(end+1) = uicontrol('Parent',this.guifig,'String','Freehand',...
'units','normalized',...
'Position',[0.01 0.5 0.15 0.15],...
'Callback',@(h,e)this.freeclick(h,e));
buttons(end+1) = uicontrol('Parent',this.guifig,'String','Rectangle',...
'units','normalized',...
'Position',[0.01 0.35 0.15 0.15],...
'Callback',@(h,e)this.rectclick(h,e));
buttons(end+1) = uicontrol('Parent',this.guifig,'String','Delete',...
'units','normalized',...
'Position',[0.01 0.2 0.15 0.15],...
'Callback',@(h,e)this.deleteclick(h,e));
buttons(end+1) = uicontrol('Parent',this.guifig,'String','Apply',...
'units','normalized',...
'Position',[0.01 0.05 0.15 0.15],...
'Callback',@(h,e)this.applyclick(h,e));
% axes
this.imax = axes('parent',this.guifig,'units','normalized','position',[0.18 0.07 0.4 0.87]);
this.roiax = axes('parent',this.guifig,'units','normalized','position',[0.59 0.07 0.4 0.87]);
linkaxes([this.imax this.roiax]);
% create toolbar
this.createToolbar(this.guifig);
% add listeners
set(this.guifig,'WindowButtonDownFcn',@(h,e)this.winpressed(h,e,'down'));
set(this.guifig,'WindowButtonUpFcn',@(h,e)this.winpressed(h,e,'up')) ;
% axis titles
uicontrol('tag','txtimax','style','text','string','Working Area','units','normalized',...
'position',[0.18 0.95 0.4 0.05], ...
'BackgroundColor','w');
uicontrol('tag','txtroiax','style','text','string','ROI Preview','units','normalized',...
'position',[0.59 0.95 0.4 0.05], ...
'BackgroundColor','w');
% file load info
this.tl = uicontrol('tag','txtfileinfo','style','text','string','','units','normalized',...
'position',[0.18 0.01 0.81 0.05], ...
'BackgroundColor','g','visible','off');
end
function resizeWindow(this)
[h,w]=size(this.image);
f = w/h;
this.figw = this.figh*this.hwar*f;
set(this.guifig,'position',[0 0 this.figw this.figh]);
movegui(this.guifig,'center');
set(this.guifig,'visible','on');
end
function tb=createToolbar(this, fig)
tb = uitoolbar('parent',fig);
hpt=[];
hpt(end+1) = uipushtool(tb,'CData',localLoadIconCData('file_new.png'),...
'TooltipString','New ROI',...
'ClickedCallback',...
@this.newROI);
hpt(end+1) = uipushtool(tb,'CData',localLoadIconCData('file_open.png'),...
'TooltipString','Open ROI',...
'ClickedCallback',...
@this.openROI);
hpt(end+1) = uipushtool(tb,'CData',localLoadIconCData('file_save.png'),...
'TooltipString','Save ROI',...
'ClickedCallback',...
@this.saveROI);
%---
hpt(end+1) = uitoggletool(tb,'CData',localLoadIconCData('tool_zoom_in.png'),...
'TooltipString','Zoom In',...
'ClickedCallback',...
'putdowntext(''zoomin'',gcbo)',...
'Separator','on');
hpt(end+1) = uitoggletool(tb,'CData',localLoadIconCData('tool_zoom_out.png'),...
'TooltipString','Zoom Out',...
'ClickedCallback',...
'putdowntext(''zoomout'',gcbo)');
hpt(end+1) = uitoggletool(tb,'CData',localLoadIconCData('tool_hand.png'),...
'TooltipString','Pan',...
'ClickedCallback',...
'putdowntext(''pan'',gcbo)');
end
end % end private methods
end
% this is copied from matlabs uitoolfactory.m, to load the icons for the toolbar
function cdata = localLoadIconCData(filename)
% Loads CData from the icon files (PNG, GIF or MAT) in toolbox/matlab/icons.
% filename = info.icon;
% Load cdata from *.gif file
persistent ICONROOT
if isempty(ICONROOT)
ICONROOT = fullfile(matlabroot,'toolbox','matlab','icons',filesep);
end
if length(filename)>3 && strncmp(filename(end-3:end),'.gif',4)
[cdata,map] = imread([ICONROOT,filename]);
% Set all white (1,1,1) colors to be transparent (nan)
ind = map(:,1)+map(:,2)+map(:,3)==3;
map(ind) = NaN;
cdata = ind2rgb(cdata,map);
% Load cdata from *.png file
elseif length(filename)>3 && strncmp(filename(end-3:end),'.png',4)
[cdata map alpha] = imread([ICONROOT,filename],'Background','none');
% Converting 16-bit integer colors to MATLAB colorspec
cdata = double(cdata) / 65535.0;
% Set all transparent pixels to be transparent (nan)
cdata(alpha==0) = NaN;
% Load cdata from *.mat file
else
temp = load([ICONROOT,filename],'cdata');
cdata = temp.cdata;
end
end