{"id":662,"date":"2012-09-04T16:02:15","date_gmt":"2012-09-04T20:02:15","guid":{"rendered":"https:\/\/blogs.mathworks.com\/steve\/?p=662"},"modified":"2019-10-31T14:59:26","modified_gmt":"2019-10-31T18:59:26","slug":"detecting-circular-objects-in-images","status":"publish","type":"post","link":"https:\/\/blogs.mathworks.com\/steve\/2012\/09\/04\/detecting-circular-objects-in-images\/","title":{"rendered":"Detecting Circular Objects in Images"},"content":{"rendered":"<div class=\"content\"><!--introduction--><p><i>Today's blog post was written by Spandan Tiwari, a developer on the Image Processing Toolbox team. Thanks, Spandan!<\/i><\/p><p>This example shows how you can use <tt>imfindcircles<\/tt> to automatically detect circles or circular objects in an image. It also shows how you can use <tt>viscircles<\/tt> to visualize the detected circles.<\/p><!--\/introduction--><p>I was looking for an interesting image to show the use of <tt>imfindcircles<\/tt> when my fellow Mathworker Ashish reminded of the delicious M&amp;M image from Steve's earlier post: <a href=\"https:\/\/blogs.mathworks.com\/steve\/2010\/12\/17\/what-color-is-green\">What color is green?<\/a><\/p><pre class=\"codeinput\">url = <span class=\"string\">'https:\/\/blogs.mathworks.com\/images\/steve\/2010\/mms.jpg'<\/span>;\r\nrgb = imread(url);\r\nimshow(rgb)\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_01.jpg\" alt=\"\"> <p>Besides having plenty of <i>sweet<\/i> circles to detect, there are a few interesting things going on in this image from a circle detection point-of-view:<\/p><div><ol><li>There are M&amp;Ms of different colors, which have different contrasts with respect to the background. On one end, the black ones clearly stand out on this background. On the other end, the yellow M&amp;Ms do not contrast well with the background.<\/li><li>Notice how several M&amp;Ms are close together and almost touching each other. Touching and overlapping object boundaries is usually a challenging scenario for object detection.<\/li><\/ol><\/div><p>So let's see how we can use <tt>imfindcircles<\/tt> to find the M&amp;Ms.<\/p><p>First we need to specify a radius range to search for the circles. A quick way to find the appropriate radius range is to use the interactive tool <tt>imdistline<\/tt> to get an approximate estimate of the radii of various objects.<\/p><pre class=\"codeinput\">d = imdistline;\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_02.jpg\" alt=\"\"> <p><tt>imdistline<\/tt> creates a draggable tool that can be moved to fit across an M&amp;M and read off the numbers to get an idea of the radius. Most M&amp;Ms have radius in the range of 16-19 pixels. I will use a slightly larger range of 15-20 pixels just to be sure. But before that let's remove the <tt>imdistline<\/tt> tool.<\/p><pre class=\"codeinput\">delete(d);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_03.jpg\" alt=\"\"> <p>So, let's call <tt>imfindcircles<\/tt> on this image with the search radius of [15 20] pixels and see what we get. Before we do that, it is a good practice to ask whether the objects are brighter or darker than the background. This is needed because, by default, <tt>imfindcircles<\/tt> finds circular objects that are brighter than the background. In scenarios where the objects of interest are darker than the background, we need to set the parameter 'ObjectPolarity' to 'dark'. To answer whether the M&amp;Ms in our image are brighter or darker than the background, let's look at the grayscale version of the image.<\/p><pre class=\"codeinput\">gray_image = rgb2gray(rgb);\r\nimshow(gray_image);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_04.jpg\" alt=\"\"> <p>We see that the background is quite bright and most of the M&amp;Ms are probably darker than the background. So we will call <tt>imfindcircles<\/tt> with the parameter 'ObjectPolarity' set to 'dark'.<\/p><pre class=\"codeinput\">[centers, radii] = imfindcircles(rgb,[15 20],<span class=\"string\">'ObjectPolarity'<\/span>,<span class=\"string\">'dark'<\/span>)\r\n<\/pre><pre class=\"codeoutput\">\r\ncenters =\r\n\r\n     []\r\n\r\n\r\nradii =\r\n\r\n     []\r\n\r\n<\/pre><p>Well, the outputs <tt>centers<\/tt> and <tt>radii<\/tt> are empty, which means that no circles were found. This happens frequently because <tt>imfindcircles<\/tt> is a circle <i>detector<\/i>, and similar to most detectors, <tt>imfindcircles<\/tt> has an internal <i>detection threshold<\/i> that determines its sensitivity. In simple terms it means that the detector's confidence in a certain (circle) detection has to be greater than a certain level before it is considered a <i>valid<\/i> detection. <tt>imfindcircles<\/tt> has a parameter 'Sensitivity' which can be used to control this internal threshold, and consequently, the sensitivity of the algorithm. This is similar to the sensitivity control on the motion detectors used in home security systems. Coming back to our M&amp;M image, it is possible that at the default sensitivity level all the circles are lower than the internal threshold, which is why we don't see any detections. By default, 'Sensitivity', which is a number between [0-1], is set to 0.85. So let's try increase 'Sensitivity' to say 0.9.<\/p><pre class=\"codeinput\">[centers, radii] = imfindcircles(rgb,[15 20],<span class=\"string\">'ObjectPolarity'<\/span>,<span class=\"string\">'dark'<\/span>,<span class=\"string\">'Sensitivity'<\/span>,0.9)\r\n<\/pre><pre class=\"codeoutput\">\r\ncenters =\r\n\r\n   97.2101  108.7405\r\n  349.1440   44.8708\r\n  430.4268  243.4405\r\n  368.8914  210.2235\r\n  167.4823   48.9834\r\n  256.0455  280.1922\r\n  356.3265   86.3372\r\n  191.0240  357.9989\r\n  305.4986  190.8765\r\n  210.4295  207.9475\r\n  121.3942  141.0331\r\n  396.4924  184.1435\r\n  444.3143  299.8000\r\n  363.3456  247.3126\r\n  461.6538  208.0843\r\n\r\n\r\nradii =\r\n\r\n   18.5272\r\n   18.1409\r\n   18.2202\r\n   18.0592\r\n   18.8884\r\n   17.7121\r\n   18.6441\r\n   17.7396\r\n   17.7436\r\n   18.2228\r\n   17.9140\r\n   18.1599\r\n   17.8539\r\n   18.5682\r\n   17.8709\r\n\r\n<\/pre><p>OK, so now we found some circles - 15 to be precise. <tt>centers<\/tt> contains the locations of circle centers and <tt>radii<\/tt> contains the estimated radii of those circles.<\/p><p>We can use function <tt>viscircles<\/tt>, which was also shipped in R2012a, to draw these circles on the image. The variables <tt>centers<\/tt> and <tt>radii<\/tt> can be passed directly to <tt>viscircles<\/tt>.<\/p><pre class=\"codeinput\">imshow(rgb);\r\n\r\nh = viscircles(centers,radii);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_05.jpg\" alt=\"\"> <p>The circle centers seem correctly positioned and their corresponding radii seem to match well to the actual M&amp;Ms. But still we are missing quite a few of them. What's up with that?<\/p><p>Let's try to increase 'Sensitivity' even more, to 0.95, and see what happens.<\/p><pre class=\"codeinput\">[centers, radii] = imfindcircles(rgb,[15 20],<span class=\"string\">'ObjectPolarity'<\/span>,<span class=\"string\">'dark'<\/span>,<span class=\"string\">'Sensitivity'<\/span>,0.95);\r\nsize(centers)\r\n<\/pre><pre class=\"codeoutput\">\r\nans =\r\n\r\n    34     2\r\n\r\n<\/pre><p>So increasing 'Sensitivity' gets us 34 circles. Let's plot these circles on the image again.<\/p><pre class=\"codeinput\">delete(h);\r\nh = viscircles(centers,radii);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_06.jpg\" alt=\"\"> <p>Much better.<\/p><p>Now, under the hood, <tt>imfindcircles<\/tt> has two different methods for finding circles. So far we have been using the default method, which is the <i>phase coding<\/i> method. There's another method, popularly called the <i>two-stage<\/i> method, that is available in <tt>imfindcircles<\/tt>. Let's try to use the two-stage method and see what we get.<\/p><pre class=\"codeinput\">[centers, radii, metric] = imfindcircles(rgb,[15 20], <span class=\"keyword\">...<\/span>\r\n    <span class=\"string\">'ObjectPolarity'<\/span>,<span class=\"string\">'dark'<\/span>,<span class=\"string\">'Sensitivity'<\/span>,0.95,<span class=\"string\">'Method'<\/span>,<span class=\"string\">'twostage'<\/span>);\r\n\r\ndelete(h);\r\n\r\nh = viscircles(centers,radii);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_07.jpg\" alt=\"\"> <p>OK, great! The two-stage method is detecting more circles, at the Sensitivity of 0.95.<\/p><p>In general, these two method are complementary in that have they have different strengths. In my experience, phase coding method is typically faster and slightly more robust to noise than the two-stage method. But it can also need higher 'Sensitivity' levels to get the same number of detections as the two-stage method as we saw here.<\/p><p>Coming back to our image, it is curious that we are not picking up the yellow M&amp;Ms in the image. Hmmm, the yellow M&amp;Ms do not have a strong contrast with the background. In fact they seem to have very similar intensities as the background. Is it possible that the yellow M&amp;Ms are not really 'darker' than the background as we are assuming. Let's check that out by looking at the grayscale version of this image again.<\/p><pre class=\"codeinput\">imshow(gray_image);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_08.jpg\" alt=\"\"> <p>Indeed! The yellow M&amp;Ms are almost the same intensity, maybe even brighter, as compared to the background. This suggests that we should change 'ObjectPolarity' to 'bright' to detect the yellow ones.<\/p><pre class=\"codeinput\">[centersBright, radiiBright] = imfindcircles(rgb,[15 20],<span class=\"string\">'ObjectPolarity'<\/span>,<span class=\"string\">'bright'<\/span>,<span class=\"string\">'Sensitivity'<\/span>,0.95);\r\n<\/pre><p>And let's draw the bright circles in a different color, say blue, by changing the 'EdgeColor' parameter in <tt>viscircles<\/tt>.<\/p><pre class=\"codeinput\">imshow(rgb);\r\nhBright = viscircles(centersBright, radiiBright,<span class=\"string\">'EdgeColor'<\/span>,<span class=\"string\">'b'<\/span>);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_09.jpg\" alt=\"\"> <p>OK, so we got one of them. That's a start! These yellow ones are hard to find because of they don't <i>stand out<\/i> as well as others on this background. Let me introduce you to another parameter which might help us here - 'EdgeThreshold'. To find circles, <tt>imfindcircles<\/tt> uses only the edge pixels in the image. These edge pixels are essentially pixels with high gradient value. The 'EdgeThreshold' parameter controls how <i>high<\/i> the gradient value at a pixel has to be before it is considered an edge pixel and included in computation. A high value (closer to 1) of this parameter will allow only the strong edges (higher gradient values) to be included, whereas a low value (closer to 0) is more permissive and includes even the weaker edges (lower gradient values) in computation. In case of yellow M&amp;Ms, since the contrast is low, we expect the boundary pixels (on the circumference of the M&amp;M) to have low gradient values. So let's try to lower the 'EdgeThreshold' parameter to ensure that they are included in computation.<\/p><pre class=\"codeinput\">[centersBright, radiiBright, metricBright] = imfindcircles(rgb,[15 20], <span class=\"keyword\">...<\/span>\r\n    <span class=\"string\">'ObjectPolarity'<\/span>,<span class=\"string\">'bright'<\/span>,<span class=\"string\">'Sensitivity'<\/span>,0.95,<span class=\"string\">'EdgeThreshold'<\/span>,0.1);\r\n\r\ndelete(hBright);\r\nhBright = viscircles(centersBright, radiiBright,<span class=\"string\">'EdgeColor'<\/span>,<span class=\"string\">'b'<\/span>);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_10.jpg\" alt=\"\"> <p>All right! We are now picking up most of the yellow ones, and some green ones too. Let's also draw the other M&amp;Ms, which we found earlier, in red.<\/p><pre class=\"codeinput\">h = viscircles(centers,radii);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_11.jpg\" alt=\"\"> <p>We are still missing a few of them, such as the yellow one of the left top corner. I am sure we can adjust the parameters to pick up on those as well. However, at some point on our journey to find all the M&amp;Ms we will see some false circles showing up (M&amp;Ms where there are none!). This highlights that there is always a trade-off between how many true circles you can find (detection rate) and how many false circles you get with them (false alarm rate). Both go hand in hand, i.e., getting more of the actual circles means a greater likelihood of getting false circles.<\/p><p>So, now that we have most of our circles let's have some fun. Let's try and draw these M&amp;Ms in their own respective colors.<\/p><p>First let's collect all our circle centers and radii together.<\/p><pre class=\"codeinput\">centers = round([centers; centersBright]);\r\nradii = [radii; radiiBright];\r\n<\/pre><p>Also, let's smooth our image to remove some of the lighting and writing artifacts (the 'm's) from our M&amp;Ms.<\/p><pre class=\"codeinput\">filt_rgb = imfilter(rgb,ones(5)\/25);\r\n<\/pre><p>Now we will extract the colors at the center of the circles. For this first I make a list of all the elements of the array that I want to get. Remember the center locations are stored in <tt>centers<\/tt>. But I want to get the color value at these locations, which means that I have to extract the values from all the three planes (along the third dimension) of this RGB image at those center locations.<\/p><p>To give an example, let's say we have two circles with centers   example_centers = [10 20 ;                      40 50 ]<\/p><p>So to get the color values we need to get the values from the following elements of the matrix. Here each row shows the element location in the 3-dimensional image matrix.<\/p><pre class=\"language-matlab\">locs = [10 20 1 ;\r\n        40 50 1 ;\r\n        10 20 2 ;\r\n        40 50 2 ;\r\n        10 20 3 ;\r\n        40 50 3 ]\r\n<\/pre><p>There are many different ways you can do this in MATLAB. I like to do it using <tt>repmat<\/tt> and a simple trick using <tt>kron<\/tt>, which is the function for computing Kronecker product of two matrices.<\/p><pre class=\"codeinput\">example_centers = [10 20; 40 50];\r\nlocs = [repmat(example_centers,3,1) <span class=\"keyword\">...<\/span>\r\n     kron((1:3)',ones(size(example_centers,1),1))]\r\n<\/pre><pre class=\"codeoutput\">\r\nlocs =\r\n\r\n    10    20     1\r\n    40    50     1\r\n    10    20     2\r\n    40    50     2\r\n    10    20     3\r\n    40    50     3\r\n\r\n<\/pre><p>Similarly we can make a list of all the elements we need to extract for all the circle centers in <tt>centers<\/tt>. Note that the first and second columns of <tt>centers<\/tt> have the column and row location of the circle centers, respectively. Since we want the first column in 'locs' to be the row location we switch the columns of <tt>centers<\/tt> using <tt>fliplr<\/tt>.<\/p><pre class=\"codeinput\">locs = [repmat(fliplr(centers),3,1) kron((1:3)',ones(size(centers,1),1))];\r\n<\/pre><p>Next, we convert these locations to linear indices using <tt>sub2ind<\/tt>.<\/p><pre class=\"codeinput\">center_idx = sub2ind(size(rgb),locs(:,1),locs(:,2),locs(:,3));\r\n<\/pre><p>Finally we extract these elements from the smoothed M&amp;M image.<\/p><pre class=\"codeinput\">cent_color = filt_rgb(center_idx);\r\n<\/pre><p>Since 'cent_color' is a vector, we reshape it to get it in 3-column format where the three columns correspond to R, G, and B colors for the circle centers.<\/p><pre class=\"codeinput\">cent_color = reshape(cent_color,size(centers,1),3);\r\n<\/pre><p>Now that we have the colors corresponding to each circle, we can plot these circles with the right colors. Remember to normalize the colors between [0-1] for plotting.<\/p><pre class=\"codeinput\">imshow(rgb)\r\n\r\n<span class=\"keyword\">for<\/span> i = 1:size(centers,1)\r\n    viscircles(centers(i,:),radii(i),<span class=\"string\">'EdgeColor'<\/span>,double(cent_color(i,:))\/255);\r\n<span class=\"keyword\">end<\/span>\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_12.jpg\" alt=\"\"> <p> That looks about right. If you look closely, some circles are not\r\nexactly the same color as the M&Ms. This is because of there is variation\r\nin the color even within an M&M. This variation can be seen on closer\r\nlook, as shown in Steve's post <a\r\nhref=\"https:\/\/blogs.mathworks.com\/steve\/2010\/12\/17\/what-color-is-green\">What\r\ncolor is green?<\/a>. <\/p><p>Now that I have the circle colors extracted out what else can I do? Maybe I can simulate an M&amp;M image of my own. Hmmm, what do I need to do for that? I see broadly two steps for doing this. First step is to <i>paint<\/i> the M&amp;Ms with the right colors at the right locations. Second step is to fill in the background color.<\/p><p>For the first step of painting the M&amp;Ms, we will use a simple trick from Linear Systems theory. Linear systems theory tells us that when we convolve a function with a shifted impulse (Dirac delta) function, we get the same function shifted to the location of the impulse, i.e. $f(t)*d(t-T) = f(t-T)$, where $f$ is the function, $d$ is the impulse function, and $*$ denotes convolution. Essentially, convolving a function with an impulse creates a copy of the function centered at the impulse location.<\/p><p>The first thing we need to do is create a blank canvas similar to our original image.<\/p><pre class=\"codeinput\">cartoon_rgb = zeros(size(rgb),class(rgb));\r\n<\/pre><p>Next we fill in the right color values at the center locations for all the M&amp;Ms in the image.<\/p><pre class=\"codeinput\">cartoon_rgb(center_idx) = cent_color(:);\r\nimshow(cartoon_rgb)\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_13.jpg\" alt=\"\"> <p>These individual pixels of different colors are like digital 'impulses', (single-pixel wide, height corresponding to a color). Next we need to make a 'function', which in our case is just a circular mask (because the M&amp;Ms are circular), to convolve with these impulses and make a copy of at each of these impulse locations. For simplicity, I use a circle of radius 17 pixels for all the M&amp;Ms even though they have slightly different radii.<\/p><pre class=\"codeinput\">radius = 17;\r\ncirc_mask = double(getnhood(strel(<span class=\"string\">'ball'<\/span>,radius,radius,0)));\r\nimshow(circ_mask, <span class=\"string\">'InitialMagnification'<\/span>, 500)\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_14.jpg\" alt=\"\"> <p>Finally we convolve the image with the <i>colored<\/i> impulses with the circular mask to paint the M&amp;Ms at the right locations.<\/p><pre class=\"codeinput\">cartoon_rgb = imfilter(cartoon_rgb,circ_mask,<span class=\"string\">'conv'<\/span>);\r\nimshow(cartoon_rgb)\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_15.jpg\" alt=\"\"> <p>A word of caution about this trick here. You might have noticed we are working with multiple <i>impulses<\/i> simultaneously, i.e. we are not convolving the circular mask with just one impulse but a string of impulses. We are getting away with that because our impulses (circles) are well-separated (more than 2 x radius = 34 pixels apart) and the circles don't overlap. If that was not the case, we would see unwanted colors in the overlapping part of the circles due to superpositioning of the masks.<\/p><p>So now we have painted the colored M&amp;Ms. All we have to do now is to paint the background. I just pick an RGB color to match the background.<\/p><pre class=\"codeinput\">bg_color = [235; 218; 175];\r\n<\/pre><p>Next we need to find all the pixels that are part of the background. We do this by using the same convolution trick we used. We make an image with the 'impulses'.<\/p><pre class=\"codeinput\">bw = false(size(rgb,1),size(rgb,2));\r\nbw(sub2ind(size(bw),centers(:,2),centers(:,1))) = true;\r\n<\/pre><p>Then we convolve this image with a circular mask to get circles painted. Then we complement the image to highlight the background (in white) and record the locations (linear indices) of the pixels belonging to the background.<\/p><pre class=\"codeinput\">bw = imfilter(bw,circ_mask);\r\nbw = imcomplement(bw);\r\nimshow(bw)\r\nbg_idx_gray = find(bw);\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_16.jpg\" alt=\"\"> <p>Now we will fill these pixels locations with the background color. Since this is a color image we need to do some work to fill the colors. We do this is in a way similar to what to we did to extract the colors for the circle center locations. First we find the linear indices of all the elements in the color image matrix that need to be filled.<\/p><pre class=\"codeinput\">bg_idx_color = [bg_idx_gray; numel(bw)+bg_idx_gray; 2*numel(bw)+bg_idx_gray];\r\n<\/pre><p>Next we make a vector that has the color values that go into those elements. We do this by repeating the color values in <tt>bg_color<\/tt> once for each pixel location.<\/p><pre class=\"codeinput\">bg_fill_values = kron(bg_color,ones(size(bg_idx_gray)));\r\n<\/pre><p>Finally we fill the color values in the background locations.<\/p><pre class=\"codeinput\">cartoon_rgb(bg_idx_color) = bg_fill_values;\r\n<\/pre><p>That's it. We have made our simulated M&amp;M image and now let's see how it has come out. To view this, I will use a new function, <tt>imshowpair<\/tt>, introduced in R2012a. It has some great options for viewing and comparing two images. I will use the 'montage' option to view the original and simulated M&amp;M images side-by-side. There are several other cool options in <tt>imshowpair<\/tt>, such as 'blend', which is my favorite. You should check it out.<\/p><p>So here you go: spot the difference!<\/p><pre class=\"codeinput\">imshowpair(rgb,cartoon_rgb,<span class=\"string\">'montage'<\/span>);\r\ntitle(<span class=\"string\">'Spot the difference!'<\/span>);\r\naxis <span class=\"string\">off<\/span>;\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2012\/finding_circles_spandan_17.jpg\" alt=\"\"> <p>Happy circle hunting!<\/p><script language=\"JavaScript\"> <!-- \r\n    function grabCode_2a11a2e4d3a24e388d9b12545149b321() {\r\n        \/\/ Remember the title so we can use it in the new page\r\n        title = document.title;\r\n\r\n        \/\/ Break up these strings so that their presence\r\n        \/\/ in the Javascript doesn't mess up the search for\r\n        \/\/ the MATLAB code.\r\n        t1='2a11a2e4d3a24e388d9b12545149b321 ' + '##### ' + 'SOURCE BEGIN' + ' #####';\r\n        t2='##### ' + 'SOURCE END' + ' #####' + ' 2a11a2e4d3a24e388d9b12545149b321';\r\n    \r\n        b=document.getElementsByTagName('body')[0];\r\n        i1=b.innerHTML.indexOf(t1)+t1.length;\r\n        i2=b.innerHTML.indexOf(t2);\r\n \r\n        code_string = b.innerHTML.substring(i1, i2);\r\n        code_string = code_string.replace(\/REPLACE_WITH_DASH_DASH\/g,'--');\r\n\r\n        \/\/ Use \/x3C\/g instead of the less-than character to avoid errors \r\n        \/\/ in the XML parser.\r\n        \/\/ Use '\\x26#60;' instead of '<' so that the XML parser\r\n        \/\/ doesn't go ahead and substitute the less-than character. \r\n        code_string = code_string.replace(\/\\x3C\/g, '\\x26#60;');\r\n\r\n        copyright = 'Copyright 2012 The MathWorks, Inc.';\r\n\r\n        w = window.open();\r\n        d = w.document;\r\n        d.write('<pre>\\n');\r\n        d.write(code_string);\r\n\r\n        \/\/ Add copyright line at the bottom if specified.\r\n        if (copyright.length > 0) {\r\n            d.writeln('');\r\n            d.writeln('%%');\r\n            if (copyright.length > 0) {\r\n                d.writeln('% _' + copyright + '_');\r\n            }\r\n        }\r\n\r\n        d.write('<\/pre>\\n');\r\n\r\n        d.title = title + ' (MATLAB code)';\r\n        d.close();\r\n    }   \r\n     --> <\/script><p style=\"text-align: right; font-size: xx-small; font-weight:lighter;   font-style: italic; color: gray\"><br><a href=\"javascript:grabCode_2a11a2e4d3a24e388d9b12545149b321()\"><span style=\"font-size: x-small;        font-style: italic;\">Get \r\n      the MATLAB code <noscript>(requires JavaScript)<\/noscript><\/span><\/a><br><br>\r\n      Published with MATLAB&reg; 7.14<br><\/p><p class=\"footer\"><br>\r\n      Published with MATLAB&reg; 7.14<br><\/p><\/div><!--\r\n2a11a2e4d3a24e388d9b12545149b321 ##### SOURCE BEGIN #####\r\n%% Detecting Circular Objects in Images\r\n%\r\n% _Today's blog post was written by Spandan Tiwari, a developer on the\r\n% Image Processing Toolbox team. Thanks, Spandan!_\r\n%\r\n% This example shows how you can use |imfindcircles| to automatically\r\n% detect circles or circular objects in an image. It also shows how you can\r\n% use |viscircles| to visualize the detected circles.\r\n\r\n%% \r\n% I was looking for an interesting image to show the use of |imfindcircles|\r\n% when my fellow Mathworker Ashish reminded of the delicious M&M image from\r\n% Steve's earlier post:\r\n% <https:\/\/blogs.mathworks.com\/steve\/2010\/12\/17\/what-color-is-green What\r\n% color is green?>\r\n\r\n%% \r\nurl = 'https:\/\/blogs.mathworks.com\/images\/steve\/2010\/mms.jpg';\r\nrgb = imread(url);\r\nimshow(rgb)\r\n\r\n%% \r\n% Besides having plenty of _sweet_ circles to detect, there are a few\r\n% interesting things going on in this image from a circle detection\r\n% point-of-view: \r\n% \r\n% # There are M&Ms of different colors, which have different contrasts with\r\n% respect to the background. On one end, the black ones clearly stand out\r\n% on this background. On the other end, the yellow M&Ms do not contrast\r\n% well with the background.\r\n% # Notice how several M&Ms are close together and almost touching each\r\n% other. Touching and overlapping object boundaries is usually a\r\n% challenging scenario for object detection.\r\n% \r\n% So let's see how we can use |imfindcircles| to find the M&Ms. \r\n\r\n%% \r\n% First we need to specify a radius range to search for the circles. A\r\n% quick way to find the appropriate radius range is to use the interactive\r\n% tool |imdistline| to get an approximate estimate of the radii of various\r\n% objects.\r\nd = imdistline;\r\n\r\n\r\n%% \r\n% |imdistline| creates a draggable tool that can be moved to fit across an\r\n% M&M and read off the numbers to get an idea of the radius. Most M&Ms have\r\n% radius in the range of 16-19 pixels. I will use a slightly larger range\r\n% of 15-20 pixels just to be sure. But before that let's remove the\r\n% |imdistline| tool.\r\ndelete(d);\r\n\r\n%%\r\n% So, let's call |imfindcircles| on this image with the search radius of\r\n% [15 20] pixels and see what we get. Before we do that, it is a good\r\n% practice to ask whether the objects are brighter or darker than the\r\n% background. This is needed because, by default, |imfindcircles| finds\r\n% circular objects that are brighter than the background. In scenarios\r\n% where the objects of interest are darker than the background, we need to\r\n% set the parameter 'ObjectPolarity' to 'dark'. To answer whether the M&Ms\r\n% in our image are brighter or darker than the background, let's look at\r\n% the grayscale version of the image.\r\n\r\ngray_image = rgb2gray(rgb);\r\nimshow(gray_image);\r\n\r\n%%\r\n% We see that the background is quite bright and most of the M&Ms are\r\n% probably darker than the background. So we will call |imfindcircles| with\r\n% the parameter 'ObjectPolarity' set to 'dark'.\r\n\r\n[centers, radii] = imfindcircles(rgb,[15 20],'ObjectPolarity','dark')\r\n\r\n%%\r\n% Well, the outputs |centers| and |radii| are empty, which means that no\r\n% circles were found. This happens frequently because |imfindcircles| is a\r\n% circle _detector_, and similar to most detectors, |imfindcircles| has an\r\n% internal _detection threshold_ that determines its sensitivity. In simple\r\n% terms it means that the detector's confidence in a certain (circle)\r\n% detection has to be greater than a certain level before it is considered\r\n% a _valid_ detection. |imfindcircles| has a parameter 'Sensitivity' which\r\n% can be used to control this internal threshold, and consequently, the\r\n% sensitivity of the algorithm. This is similar to the sensitivity control\r\n% on the motion detectors used in home security systems. Coming back to our\r\n% M&M image, it is possible that at the default sensitivity level all the\r\n% circles are lower than the internal threshold, which is why we don't see\r\n% any detections. By default, 'Sensitivity', which is a number between\r\n% [0-1], is set to 0.85. So let's try increase 'Sensitivity' to say 0.9.\r\n[centers, radii] = imfindcircles(rgb,[15 20],'ObjectPolarity','dark','Sensitivity',0.9)\r\n\r\n%% \r\n% OK, so now we found some circles - 15 to be precise. |centers| contains\r\n% the locations of circle centers and |radii| contains the estimated radii\r\n% of those circles.\r\n% \r\n% We can use function |viscircles|, which was also shipped in R2012a, to\r\n% draw these circles on the image. The variables |centers| and |radii| can\r\n% be passed directly to |viscircles|.\r\nimshow(rgb);\r\n\r\nh = viscircles(centers,radii);\r\n\r\n%%\r\n% The circle centers seem correctly positioned and their corresponding\r\n% radii seem to match well to the actual M&Ms. But still we are missing\r\n% quite a few of them. What's up with that?\r\n% \r\n% Let's try to increase 'Sensitivity' even more, to 0.95, and see what\r\n% happens.\r\n% \r\n[centers, radii] = imfindcircles(rgb,[15 20],'ObjectPolarity','dark','Sensitivity',0.95);\r\nsize(centers)\r\n\r\n%%\r\n% So increasing 'Sensitivity' gets us 34 circles. Let's plot these circles\r\n% on the image again.\r\ndelete(h); \r\nh = viscircles(centers,radii);\r\n\r\n%%\r\n% Much better. \r\n%  \r\n% Now, under the hood, |imfindcircles| has two different methods for\r\n% finding circles. So far we have been using the default method, which is\r\n% the _phase coding_ method. There's another method, popularly called the\r\n% _two-stage_ method, that is available in |imfindcircles|. Let's try to use\r\n% the two-stage method and see what we get.\r\n\r\n%%\r\n[centers, radii, metric] = imfindcircles(rgb,[15 20], ...\r\n    'ObjectPolarity','dark','Sensitivity',0.95,'Method','twostage');\r\n\r\ndelete(h);\r\n\r\nh = viscircles(centers,radii);\r\n\r\n%%\r\n% OK, great! The two-stage method is detecting more circles, at the\r\n% Sensitivity of 0.95. \r\n% \r\n% In general, these two method are complementary in that have they have\r\n% different strengths. In my experience, phase coding method is typically\r\n% faster and slightly more robust to noise than the two-stage method. But\r\n% it can also need higher 'Sensitivity' levels to get the same number of\r\n% detections as the two-stage method as we saw here.\r\n% \r\n% Coming back to our image, it is curious that we are not picking up the\r\n% yellow M&Ms in the image. Hmmm, the yellow M&Ms do not have a strong\r\n% contrast with the background. In fact they seem to have very similar\r\n% intensities as the background. Is it possible that the yellow M&Ms are\r\n% not really 'darker' than the background as we are assuming. Let's check\r\n% that out by looking at the grayscale version of this image again.\r\n\r\nimshow(gray_image);\r\n\r\n%%\r\n% Indeed! The yellow M&Ms are almost the same intensity, maybe even\r\n% brighter, as compared to the background. This suggests that we should\r\n% change 'ObjectPolarity' to 'bright' to detect the yellow ones.\r\n\r\n[centersBright, radiiBright] = imfindcircles(rgb,[15 20],'ObjectPolarity','bright','Sensitivity',0.95);\r\n\r\n%%\r\n% And let's draw the bright circles in a different color, say blue, by\r\n% changing the 'EdgeColor' parameter in |viscircles|.\r\nimshow(rgb); \r\nhBright = viscircles(centersBright, radiiBright,'EdgeColor','b');\r\n\r\n%%\r\n% OK, so we got one of them. That's a start! These yellow ones are hard to\r\n% find because of they don't _stand out_ as well as others on this\r\n% background. Let me introduce you to another parameter which might help us\r\n% here - 'EdgeThreshold'. To find circles, |imfindcircles| uses only the\r\n% edge pixels in the image. These edge pixels are essentially pixels with\r\n% high gradient value. The 'EdgeThreshold' parameter controls how _high_\r\n% the gradient value at a pixel has to be before it is considered an edge\r\n% pixel and included in computation. A high value (closer to 1) of this\r\n% parameter will allow only the strong edges (higher gradient values) to be\r\n% included, whereas a low value (closer to 0) is more permissive and\r\n% includes even the weaker edges (lower gradient values) in computation. In\r\n% case of yellow M&Ms, since the contrast is low, we expect the boundary\r\n% pixels (on the circumference of the M&M) to have low gradient values. So\r\n% let's try to lower the 'EdgeThreshold' parameter to ensure that they are\r\n% included in computation.\r\n\r\n[centersBright, radiiBright, metricBright] = imfindcircles(rgb,[15 20], ...\r\n    'ObjectPolarity','bright','Sensitivity',0.95,'EdgeThreshold',0.1);\r\n\r\ndelete(hBright);\r\nhBright = viscircles(centersBright, radiiBright,'EdgeColor','b');\r\n\r\n%% \r\n% All right! We are now picking up most of the yellow ones, and some green\r\n% ones too. Let's also draw the other M&Ms, which we found earlier, in red.\r\nh = viscircles(centers,radii);\r\n\r\n%%\r\n% We are still missing a few of them, such as the yellow one of the left\r\n% top corner. I am sure we can adjust the parameters to pick up on\r\n% those as well. However, at some point on our journey to find all the M&Ms\r\n% we will see some false circles showing up (M&Ms where there are none!).\r\n% This highlights that there is always a trade-off between how many true\r\n% circles you can find (detection rate) and how many false circles you get\r\n% with them (false alarm rate). Both go hand in hand, i.e., getting more of\r\n% the actual circles means a greater likelihood of getting false circles.\r\n% \r\n% So, now that we have most of our circles let's have some fun. Let's try\r\n% and draw these M&Ms in their own respective colors. \r\n% \r\n% First let's collect all our circle centers and radii together. \r\ncenters = round([centers; centersBright]);\r\nradii = [radii; radiiBright];\r\n\r\n%%\r\n% Also, let's smooth our image to remove some of the lighting and writing\r\n% artifacts (the 'm's) from our M&Ms. \r\nfilt_rgb = imfilter(rgb,ones(5)\/25);\r\n\r\n%%\r\n% Now we will extract the colors at the center of the circles. For this\r\n% first I make a list of all the elements of the array that I want to get.\r\n% Remember the center locations are stored in |centers|. But I want to get\r\n% the color value at these locations, which means that I have to extract\r\n% the values from all the three planes (along the third dimension) of this\r\n% RGB image at those center locations.\r\n% \r\n% To give an example, let's say we have two circles with centers\r\n%   example_centers = [10 20 ; \r\n%                      40 50 ]\r\n% \r\n% So to get the color values we need to get the values from the following\r\n% elements of the matrix. Here each row shows the element location in the\r\n% 3-dimensional image matrix. \r\n% \r\n%   locs = [10 20 1 ;\r\n%           40 50 1 ;\r\n%           10 20 2 ;\r\n%           40 50 2 ;                         \r\n%           10 20 3 ;\r\n%           40 50 3 ]\r\n% \r\n% There are many different ways you can do this in MATLAB. I like to do it\r\n% using |repmat| and a simple trick using |kron|, which is the function for\r\n% computing Kronecker product of two matrices.\r\nexample_centers = [10 20; 40 50];\r\nlocs = [repmat(example_centers,3,1) ...\r\n     kron((1:3)',ones(size(example_centers,1),1))]\r\n \r\n \r\n%%\r\n% Similarly we can make a list of all the elements we need to extract for\r\n% all the circle centers in |centers|. Note that the first and second\r\n% columns of |centers| have the column and row location of the circle\r\n% centers, respectively. Since we want the first column in 'locs' to be the\r\n% row location we switch the columns of |centers| using |fliplr|.\r\n\r\nlocs = [repmat(fliplr(centers),3,1) kron((1:3)',ones(size(centers,1),1))]; \r\n\r\n%% \r\n% Next, we convert these locations to linear indices using |sub2ind|. \r\ncenter_idx = sub2ind(size(rgb),locs(:,1),locs(:,2),locs(:,3));\r\n\r\n%%\r\n% Finally we extract these elements from the smoothed M&M image.\r\ncent_color = filt_rgb(center_idx);\r\n\r\n%%\r\n% Since 'cent_color' is a vector, we reshape it to get it in 3-column\r\n% format where the three columns correspond to R, G, and B colors for the\r\n% circle centers.\r\n%  \r\ncent_color = reshape(cent_color,size(centers,1),3); \r\n\r\n%% \r\n% Now that we have the colors corresponding to each circle, we can plot\r\n% these circles with the right colors. Remember to normalize the colors\r\n% between [0-1] for plotting.\r\nimshow(rgb)\r\n\r\nfor i = 1:size(centers,1)\r\n    viscircles(centers(i,:),radii(i),'EdgeColor',double(cent_color(i,:))\/255);\r\nend\r\n\r\n%%\r\n% <html> That looks about right. If you look closely, some circles are not\r\n% exactly the same color as the M&Ms. This is because of there is variation\r\n% in the color even within an M&M. This variation can be seen on closer\r\n% look, as shown in Steve's post <a\r\n% href=\"https:\/\/blogs.mathworks.com\/steve\/2010\/12\/17\/what-color-is-green\">What\r\n% color is green?<\/a>. <\/html>\r\n\r\n%%\r\n% Now that I have the circle colors extracted out what else can I do? Maybe\r\n% I can simulate an M&M image of my own. Hmmm, what do I need to do for\r\n% that? I see broadly two steps for doing this. First step is to\r\n% _paint_ the M&Ms with the right colors at the right locations. Second\r\n% step is to fill in the background color.\r\n% \r\n% For the first step of painting the M&Ms, we will use a simple trick from\r\n% Linear Systems theory. Linear systems theory tells us that when we\r\n% convolve a function with a shifted impulse (Dirac delta) function, we get\r\n% the same function shifted to the location of the impulse, i.e.\r\n% $f(t)*d(t-T) = f(t-T)$, where $f$ is the function, $d$ is the impulse\r\n% function, and $*$ denotes convolution. Essentially, convolving a function\r\n% with an impulse creates a copy of the function centered at the impulse\r\n% location.\r\n% \r\n% The first thing we need to do is create a blank canvas similar to our\r\n% original image.\r\ncartoon_rgb = zeros(size(rgb),class(rgb));\r\n\r\n%% \r\n% Next we fill in the right color values at the center locations for all\r\n% the M&Ms in the image. \r\ncartoon_rgb(center_idx) = cent_color(:);\r\nimshow(cartoon_rgb)\r\n\r\n%% \r\n% These individual pixels of different colors are like digital 'impulses',\r\n% (single-pixel wide, height corresponding to a color). Next we need to\r\n% make a 'function', which in our case is just a circular mask (because the\r\n% M&Ms are circular), to convolve with these impulses and make a copy of at\r\n% each of these impulse locations. For simplicity, I use a circle of radius\r\n% 17 pixels for all the M&Ms even though they have slightly different\r\n% radii.\r\n\r\nradius = 17;\r\ncirc_mask = double(getnhood(strel('ball',radius,radius,0)));\r\nimshow(circ_mask, 'InitialMagnification', 500)\r\n\r\n%% \r\n% Finally we convolve the image with the _colored_ impulses with the\r\n% circular mask to paint the M&Ms at the right locations. \r\n\r\ncartoon_rgb = imfilter(cartoon_rgb,circ_mask,'conv');\r\nimshow(cartoon_rgb)\r\n\r\n%% \r\n% A word of caution about this trick here. You might have noticed we are\r\n% working with multiple _impulses_ simultaneously, i.e. we are not\r\n% convolving the circular mask with just one impulse but a string of\r\n% impulses. We are getting away with that because our impulses (circles)\r\n% are well-separated (more than 2 x radius = 34 pixels apart) and the\r\n% circles don't overlap. If that was not the case, we would see unwanted\r\n% colors in the overlapping part of the circles due to superpositioning of\r\n% the masks.\r\n% \r\n% So now we have painted the colored M&Ms. All we have to do now is to\r\n% paint the background. I just pick an RGB color to match the background. \r\n\r\nbg_color = [235; 218; 175];\r\n\r\n%% \r\n% Next we need to find all the pixels that are part of the background. We\r\n% do this by using the same convolution trick we used. We make an image\r\n% with the 'impulses'. \r\nbw = false(size(rgb,1),size(rgb,2));\r\nbw(sub2ind(size(bw),centers(:,2),centers(:,1))) = true;\r\n\r\n%% \r\n% Then we convolve this image with a circular mask to get circles painted.\r\n% Then we complement the image to highlight the background (in white) and\r\n% record the locations (linear indices) of the pixels belonging to the\r\n% background.\r\nbw = imfilter(bw,circ_mask);\r\nbw = imcomplement(bw);\r\nimshow(bw)\r\nbg_idx_gray = find(bw);\r\n\r\n%% \r\n% Now we will fill these pixels locations with the background color.\r\n% Since this is a color image we need to do some work to fill the colors.\r\n% We do this is in a way similar to what to we did to extract the colors\r\n% for the circle center locations. First we find the linear indices of all\r\n% the elements in the color image matrix that need to be filled.\r\nbg_idx_color = [bg_idx_gray; numel(bw)+bg_idx_gray; 2*numel(bw)+bg_idx_gray];\r\n\r\n%% \r\n% Next we make a vector that has the color values that go into those\r\n% elements. We do this by repeating the color values in |bg_color| once for\r\n% each pixel location.\r\nbg_fill_values = kron(bg_color,ones(size(bg_idx_gray)));\r\n\r\n%% \r\n% Finally we fill the color values in the background locations. \r\ncartoon_rgb(bg_idx_color) = bg_fill_values;\r\n\r\n%% \r\n% That's it. We have made our simulated M&M image and now let's see how it\r\n% has come out. To view this, I will use a new function, |imshowpair|,\r\n% introduced in R2012a. It has some great options for viewing and comparing\r\n% two images. I will use the 'montage' option to view the original and\r\n% simulated M&M images side-by-side. There are several other cool options\r\n% in |imshowpair|, such as 'blend', which is my favorite. You should check\r\n% it out.\r\n% \r\n% So here you go: spot the difference!\r\nimshowpair(rgb,cartoon_rgb,'montage'); \r\ntitle('Spot the difference!');\r\naxis off;\r\n\r\n%% \r\n% Happy circle hunting!\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n##### SOURCE END ##### 2a11a2e4d3a24e388d9b12545149b321\r\n-->","protected":false},"excerpt":{"rendered":"<!--introduction--><p><i>Today's blog post was written by Spandan Tiwari, a developer on the Image Processing Toolbox team. Thanks, Spandan!<\/i>... <a class=\"read-more\" href=\"https:\/\/blogs.mathworks.com\/steve\/2012\/09\/04\/detecting-circular-objects-in-images\/\">read more >><\/a><\/p>","protected":false},"author":42,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[50,917,733,402,102,348,192,518,146,653,370,871,76,36,867,915,162,288,116,170,104,188,190,106,468,100,873,130],"_links":{"self":[{"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/posts\/662"}],"collection":[{"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/users\/42"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/comments?post=662"}],"version-history":[{"count":9,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/posts\/662\/revisions"}],"predecessor-version":[{"id":3799,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/posts\/662\/revisions\/3799"}],"wp:attachment":[{"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/media?parent=662"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/categories?post=662"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/tags?post=662"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}