changeset 818:355bd9c476d9

bwperim: rewrite to support N-dimensional images and connectivity. * bwperim.m: rewrite the function in order to accept images with any number of dimensions. Treat the connectivity argument as more standard (relying on the new private make_conn function). Display image instead of returning its value if nargout less than 1. Add tests. * private/make_conn.m: new private function to be shared among functions accepting a connectivity argument. * NEWS: add bwperim to functions that have been changed.
author Carnë Draug <carandraug@octave.org>
date Tue, 29 Oct 2013 02:32:44 +0000
parents 201fd05fbc79
children 83f060d508a7
files NEWS inst/bwperim.m inst/private/make_conn.m
diffstat 3 files changed, 280 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS
+++ b/NEWS
@@ -118,6 +118,7 @@
     Matlab compatibility, or performance:
 
       bwlabel
+      bwperim
       padarray
 
  Summary of important user-visible changes for image 2.0.0 (2012/11/08):
--- a/inst/bwperim.m
+++ b/inst/bwperim.m
@@ -1,4 +1,4 @@
-## Copyright (C) 2006 Søren Hauberg <soren@hauberg.org>
+## Copyright (C) 2013 Carnë Draug <carandraug@octave.org>
 ##
 ## This program is free software; you can redistribute it and/or modify it under
 ## the terms of the GNU General Public License as published by the Free Software
@@ -14,57 +14,216 @@
 ## this program; if not, see <http://www.gnu.org/licenses/>.
 
 ## -*- texinfo -*-
-## @deftypefn {Function File} @var{BW2} = bwperim(@var{BW1})
-## @deftypefnx{Function File} @var{BW2} = bwperim(@var{BW1}, @var{n})
-## Find the perimeter of objects in binary images.
+## @deftypefn  {Function File} {} bwperim (@var{bw})
+## @deftypefnx {Function File} {} bwperim (@var{bw}, @var{conn})
+## Find perimeter of objects in binary images.
+##
+## Values from the matrix @var{bw} are considered part of an object perimeter
+## if their value is non-zero and is connected to at least one zero-valued
+## element.
+##
+## Element connectivity @var{conn}, to define the size of objects, can be
+## specified with a numeric scalar (number of elements in the neighborhood):
 ##
-## A pixel is part of an object perimeter if its value is one and there
-## is at least one zero-valued pixel in its neighborhood.
+## @table @samp
+## @item 4 or 8
+## for 2 dimensional matrices;
+## @item 6, 18 or 26
+## for 3 dimensional matrices;
+## @end table
 ##
-## By default the neighborhood of a pixel is 4 nearest pixels, but
-## if @var{n} is set to 8 the 8 nearest pixels will be considered.
+## or with a binary matrix representing a connectivity array.  Defaults to
+## @code{conndef (ndims (@var{bw}), "minimal")} which is equivalent to
+## @var{conn} of 4 and 6 for 2 and 3 dimensional matrices respectively.
+##
+## @seealso{bwarea, bwboundaries, imerode, mmgrad}
 ## @end deftypefn
 
-function out = bwperim(bw, n=4)
-  ## Input checking
-  if (nargin < 1)
-    print_usage();
+function varargout = bwperim (bw, conn)
+
+  if (nargin < 1 || nargin > 2)
+    print_usage ();
+  elseif (! ismatrix (bw) || ! (isnumeric (bw) || islogical (bw)))
+    error("bwperim: BW must be a numeric matrix");
   endif
-  if (!isbw(bw) || ndims(bw)!=2)
-    error("bwperim: first input argument must be a 2-dimensional binary image");
+
+  nDims = ndims (bw);
+  if (nargin < 2)
+    ## Defining default connectivity here because it's dependent
+    ## on the first argument
+    conn = conndef (nDims, "minimal");
+  else
+    conn = make_conn ("bwperim", 2, nDims, conn);
   endif
-  if (!isscalar(n) || (n!=4 && n!=8))
-    error("bwperim: second argument must be 4 or 8");
-  endif
-  
+
   ## Make sure bw is logical;
   bw = logical (bw);
-  
-  ## Translate image by one pixel in all directions
-  [rows, cols] = size(bw);
-  north = [bw(2:end, :); zeros(1, cols, "logical")];
-  south = [zeros(1, cols, "logical"); bw(1:end-1, :)];
-  west  = [bw(:, 2:end), zeros(rows, 1, "logical")];
-  east  = [zeros(rows, 1, "logical"), bw(:, 1:end-1)];
-  if (n == 8)
-    north_east = north_west = south_east = south_west = zeros (rows, cols, "logical");
-    north_east (1:end-1, 2:end)   = bw (2:end, 1:end-1);
-    north_west (1:end-1, 1:end-1) = bw (2:end, 2:end);
-    south_east (2:end, 2:end)     = bw (1:end-1, 1:end-1);
-    south_west (2:end, 1:end-1)   = bw (1:end-1, 2:end);
-  endif
-  
-  ## Do the comparing
-  if (n == 4)
-    out = bw;
-    idx = (north == bw) & (south == bw) & (west == bw) & (east == bw);
-    out(idx) = false;
-  else # n == 8
-    out = bw;
-    idx = (north == bw) & (north_east == bw) & ...
-          (east  == bw) & (south_east == bw) & ...
-          (south == bw) & (south_west == bw) & ...
-          (west  == bw) & (north_west == bw);
-    out (idx) = false;
+
+  ## Recover the elements that would get removed by erosion
+  perim = (! imerode (bw, conn)) & bw;
+
+  ## Get the borders back (they are removed during erosion
+  tmp_idx = repmat ({":"}, [1 nDims]);
+  p_size  = size (perim);
+  for dim = 1:nDims
+    idx       = tmp_idx;
+    idx{dim}  = [1 p_size(dim)];
+    perim(idx{:}) = bw(idx{:});
+  endfor
+
+  if (nargout > 0)
+    varargout{1} = perim;
+  else
+    imshow (perim);
   endif
 endfunction
+
+%!shared in, out
+%! in = [ 1   1   1   1   0   1   1   0   1   1
+%!        1   1   0   1   1   1   1   1   1   0
+%!        1   1   1   0   1   1   1   1   1   1
+%!        1   1   1   1   0   1   1   1   0   1
+%!        1   1   1   0   1   1   1   1   1   0
+%!        1   1   1   1   1   1   0   1   0   1
+%!        1   1   1   1   1   1   1   1   1   0
+%!        1   1   1   1   1   1   1   1   1   1
+%!        1   1   1   1   1   1   0   0   1   1
+%!        1   1   1   1   0   1   0   1   1   0];
+%!
+%! out = [1   1   1   1   0   1   1   0   1   1
+%!        1   1   0   1   1   0   0   1   1   0
+%!        1   0   1   0   1   0   0   0   1   1
+%!        1   0   0   1   0   1   0   1   0   1
+%!        1   0   1   0   1   0   1   0   1   0
+%!        1   0   0   1   0   1   0   1   0   1
+%!        1   0   0   0   0   0   1   0   1   0
+%!        1   0   0   0   0   0   1   1   0   1
+%!        1   0   0   0   1   1   0   0   1   1
+%!        1   1   1   1   0   1   0   1   1   0];
+%!assert (bwperim (in), logical (out));
+%!assert (bwperim (in, 4), logical (out));
+%!
+%! out = [1   1   1   1   0   1   1   0   1   1
+%!        1   1   0   1   1   1   1   1   1   0
+%!        1   1   1   0   1   1   0   1   1   1
+%!        1   0   1   1   0   1   0   1   0   1
+%!        1   0   1   0   1   1   1   1   1   0
+%!        1   0   1   1   1   1   0   1   0   1
+%!        1   0   0   0   0   1   1   1   1   0
+%!        1   0   0   0   0   1   1   1   1   1
+%!        1   0   0   1   1   1   0   0   1   1
+%!        1   1   1   1   0   1   0   1   1   0];
+%!assert (bwperim (in, 8), logical (out));
+%!
+%! out = [1   1   1   1   0   1   1   0   1   1
+%!        1   0   0   0   0   1   0   0   1   0
+%!        1   0   0   0   0   0   0   1   0   1
+%!        1   0   1   0   0   0   0   0   0   1
+%!        1   0   0   0   0   1   0   1   0   0
+%!        1   0   0   0   1   0   0   0   0   1
+%!        1   0   0   0   0   0   0   1   0   0
+%!        1   0   0   0   0   1   1   0   0   1
+%!        1   0   0   1   0   1   0   0   1   1
+%!        1   1   1   1   0   1   0   1   1   0];
+%!assert (bwperim (in, [1 0 0; 0 1 0; 0 0 1]), logical (out));
+
+## test that any non-zero value is valid (even i and Inf)
+%!shared in, out
+%! in = [ 0   0   0   0   0   0   0
+%!        0   0   5   0   0   1   9
+%!        0 Inf   9   7   0   0   0
+%!        0 1.5   5   7   1   0   0
+%!        0 0.5  -1  89   i   0   0
+%!        0   4  10  15   1   0   0
+%!        0   0   0   0   0   0   0];
+%! out = [0   0   0   0   0   0   0
+%!        0   0   1   0   0   1   1
+%!        0   1   0   1   0   0   0
+%!        0   1   0   0   1   0   0
+%!        0   1   0   0   1   0   0
+%!        0   1   1   1   1   0   0
+%!        0   0   0   0   0   0   0];
+%!assert (bwperim (in), logical (out));
+
+## test for 3D
+%!shared in, out
+%! in = reshape (magic(16), [8 8 4]) > 50;
+%! out(:,:,1) = [
+%!    1   1   0   1   0   1   1   1
+%!    0   1   1   1   1   1   0   1
+%!    0   1   1   1   1   1   0   1
+%!    1   1   0   1   1   1   1   1
+%!    1   1   1   1   1   1   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   0   1   1   1   1   1   0];
+%! out(:,:,2) = [
+%!    1   1   0   1   0   1   1   1
+%!    0   1   1   0   1   1   0   1
+%!    0   1   0   0   0   1   0   1
+%!    1   0   1   0   0   0   1   1
+%!    1   0   0   1   0   1   0   1
+%!    1   0   1   0   1   0   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   0   1   1   1   1   1   0];
+%! out(:,:,3) = [
+%!    1   1   0   1   0   1   1   1
+%!    0   1   1   0   1   1   0   1
+%!    0   1   0   0   0   1   0   1
+%!    1   0   0   0   0   0   1   1
+%!    1   0   0   1   0   1   0   1
+%!    1   0   1   0   1   0   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   0   1   1   1   1   1   0];
+%! out(:,:,4) = [
+%!    1   1   0   1   0   1   1   1
+%!    0   1   1   1   1   1   0   1
+%!    0   1   1   1   1   1   0   1
+%!    1   1   1   1   1   1   1   1
+%!    1   1   1   1   1   1   1   0
+%!    1   1   1   0   1   0   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   0   1   1   1   1   1   0];
+%!assert (bwperim (in), logical (out));
+%!
+%! out(:,:,1) = [
+%!    1   1   0   1   0   1   1   1
+%!    0   1   1   1   1   1   0   1
+%!    0   1   1   1   1   1   0   1
+%!    1   1   0   1   1   1   1   1
+%!    1   1   1   1   1   1   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   0   1   1   1   1   1   0];
+%! out(:,:,2) = [
+%!    1   1   0   1   0   1   1   1
+%!    0   1   1   1   1   1   0   1
+%!    0   1   1   0   0   1   0   1
+%!    1   1   1   1   0   1   1   1
+%!    1   0   1   1   1   1   1   1
+%!    1   0   1   0   1   0   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   0   1   1   1   1   1   0];
+%! out(:,:,3) = [
+%!    1   1   0   1   0   1   1   1
+%!    0   1   1   1   1   1   0   1
+%!    0   1   0   0   0   1   0   1
+%!    1   1   0   0   0   1   1   1
+%!    1   0   1   1   1   1   1   1
+%!    1   0   1   0   1   0   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   0   1   1   1   1   1   0];
+%! out(:,:,4) = [
+%!    1   1   0   1   0   1   1   1
+%!    0   1   1   1   1   1   0   1
+%!    0   1   1   1   1   1   0   1
+%!    1   1   1   1   1   1   1   1
+%!    1   1   1   1   1   1   1   0
+%!    1   1   1   0   1   0   1   1
+%!    1   1   1   0   1   0   1   1
+%!    1   0   1   1   1   1   1   0];
+%!assert (bwperim (in, 18), logical (out));
+
+%!error bwperim ("text")
+%!error bwperim (rand (10), 5)
+%!error bwperim (rand (10), "text")
new file mode 100644
--- /dev/null
+++ b/inst/private/make_conn.m
@@ -0,0 +1,75 @@
+## Copyright (C) 2013 Carnë Draug <carandraug+dev@gmail.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, see <http://www.gnu.org/licenses/>.
+
+## Private function to create a connectivity array
+##
+## Use the following Texinfo on the documentation of functions
+## that make use of it (adjust the default)
+##
+## Element connectivity @var{conn}, to define the size of objects, can be
+## specified with a numeric scalar (number of elements in the neighborhood):
+##
+## @table @samp
+## @item 4 or 8
+## for 2 dimensional matrices;
+## @item 6, 18 or 26
+## for 3 dimensional matrices;
+## @end table
+##
+## or with a binary matrix representing a connectivity array.  Defaults to
+## @code{conndef (ndims (@var{bw}), "maximal")} which is equivalent to
+## @var{conn} of 8 and 26 for 2 and 3 dimensional matrices respectively.
+
+function conn = make_conn (func, arg_pos, n_dims, conn)
+
+  iptcheckconn (conn, func, "CONN", arg_pos);
+  if (isscalar (conn))
+    if (n_dims == 2)
+      if (conn == 4)
+        conn = [0 1 0
+                1 1 1
+                0 1 0];
+      elseif (conn == 8)
+        conn = [1 1 1
+                1 1 1
+                1 1 1];
+      else
+        error ("%s: CONN must have a value of 4 or 8 for 2 dimensional matrices",
+               func);
+      endif
+
+    elseif (n_dims == 3)
+      if (conn == 6)
+        conn = false (3, 3, 3);
+        conn(:,2,2) = true;
+        conn(2,:,2) = true;
+        conn(2,2,:) = true;
+      elseif (conn == 18)
+        conn = false (3, 3, 3);
+        conn(2,:,:) = true;
+        conn(:,2,:) = true;
+        conn(:,:,2) = true;
+      elseif (conn == 26)
+        conn = true (3, 3, 3);
+      else
+        error (["%s: CONN must have a value of 6, 18, or 26 for 3 " ...
+                "dimensional matrices"], func);
+      endif
+    else
+      error (["%s: CONN must be defined as a binary matrix for matrices " ...
+              "with more than 3 dimensions"], func);
+    endif
+  endif
+endfunction