{"id":393,"date":"2011-10-04T22:07:16","date_gmt":"2011-10-05T02:07:16","guid":{"rendered":"https:\/\/blogs.mathworks.com\/steve\/2011\/10\/04\/binary-image-convex-hull-algorithm-notes\/"},"modified":"2019-10-29T16:57:27","modified_gmt":"2019-10-29T20:57:27","slug":"binary-image-convex-hull-algorithm-notes","status":"publish","type":"post","link":"https:\/\/blogs.mathworks.com\/steve\/2011\/10\/04\/binary-image-convex-hull-algorithm-notes\/","title":{"rendered":"Binary image convex hull &#8211; algorithm notes"},"content":{"rendered":"<div xmlns:mwsh=\"https:\/\/www.mathworks.com\/namespace\/mcode\/v1\/syntaxhighlight.dtd\" class=\"content\">\r\n   <p>Today I want to tell a little image processing algorithm story related to my <a href=\"https:\/\/blogs.mathworks.com\/steve\/2011\/09\/30\/binary-image-convex-hull\/\">post last week<\/a> about the new <a href=\"https:\/\/www.mathworks.com\/help\/releases\/R2011b\/toolbox\/images\/ref\/bwconvhull.html\"><tt>bwconvhull<\/tt><\/a> function in the Image Processing Toolbox.\r\n   <\/p>\r\n   <p>The developer (<a href=\"https:\/\/blogs.mathworks.com\/steve\/2011\/09\/23\/dealing-with-really-big-images-image-adapters\/\">Brendan<\/a>) who worked on this function came to see me sometime last year to find out how the 'ConvexImage' measurement offered by <a href=\"https:\/\/www.mathworks.com\/help\/releases\/R2011b\/toolbox\/images\/ref\/regionprops.html\"><tt>regionprops<\/tt><\/a> was computed so that he could use the same procedure for <tt>bwconvhull<\/tt>. I believed that I knew the answer off the top of my head, so without looking at the code I rattled off the following steps:\r\n   <\/p>\r\n   <div>\r\n      <ol>\r\n         <li>Compute the x- and y-coordinates for the four corners of all the foreground pixels in the binary image.<\/li>\r\n         <li>Use <a href=\"https:\/\/www.mathworks.com\/help\/releases\/R2011b\/techdoc\/ref\/convhull.html\"><tt>convhull<\/tt><\/a> to compute the convex hull of the (x,y) pairs from step 1.\r\n         <\/li>\r\n         <li>Use <a href=\"https:\/\/www.mathworks.com\/help\/releases\/R2011b\/toolbox\/images\/ref\/poly2mask.html\"><tt>poly2mask<\/tt><\/a> to convert the convex hull polygon to a binary image mask.\r\n         <\/li>\r\n      <\/ol>\r\n   <\/div>\r\n   <p>A few days later Brendan came back to tell me that, although my description was clear, the code that I wrote ten years ago\r\n      for <tt>regionprops<\/tt> actually does something else.\r\n   <\/p>\r\n   <p>Oops.<\/p>\r\n   <p>I looked at the code and realized that Brendan was right, and I started to remember that I had actually made this same mistake\r\n      many years before.\r\n   <\/p>\r\n   <p>In fact, I did originally implement 'ConvexImage' in <tt>regionprops<\/tt> using the procedure outlined above. Before it shipped, though, I discovered (and fixed) a big problem with it.\r\n   <\/p>\r\n   <p>Let me show you the problem using a small example. Here's a tiny binary image.<\/p><pre style=\"background: #F9F7F3; padding: 10px; border: 1px solid rgb(200,200,200)\">bw = [<span style=\"color: #0000FF\">...<\/span>\r\n    0 0 0 0 0 0 0 0 0\r\n    0 0 0 0 1 0 0 0 0\r\n    0 0 0 1 0 1 0 0 0\r\n    0 0 1 0 0 0 1 0 0\r\n    0 1 0 0 0 0 0 1 0\r\n    0 0 0 0 0 0 0 0 0 ];\r\nimshow(bw, <span style=\"color: #A020F0\">'InitialMagnification'<\/span>, <span style=\"color: #A020F0\">'fit'<\/span>)<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2011\/convex_hull_calculation_01.png\"> <p>Now here's the convex image as computed by <tt>bwconvhull<\/tt>. The \"filled-in\" pixels are shown in light gray.\r\n   <\/p><pre style=\"background: #F9F7F3; padding: 10px; border: 1px solid rgb(200,200,200)\">bwch = bwconvhull(bw)\r\nhold <span style=\"color: #A020F0\">on<\/span>\r\nh = imshow(bwch);\r\nset(h, <span style=\"color: #A020F0\">'AlphaData'<\/span>, 0.8);\r\nhold <span style=\"color: #A020F0\">off<\/span><\/pre><pre style=\"font-style:oblique\">\r\nbwch =\r\n\r\n     0     0     0     0     0     0     0     0     0\r\n     0     0     0     0     1     0     0     0     0\r\n     0     0     0     1     1     1     0     0     0\r\n     0     0     1     1     1     1     1     0     0\r\n     0     1     1     1     1     1     1     1     0\r\n     0     0     0     0     0     0     0     0     0\r\n\r\n<\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2011\/convex_hull_calculation_02.png\"> <p>Now I'll graphically illustrate the computational procedure above. First, compute the set of corner locations for all the\r\n      foreground pixels.\r\n   <\/p><pre style=\"background: #F9F7F3; padding: 10px; border: 1px solid rgb(200,200,200)\">[y,x] = find(bw);\r\ndx = [-0.5 -0.5  0.5  0.5];\r\ndy = [-0.5  0.5 -0.5  0.5];\r\nx_corners = bsxfun(@plus, x, dx);\r\ny_corners = bsxfun(@plus, y, dy);\r\nx_corners = x_corners(:);\r\ny_corners = y_corners(:);<\/pre><p>Visualize step 1 by superimposing those corner locations on the input image.<\/p><pre style=\"background: #F9F7F3; padding: 10px; border: 1px solid rgb(200,200,200)\">imshow(bw, <span style=\"color: #A020F0\">'InitialMagnification'<\/span>, <span style=\"color: #A020F0\">'fit'<\/span>)\r\nhold <span style=\"color: #A020F0\">on<\/span>\r\nplot(x_corners, y_corners, <span style=\"color: #A020F0\">'og'<\/span>)\r\nhold <span style=\"color: #A020F0\">off<\/span><\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2011\/convex_hull_calculation_03.png\"> <p>Step 2: Compute the convex hull of all the corner points. Visualize by superimposing the convex hull in red.<\/p><pre style=\"background: #F9F7F3; padding: 10px; border: 1px solid rgb(200,200,200)\">k = convhull(x_corners, y_corners);\r\nx_hull = x_corners(k);\r\ny_hull = y_corners(k);\r\nhold <span style=\"color: #A020F0\">on<\/span>\r\nplot(x_hull, y_hull, <span style=\"color: #A020F0\">'r'<\/span>, <span style=\"color: #A020F0\">'LineWidth'<\/span>, 4)\r\nhold <span style=\"color: #A020F0\">off<\/span><\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2011\/convex_hull_calculation_04.png\"> <p>Step 3: Use <tt>poly2mask<\/tt> to convert the convex hull to a binary image mask.\r\n   <\/p><pre style=\"background: #F9F7F3; padding: 10px; border: 1px solid rgb(200,200,200)\">mask = poly2mask(x_hull, y_hull, 6, 9);\r\n\r\nimshow(bw, <span style=\"color: #A020F0\">'InitialMagnification'<\/span>, <span style=\"color: #A020F0\">'fit'<\/span>)\r\nhold <span style=\"color: #A020F0\">on<\/span>\r\nh = imshow(mask);\r\nset(h, <span style=\"color: #A020F0\">'AlphaData'<\/span>, 0.8)\r\nplot(x_hull, y_hull, <span style=\"color: #A020F0\">'r'<\/span>, <span style=\"color: #A020F0\">'LineWidth'<\/span>, 4)\r\nhold <span style=\"color: #A020F0\">off<\/span><\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2011\/convex_hull_calculation_05.png\"> <p>You can see that there are extra pixels along the diagonal edges that got put into the mask. That's not good. If you repeat\r\n      the operation, those diagonals will keep filling out until you're left with a rectangle. That's not supposed to happen with\r\n      repeated application of the convex hull.\r\n   <\/p>\r\n   <p>The reason this is happening is that the convex hull goes exactly through the center of pixels that are along the diagonal\r\n      but \"outside\" the original set of foreground pixels. Because of the <a href=\"https:\/\/blogs.mathworks.com\/steve\/2006\/12\/22\/poly2mask-and-roipoly-part-3\/\">tie-breaking rules applied by <tt>poly2mask<\/tt><\/a>, all (in this case) of those outside pixels got included in the output mask.\r\n   <\/p>\r\n   <p>The solution that I settled on ten years ago, and which is now also used in <tt>bwconvhull<\/tt>, was to modify the first step of the procedure. Instead of collecting the set of <b>corner<\/b> points of each foreground pixel, the correct procedure collects points along the center of each edge of each foreground pixel.\r\n   <\/p>\r\n   <p>Here's what that looks like.<\/p><pre style=\"background: #F9F7F3; padding: 10px; border: 1px solid rgb(200,200,200)\">dx = [0.0  0.0  0.5 -0.5];\r\ndy = [0.5 -0.5  0.0  0.0];\r\nx_edges = bsxfun(@plus, x, dx);\r\ny_edges = bsxfun(@plus, y, dy);\r\nx_edges = x_edges(:);\r\ny_edges = y_edges(:);\r\n\r\nk = convhull(x_edges, y_edges);\r\nx_hull = x_edges(k);\r\ny_hull = y_edges(k);\r\n\r\nimshow(bw, <span style=\"color: #A020F0\">'InitialMagnification'<\/span>, <span style=\"color: #A020F0\">'fit'<\/span>)\r\nhold <span style=\"color: #A020F0\">on<\/span>\r\nplot(x_edges, y_edges, <span style=\"color: #A020F0\">'og'<\/span>)\r\nplot(x_hull, y_hull, <span style=\"color: #A020F0\">'r'<\/span>, <span style=\"color: #A020F0\">'LineWidth'<\/span>, 4)\r\nhold <span style=\"color: #A020F0\">off<\/span><\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2011\/convex_hull_calculation_06.png\"> <pre style=\"background: #F9F7F3; padding: 10px; border: 1px solid rgb(200,200,200)\">mask = poly2mask(x_hull, y_hull, 6, 9);\r\nimshow(mask, <span style=\"color: #A020F0\">'InitialMagnification'<\/span>, <span style=\"color: #A020F0\">'fit'<\/span>)\r\nhold <span style=\"color: #A020F0\">on<\/span>\r\nplot(x_hull, y_hull, <span style=\"color: #A020F0\">'r'<\/span>, <span style=\"color: #A020F0\">'LineWidth'<\/span>, 4)\r\nplot(x_edges, y_edges, <span style=\"color: #A020F0\">'og'<\/span>)\r\nhold <span style=\"color: #A020F0\">off<\/span><\/pre><img decoding=\"async\" vspace=\"5\" hspace=\"5\" src=\"https:\/\/blogs.mathworks.com\/images\/steve\/2011\/convex_hull_calculation_07.png\"> <p>Much better!<\/p>\r\n   <p>I have been fooled more often than I would like to admit by sometimes nonintuitive digital image geometry. For you image processing\r\n      algorithm people out there, I hope seeing these pictures and techniques will help you avoid similar pitfalls someday.\r\n   <\/p><script language=\"JavaScript\">\r\n<!--\r\n\r\n    function grabCode_442a6f805d6d4bea8e6e7ee23fd19e3e() {\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='442a6f805d6d4bea8e6e7ee23fd19e3e ' + '##### ' + 'SOURCE BEGIN' + ' #####';\r\n        t2='##### ' + 'SOURCE END' + ' #####' + ' 442a6f805d6d4bea8e6e7ee23fd19e3e';\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        author = 'Steve Eddins';\r\n        copyright = 'Copyright 2011 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 author and copyright lines at the bottom if specified.\r\n        if ((author.length > 0) || (copyright.length > 0)) {\r\n            d.writeln('');\r\n            d.writeln('%%');\r\n            if (author.length > 0) {\r\n                d.writeln('% _' + author + '_');\r\n            }\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      \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_442a6f805d6d4bea8e6e7ee23fd19e3e()\"><span style=\"font-size: x-small;        font-style: italic;\">Get \r\n            the MATLAB code \r\n            <noscript>(requires JavaScript)<\/noscript><\/span><\/a><br><br>\r\n      Published with MATLAB&reg; 7.13<br><\/p>\r\n<\/div>\r\n<!--\r\n442a6f805d6d4bea8e6e7ee23fd19e3e ##### SOURCE BEGIN #####\r\n%%\r\n% Today I want to tell a little image processing algorithm story related to\r\n% my <https:\/\/blogs.mathworks.com\/steve\/2011\/09\/30\/binary-image-convex-hull\/\r\n% post last week> about the new\r\n% <https:\/\/www.mathworks.com\/help\/releases\/R2011b\/toolbox\/images\/ref\/bwconvhull.html\r\n% |bwconvhull|> function in the Image Processing Toolbox.\r\n%\r\n% The developer\r\n% (<https:\/\/blogs.mathworks.com\/steve\/2011\/09\/23\/dealing-with-really-big-images-image-adapters\/\r\n% Brendan>) who worked on this function came to see me sometime last year\r\n% to find out how the 'ConvexImage' measurement offered by\r\n% <https:\/\/www.mathworks.com\/help\/releases\/R2011b\/toolbox\/images\/ref\/regionprops.html\r\n% |regionprops|> was computed so that he could use the same procedure for\r\n% |bwconvhull|. I believed that I knew the answer off the top of my head,\r\n% so without looking at the code I rattled off the following steps:\r\n%\r\n% # Compute the x- and y-coordinates for the four corners of all the\r\n% foreground pixels in the binary image.\r\n% # Use <https:\/\/www.mathworks.com\/help\/releases\/R2011b\/techdoc\/ref\/convhull.html \r\n% |convhull|> to compute the convex hull of the (x,y) pairs from step\r\n% 1.\r\n% # Use <https:\/\/www.mathworks.com\/help\/releases\/R2011b\/toolbox\/images\/ref\/poly2mask.html\r\n% |poly2mask|> to convert the convex hull polygon to a binary image mask.\r\n%\r\n% A few days later Brendan came back to tell me that, although my\r\n% description was clear, the code that I wrote ten years ago for\r\n% |regionprops| actually does something else.\r\n%\r\n% Oops.\r\n%\r\n% I looked at the code and realized that Brendan was right, and I started\r\n% to remember that I had actually made this same mistake many years before.\r\n%\r\n% In fact, I did originally implement 'ConvexImage' in |regionprops| using\r\n% the procedure outlined above. Before it shipped, though, I discovered\r\n% (and fixed) a big problem with it.\r\n%\r\n% Let me show you the problem using a small example. Here's a tiny binary\r\n% image.\r\n\r\nbw = [...\r\n    0 0 0 0 0 0 0 0 0\r\n    0 0 0 0 1 0 0 0 0\r\n    0 0 0 1 0 1 0 0 0\r\n    0 0 1 0 0 0 1 0 0\r\n    0 1 0 0 0 0 0 1 0\r\n    0 0 0 0 0 0 0 0 0 ];\r\nimshow(bw, 'InitialMagnification', 'fit')\r\n\r\n%%\r\n% Now here's the convex image as computed by |bwconvhull|. The \"filled-in\"\r\n% pixels are shown in light gray.\r\n\r\nbwch = bwconvhull(bw)\r\nhold on\r\nh = imshow(bwch);\r\nset(h, 'AlphaData', 0.8);\r\nhold off\r\n\r\n%%\r\n% Now I'll graphically illustrate the computational procedure above. First,\r\n% compute the set of corner locations for all the foreground pixels.\r\n[y,x] = find(bw);\r\ndx = [-0.5 -0.5  0.5  0.5];\r\ndy = [-0.5  0.5 -0.5  0.5];\r\nx_corners = bsxfun(@plus, x, dx);\r\ny_corners = bsxfun(@plus, y, dy);\r\nx_corners = x_corners(:);\r\ny_corners = y_corners(:);\r\n\r\n%%\r\n% Visualize step 1 by superimposing those corner locations on the input\r\n% image.\r\nimshow(bw, 'InitialMagnification', 'fit')\r\nhold on\r\nplot(x_corners, y_corners, 'og')\r\nhold off\r\n\r\n%%\r\n% Step 2: Compute the convex hull of all the corner points. Visualize by\r\n% superimposing the convex hull in red.\r\nk = convhull(x_corners, y_corners);\r\nx_hull = x_corners(k);\r\ny_hull = y_corners(k);\r\nhold on\r\nplot(x_hull, y_hull, 'r', 'LineWidth', 4)\r\nhold off\r\n\r\n%%\r\n% Step 3: Use |poly2mask| to convert the convex hull to a binary image\r\n% mask.\r\nmask = poly2mask(x_hull, y_hull, 6, 9);\r\n\r\nimshow(bw, 'InitialMagnification', 'fit')\r\nhold on\r\nh = imshow(mask);\r\nset(h, 'AlphaData', 0.8)\r\nplot(x_hull, y_hull, 'r', 'LineWidth', 4)\r\nhold off\r\n\r\n%%\r\n% You can see that there are extra pixels along the diagonal edges that got\r\n% put into the mask. That's not good. If you repeat the operation, those\r\n% diagonals will keep filling out until you're left with a rectangle.\r\n% That's not supposed to happen with repeated application of the convex\r\n% hull.\r\n%\r\n% The reason this is happening is that the convex hull goes exactly through\r\n% the center of pixels that are along the diagonal but \"outside\" the\r\n% original set of foreground pixels. Because of the <https:\/\/blogs.mathworks.com\/steve\/2006\/12\/22\/poly2mask-and-roipoly-part-3\/ \r\n% tie-breaking rules\r\n% applied by |poly2mask|>, all (in this case) of those outside pixels got\r\n% included in the output mask.\r\n%\r\n% The solution that I settled on ten years ago, and which is now also used\r\n% in |bwconvhull|, was to modify the first step of the procedure. Instead\r\n% of collecting the set of *corner* points of each foreground pixel, the\r\n% correct procedure collects points along the center of each edge of each\r\n% foreground pixel.\r\n%\r\n% Here's what that looks like.\r\n\r\n%%\r\ndx = [0.0  0.0  0.5 -0.5];\r\ndy = [0.5 -0.5  0.0  0.0];\r\nx_edges = bsxfun(@plus, x, dx);\r\ny_edges = bsxfun(@plus, y, dy);\r\nx_edges = x_edges(:);\r\ny_edges = y_edges(:);\r\n\r\nk = convhull(x_edges, y_edges);\r\nx_hull = x_edges(k);\r\ny_hull = y_edges(k);\r\n\r\nimshow(bw, 'InitialMagnification', 'fit')\r\nhold on\r\nplot(x_edges, y_edges, 'og')\r\nplot(x_hull, y_hull, 'r', 'LineWidth', 4)\r\nhold off\r\n\r\n%%\r\n\r\nmask = poly2mask(x_hull, y_hull, 6, 9);\r\nimshow(mask, 'InitialMagnification', 'fit')\r\nhold on\r\nplot(x_hull, y_hull, 'r', 'LineWidth', 4)\r\nplot(x_edges, y_edges, 'og')\r\nhold off\r\n\r\n%%\r\n% Much better!\r\n%\r\n% I have been fooled more often than I would like to admit by sometimes\r\n% nonintuitive digital image geometry. For you image processing algorithm\r\n% people out there, I hope seeing these pictures and techniques will help\r\n% you avoid similar pitfalls someday.\r\n\r\n##### SOURCE END ##### 442a6f805d6d4bea8e6e7ee23fd19e3e\r\n-->","protected":false},"excerpt":{"rendered":"<p>\r\n   Today I want to tell a little image processing algorithm story related to my post last week about the new bwconvhull function in the Image Processing Toolbox.\r\n   \r\n   The developer (Brendan)... <a class=\"read-more\" href=\"https:\/\/blogs.mathworks.com\/steve\/2011\/10\/04\/binary-image-convex-hull-algorithm-notes\/\">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":[320,837,835,348,90,36,68,296,168],"_links":{"self":[{"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/posts\/393"}],"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=393"}],"version-history":[{"count":1,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/posts\/393\/revisions"}],"predecessor-version":[{"id":3761,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/posts\/393\/revisions\/3761"}],"wp:attachment":[{"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/media?parent=393"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/categories?post=393"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.mathworks.com\/steve\/wp-json\/wp\/v2\/tags?post=393"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}