function FindCirclesGUI(inputImage)
% Detect and mark circles in an image
%
% SYNTAX:
%
% FindCirclesGUI
% Launches the GUI environment with the default image
% ('coins.png') pre-loaded.
%
% FindCirclesGUI(inputImage)
% Allows user to specify input image, or the name of an
% input image.
%
% (Note that OUTPUTS are supported via an "EXPORT" button.)
%
% CLASS SUPPORT:
% inputImage can be any valid image format readable by
% IMREAD. Color (RGB) images are supported, but circle
% detection is performed on the RGB2GRAY representation.
% (See help for IMFINDCIRCLES for details.)
%
% Note: This version _does not use_ the helper function
% VISCIRCLES, which can be very slow when a lot of
% circles are detected. Instead, it uses a custom plot
% function for visualization.
%
% Copyright The MathWorks, Inc. 2011.
% Written by Brett Shoelson, PhD
% brett.shoelson@mathworks.com
% Comments and suggestions welcome!
% V 1.0; 12/15/2011
% Copyright 2011-2012 MathWorks, Inc.
%
% See Also: IMFINDCIRCLES, VISCIRCLES
if nargin < 1
fname = 'coins.png';
inputImage = imread('coins.png');
elseif ischar(inputImage)
fname = inputImage;
inputImage = imread(inputImage);
else
fname = 'Original';
end
bgc = [0.55 0.65 0.65];
tbc = 240/255; %toolbar color, approximately
FindCirclesFig = figure(...
'numbertitle','off',...
'windowstyle','normal',...
'name','Find Circles GUI',...
'units','Normalized',...
'pos',[0.1875 0.05 0.625 0.85],...
'color',bgc,...
'menubar','none');
ht = uitoolbar(FindCirclesFig);
% tmp = ones(14,13);
% tmp(6:9,[1:3,6:8,11:13]) = 0;
% tmp = label2rgb(tmp,tbc*ones(3),[0 0 0]);
tmp = im2double(imread('file_open.png'));
tmp(tmp==0) = NaN;
loadImageTool = uitoggletool(ht,...
'CData', tmp,...
'oncallback', @GetNewFile,...
'offcallback', '',...
'Tooltipstring', 'Load new image',...
'Tag', 'loadImageTool');
tmp = im2double(imread('tool_zoom_in.png'));
tmp(tmp==0) = NaN;
zoomTool = uitoggletool(ht,...
'CData', tmp,...
'oncallback', 'zoom;set(gcbo,''state'',''off'')',...
'offcallback', '',...
'Tooltipstring', 'Toggle zoom state');
tmp = imread('distance_tool.gif');
tmp = label2rgb(tmp,tbc*ones(3),[0 0 0]);
distanceTool = uitoggletool(ht,...
'CData', tmp,...
'oncallback', 'imdistline;set(gcbo,''state'',''off'')',...
'offcallback', '',...
'Tooltipstring', 'Add IMDISTLINE Tool');
tmp = imcomplement(tmp);
delDistanceTool = uitoggletool(ht,...
'CData', tmp,...
'oncallback', @clearDistlineTools,...
'offcallback', '',...
'Tooltipstring', 'Clear IMDISTLINE Tool(s)');
tmp = ones(11);
tmp([1:3,9:13,21:23,33,89,99:101,109:113,119:121]) = 0;
tmp(6,:) = 0;tmp(:,6) = 0;
tmp2 = label2rgb(tmp,tbc*ones(3),[0 0 1]);
markObjectsTool = uitoggletool(ht,...
'CData', tmp2,...
'oncallback', @markPoints,...
'offcallback', '',...
'Tooltipstring', 'Manually count objects');
tmp2 = label2rgb(~tmp,tbc*ones(3),[0 0 1]);
markObjectsTool = uitoggletool(ht,...
'CData', tmp2,...
'oncallback', @clearMarkedPoints,...
'offcallback', '',...
'Tooltipstring', 'Clear counting marks');
set(FindCirclesFig,...
'defaultuicontrolunits','Normalized',...
'defaultuicontrolbackgroundcolor',bgc,...
'defaultuicontrolfontsize',9);
ImageAxis = axes(...
'Parent',FindCirclesFig,...
'Units','Normalized',...
'Position',[0.05 0.35 0.9 0.6 ],...
'Color',bgc,...
'Tag','ImageAxis',...
'XLimMode','auto',...
'YLimMode','auto',...
'Visible','off');
ImgObj = imshow(inputImage);
ImgTitle = title(fname);
expandAxes(ImageAxis);
%[objpos,objdim] = distributeObjects(4,0.05,0.95,0.025);
objpos = [0.05 0.28125 0.5125 0.74375];
objdim = 0.20625;
CommentsPanel = uipanel(...
'Parent',FindCirclesFig,...
'Title','Comments/Status',...
'Tag','CommentsPanel',...
'Units','Normalized',...
'Position',[objpos(1) 0.03 0.9 0.05]);
CommentsBox = uicontrol(...
'Parent',CommentsPanel,...
'Style','Edit',...
'String','Welcome to FindCirclesGUI! 2012 The MathWorks, Inc.',...
'Tag','CommentsBox',...
'Units','Normalized',...
'Fontsize',12,...
'Position',[0 -0.01 1 1.06],...
'Enable','Inactive');
SensitivityPanel = uipanel(...
'Parent',FindCirclesFig,...
'Title','Sensitivity',...
'Tag','SensitivityPanel',...
'Units','Normalized',...
'Position',[objpos(1) 0.0925 objdim 0.125]);
SensitivitySlider = sliderPanel(...
'Parent' , SensitivityPanel, ...
'Title' , '', ...
'Position', [0.05 0.05 0.9 0.9], ...
'Backgroundcolor', bgc,...
'Min' , 0, ...
'Max' , 1, ...
'Value' , 0.85, ...
'NumFormat','%0.2f',...
'Callback', @processFindCircles);
set(findobj(SensitivityPanel,'style','slider'),...
'TooltipString',...
sprintf('A high sensitivity value leads to detecting more\ncircles, including weak or partially obscured ones at\nthe risk of a higher false detection rate.\nDefault value: 0.85.'));
EdgeThresholdPanel = uipanel(...
'Parent',FindCirclesFig,...
'Title','Edge Threshold',...
'Tag','EdgeThresholdPanel',...
'Units','Normalized',...
'Position',[objpos(2) 0.0925 objdim 0.125]);
EdgeThresholdSlider = sliderPanel(...
'Parent' , EdgeThresholdPanel, ...
'Title' , '', ...
'Position', [0.05 0.25 0.9 0.7], ...
'Backgroundcolor', bgc,...
'Min' , 0, ...
'Max' , 1, ...
'Value' , 0.3, ...
'NumFormat','%0.2f',...
'Callback', @processFindCircles);
EdgeSlider = findobj(EdgeThresholdSlider,'style','slider');
set(EdgeSlider,...
'Enable','Off',...
'TooltipString',...
sprintf('Specifies the gradient threshold for determining edge pixels.\nA high EdgeThreshold value leads to detecting only those circles\nthat have relatively strong edges. A low EdgeThreshold value will,\nin addition, lead to detecting circles with relatively faint edges.\n'));
UseDefaultEdgeThresh = uicontrol(...
'Parent',EdgeThresholdPanel,...
'Callback',@toggleEdgeThreshSlider,...
'Units','Normalized',...
'Position',[0.05 0.025 0.45 0.2],...
'String','Use Default',...
'Style','checkbox',...
'Value',1,...
'Tag','UseDefaultEdgeThresh',...
'TooltipString','When checked, edge threshold will be automatically determined using Graythresh.');
VisualizationPanel = uipanel(...
'Parent',FindCirclesFig,...
'Title','Visualization Options',...
'Tag','VisualizationOptionsPanel',...
'Position',[objpos(3) 0.0925 objdim*1.3 0.125]);
LineWidthText = uicontrol(...
'Parent',VisualizationPanel,...
'Callback','',...
'Units','Normalized',...
'Position',[0.05 0.525 0.35 0.4],...
'String','Line Width',...
'Style','text',...
'HorizontalAlignment','Left',...
'Tag','LineWidthText');
lineWidthVal = 2;
LineWidthValBox = uicontrol(...
'Parent',VisualizationPanel,...
'Callback',@processFindCircles,...
'Units','Normalized',...
'Position',[0.4 0.55 0.2 0.4],...
'String',[0.5;1.0;1.5;2.0;3.0;4.0;8.0],...
'Style','popupmenu',...
'Value',lineWidthVal,...
'Fontsize',10,...
'Tag','LineWidthValBox');
LineStyleText = uicontrol(...
'Parent',VisualizationPanel,...
'Callback','',...
'Units','Normalized',...
'Position',[0.05 0.225 0.35 0.4],...
'String','Line Style',...
'Style','text',...
'HorizontalAlignment','Left',...
'Tag','LineWidthText');
LineStyleValBox = uicontrol(...
'Parent',VisualizationPanel,...
'Callback',@processFindCircles,...
'Units','Normalized',...
'Position',[0.4 0.25 0.2 0.4],...
'String',{'-','--','-.',':'},...
'Style','popupmenu',...
'Value',2,...
'Fontsize',10,...
'Tag','LineWidthValBox');
circleColor = [0 1 1];
circleColorButton = uicontrol(...
'Parent',VisualizationPanel,...
'style','pushbutton',...
'pos',[0.7 0.45 0.15 0.35],...
'cdata',reshape(kron(circleColor,ones(25,25)),25,25,3),...
'callback',@changeCircleColor);
setappdata(circleColorButton,'circleColor',circleColor);
ClearPrevious = uicontrol(...
'Parent',VisualizationPanel,...
'Callback','',...
'Units','Normalized',...
'Position',[0.05 0.025 0.45 0.2],...
'String','Clear previous circles',...
'Style','checkbox',...
'Value',1,...
'Tag','ClearPreviousBox');
UseWhiteBG = uicontrol(...
'Parent',VisualizationPanel,...
'Callback',@processFindCircles,...
'Units','Normalized',...
'Position',[0.55 0.025 0.45 0.2],...
'String','White Background',...
'Style','checkbox',...
'Value',0,...
'Tag','UseWhiteBGBox');
ObjectPolarityPanel = uibuttongroup(...
'Parent',FindCirclesFig,...
'Title','Object Polarity',...
'Tag','ObjectPolarityPanel',...
'Units','Normalized',...
'Position',[objpos(1) 0.2275 objdim 0.0625],...
'SelectedObject',[],...
'SelectionChangeFcn',@processFindCircles,...
'OldSelectedObject',[]);
BrightButton = uicontrol(...
'Parent',ObjectPolarityPanel,...
'Units','Normalized',...
'Position',[0.05 0.25 0.425 0.5],...
'String','Bright',...
'Style','radiobutton',...
'TooltipString','Circles are brighter than the background.',...
'Value',1,...
'Tag','Bright');
DarkButton = uicontrol(...
'Parent',ObjectPolarityPanel,...
'Units','Normalized',...
'Position',[0.50 0.25 0.425 0.5],...
'String','Dark',...
'Style','radiobutton',...
'TooltipString','Circles are darker than the background.',...
'Tag','Dark');
MethodPanel = uibuttongroup(...
'Parent',FindCirclesFig,...
'Title','Method',...
'Tag','MethodPanel',...
'Units','Normalized',...
'Position',[objpos(2) 0.2275 objdim 0.0625],...
'SelectedObject',[],...
'SelectionChangeFcn',@processFindCircles,...
'OldSelectedObject',[]);
PhaseCodeButton = uicontrol(...
'Parent',MethodPanel,...
'Units','Normalized',...
'Position',[0.05 0.25 0.425 0.5],...
'String','Phase Code',...
'Style','radiobutton',...
'Value',1,...
'Tag','PhaseCode',...
'Tooltipstring',...
sprintf('Specifies use of Atherton and Kerbyson''s Phase Coding method\nfor computing the accumulator array. (This is the Default.)'));
TwoStageButton = uicontrol(...
'Parent',MethodPanel,...
'Units','Normalized',...
'Position',[0.5 0.25 0.425 0.5],...
'String','Two-Stage',...
'Style','radiobutton',...
'Tag','TwoStage',...
'Tooltipstring',...
sprintf('Specifies use of the Two-stage Circular Hough Transform method\nfor computing the accumulator array.'));
ProcessOptionsPanel = uibuttongroup(...
'Parent',FindCirclesFig,...
'Title','Process Options',...
'Tag','ProcessOptionsPanel',...
'Units','Normalized',...
'Position',[0.8 0.0925 0.15 0.125],...
'SelectedObject',[],...
'SelectionChangeFcn',@processFindCircles,...
'OldSelectedObject',[]);
ProcessButton = uicontrol(...
'Parent',ProcessOptionsPanel,...
'Callback',@processFindCircles,...
'FontSize',9,...
'Units','Normalized',...
'Position',[0.05 0.4 0.9 0.3],...
'String','Process Now',...
'Tag','ProcessButton');
ExportButton = uicontrol(...
'Parent',ProcessOptionsPanel,...
'Callback',@exportResults,...
'FontSize',9,...
'Units','Normalized',...
'Position',[0.05 0.05 0.9 0.3],...
'String','Export/Save Results',...
'Tag','ProcessButton');
ProcessImmediatelyBox = uicontrol(...
'Parent',ProcessOptionsPanel,...
'Callback','',...
'Units','Normalized',...
'Position',[0.05 0.8 0.9 0.15],...
'String','Process Immediately',...
'Style','checkbox',...
'Value',1,...
'Tag','ProcessImmediatelyBox');
RadiusRangePanel = uipanel(...
'Parent',FindCirclesFig,...
'Title','Radius Range',...
'Tag','RadiusRangePanel',...
'Units','Normalized',...
'Position',[objpos(3) 0.2275 1-objpos(3)-objpos(1) 0.0625]);
MinRadiusText = uicontrol(...
'Parent',RadiusRangePanel,...
'Style','text',...
'Units','Normalized',...
'Position',[0.2 0.1 0.2 0.5],...
'String','Minimum Radius',...
'HorizontalAlignment','Left',...
'Tag','MinRadiusText');
MaximumRadiusText = uicontrol(...
'Parent',RadiusRangePanel,...
'Style','text',...
'Units','Normalized',...
'Position',[0.7 0.1 0.2 0.5],...
'String','Maximum Radius',...
'HorizontalAlignment','Left',...
'Tag','MaximumRadiusText');
MinRadiusBox = uicontrol(...
'Parent',RadiusRangePanel,...
'BackgroundColor',[1 1 1],...
'Callback',@processFindCircles,...
'FontSize',10,...
'Units','Normalized',...
'Position',[0.05 0.15 0.125 0.6],...
'String','20',...
'Style','edit',...
'Tag','MinRadiusBox');
MaxRadiusBox = uicontrol(...
'Parent',RadiusRangePanel,...
'Units','Normalized',...
'BackgroundColor',[1 1 1],...
'Callback',@processFindCircles,...
'FontSize',10,...
'Position',[0.55 0.15 0.125 0.6],...
'String','30',...
'Style','edit',...
'Tag','MaxRadiusBox');
set(findobj(FindCirclesFig,'type','uipanel'),...
'Units','Normalized',...
'BorderType', 'etchedin',...
'FontSize',8,...
'ForegroundColor',[0 0 0],...
'TitlePosition','lefttop',...
'backgroundColor',bgc)
function changeCircleColor(varargin)%circleColor =
circleColor = getappdata(circleColorButton,'circleColor');
circleColor = uisetcolor(circleColor);
set(circleColorButton,'cdata',reshape(kron(circleColor,ones(25,25)),25,25,3));
setappdata(circleColorButton,'circleColor',circleColor);
processFindCircles(gcbo)
end
function myhandle = circles(radii,centers,lineWidthVal,lineStyleVal,circColor)
% Plots multiple circles as a single line object
% Written by Brett Shoelson, PhD
resolution = 2;
theta=0:resolution:360;
x_circle = bsxfun(@times,radii,cos(theta*pi/180));
x_circle = bsxfun(@plus,x_circle,centers(:,1));
x_circle = cat(2,x_circle,nan(size(x_circle,1),1));
x_circle = x_circle';
x_circle = x_circle(:);
y_circle = bsxfun(@times,radii,sin(theta*pi/180));
y_circle = bsxfun(@plus,y_circle,centers(:,2));
y_circle = cat(2,y_circle,nan(size(y_circle,1),1));
y_circle = y_circle';
y_circle = y_circle(:);
hold on;
myhandle = plot(ImageAxis,...
x_circle,y_circle);
set(myhandle,...
'linewidth',lineWidthVal,...
'linestyle',lineStyleVal,...
'color',circColor);
end
function clearDistlineTools(varargin)
delete(findall(FindCirclesFig,'tag','imline'));
set(gcbo,'state','off');
end
function clearMarkedPoints(varargin)
delete(findall(FindCirclesFig,'tag','impoint'));
set(gcbo,'state','off');
end
function exportResults(varargin)
radii = getappdata(FindCirclesFig,'radii');
if isempty(radii)
set(CommentsBox,'string','No circles detected!');
return
end
hasCVST = exist('vision.ShapeInserter')==8;
if hasCVST
prompt={'Export/save CENTERS as:',...
'Export/save RADII as:',...
'Export/save METRIC as:',...
'Export/save IMAGE as:'};
defaultanswer={'centers','radii','metric','ImgOut'};
else
prompt={'Export/save CENTERS as:',...
'Export/save RADII as:',...
'Export/save METRIC as:'};
defaultanswer={'centers','radii','metric'};
end
name='Export/save Options (Variable Names)';
answer=inputdlg(prompt,name,1,defaultanswer);
if isempty(answer)
return
end
centers = getappdata(FindCirclesFig,'centers');
assignin('base',answer{1},centers);
assignin('base',answer{2},radii);
assignin('base',answer{3},getappdata(FindCirclesFig,'metric'));
if hasCVST
if size(inputImage,3) == 1
tmpImage = cat(3,inputImage,inputImage,inputImage);
else
tmpImage = inputImage;
end
tmpImage = im2double(tmpImage);
h = vision.ShapeInserter;
h.Shape = 'Circles';
h.Fill = false;
h.BorderColor = 'Custom';
%h.CustomBorderColor = intmax(class(inputImage))*cast(circleColor,class(inputImage));%im2uint16(circleColor);
h.CustomBorderColor = im2uint8(circleColor);
h.Antialiasing = true;
tmp = step(h,tmpImage,uint16([centers radii]));
for ii = 2:round(lineWidthVal)
tmp = step(h,tmp,uint16([centers radii+ii-1]));
end
assignin('base',answer{4},tmp);
end
MinRadius = str2double(get(MinRadiusBox,'string'));
MaxRadius = str2double(get(MaxRadiusBox,'string'));
Sensitivity = get(SensitivitySlider,'value');
UseDefaultEdge = get(UseDefaultEdgeThresh,'value');
Method = get(get(MethodPanel,'SelectedObject'),'Tag');
ObjectPolarity = get(get(ObjectPolarityPanel,'SelectedObject'),'Tag');
if UseDefaultEdge == 1
EdgeThreshold = '[]';
fprintf('[%s,%s,%s] = imfindcircles(imread(''%s''),[%d %d],...\n ''Sensitivity'',%0.2f,...\n ''EdgeThreshold'',%s,...\n ''Method'',''%s'',...\n ''ObjectPolarity'',''%s'');\n\n',...
answer{1},answer{2},answer{3},fname,MinRadius,MaxRadius,Sensitivity,EdgeThreshold,Method,ObjectPolarity)
else
EdgeThreshold = get(EdgeThresholdSlider,'value');
fprintf('[%s,%s,%s] = imfindcircles(imread(''%s''),[%d %d],...\n ''Sensitivity'',%0.2f,...\n ''EdgeThreshold'',%0.2f,...\n ''Method'',''%s'',...\n ''ObjectPolarity'',''%s'');\n\n',...
answer{1},answer{2},answer{3},fname,MinRadius,MaxRadius,Sensitivity,EdgeThreshold,Method,ObjectPolarity)
end
disp('Variables written to base workspace');
set(CommentsBox,'string',sprintf('%d circles detected. Variables written (with requested names) to base workspace.',numel(radii)));
end
function GetNewFile(varargin)
filterspec = imgformats(1);
[filename,pathname] = uigetfile(filterspec, 'Select new image.');
set(gcbo,'state','off');
if ~ischar(pathname)
return
end
fname = fullfile(pathname,filename);
inputImage = imread(fname);
cla(ImageAxis);
ImgObj = imshow(inputImage,'parent',ImageAxis);
expandAxes(ImageAxis);
ImgTitle = title(fname);
% Note: I can't figure out why the following line is
% needed, but the axis is sometimes not updating
% limits properly when I load a new image.
% (XLIMMODE, etc. are somehow manual at this point. WHY???)
%set(ImageAxis','XLimMode','manual','YLimMode','manual');
set(ImageAxis,'XLim',0.5+[0 size(inputImage,2)],'YLim',0.5+[0 size(inputImage,1)]);
processFindCircles(gcbo)
end
function markPoints(varargin)
markImagePoints(ImageAxis,'markedPoints',[0 110 110]/255);
% waitfor(FindCirclesFig,'currentcharacter')
% drawnow;
set(gcbo,'state','off');
set(FindCirclesFig,'currentcharacter','1') % double('1') = 49
while double(get(FindCirclesFig,'currentcharacter')) == 49
pause(0.5);
end
markedPoints = evalin('base','markedPoints');
set(CommentsBox,'string',sprintf('%d points marked. (Locations written to "markedPoints" in base workspace.)', size(markedPoints,1)));
end
function processFindCircles(varargin)
set(CommentsBox,'string','Processing...please wait.');
drawnow
cboTag = get(varargin{1},'tag');
processImmediately = get(ProcessImmediatelyBox,'value');
if processImmediately || strcmp(cboTag,'ProcessButton');
clearPrev = get(ClearPrevious,'value');
if clearPrev
delete(findobj(FindCirclesFig,'tag','FindCirclesVis'));
end
MinRadius = str2double(get(MinRadiusBox,'string'));
MaxRadius = str2double(get(MaxRadiusBox,'string'));
if MinRadius > MaxRadius
set(CommentsBox,'string','MinRadius must not be bigger than MaxRadius!');
return
end
Sensitivity = get(SensitivitySlider,'value');
UseDefaultEdge = get(UseDefaultEdgeThresh,'value');
if UseDefaultEdge == 1
EdgeThreshold = [];
else
EdgeThreshold = get(EdgeThresholdSlider,'value');
end
Method = get(get(MethodPanel,'SelectedObject'),'Tag');
ObjectPolarity = get(get(ObjectPolarityPanel,'SelectedObject'),'Tag');
[centers,radii,metric] = imfindcircles(inputImage,[MinRadius MaxRadius],...
'Sensitivity',Sensitivity,...
'EdgeThreshold',EdgeThreshold,...
'Method',Method,...
'ObjectPolarity',ObjectPolarity);
setappdata(FindCirclesFig,'centers',centers);
setappdata(FindCirclesFig,'radii',radii);
setappdata(FindCirclesFig,'metric',metric);
if numel(radii)>0
useWhiteBGVal = get(UseWhiteBG,'value');
%For visualization
lineWidthVal = get(LineWidthValBox,'string');
tmp = get(LineWidthValBox,'value');
lineWidthVal = str2double(lineWidthVal(tmp));
lineStyleVal = get(LineStyleValBox,'string');
tmp = get(LineStyleValBox,'value');
lineStyleVal = lineStyleVal{tmp};
circleColor = getappdata(circleColorButton,'circleColor');
if useWhiteBGVal
hndls = circles(radii,centers,...
lineWidthVal+1,'-',[1 1 1]);
set(hndls,'tag','FindCirclesVis');
end
hndls = circles(radii,centers,...
lineWidthVal,lineStyleVal,circleColor);
set(hndls,'tag','FindCirclesVis');
set(CommentsBox,'string',sprintf('%d circles detected.',numel(radii)));
else
set(CommentsBox,'string','No circles detected with these settings!');
end
else
return
end
end
function toggleEdgeThreshSlider(varargin)
if get(varargin{1},'value') == 1
set(EdgeSlider,'enable','off');
else
set(EdgeSlider,'enable','on');
end
end
end