## Copyright (C) 2008 Søren Hauberg
## 
## 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, write to the Free Software
## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
##
## Note: The implementation and help text for the 'andy' edge detector came with
## the following notice:
##   Copyright (C) 1999 Andy Adler
##   This code has no warrany whatsoever.
##   Do what you like with this code as long as you
##   leave this copyright in place.

## -*- texinfo -*-
## @deftypefn {Function File} {@var{bw} =} edge (@var{im}, @var{method})
## @deftypefnx{Function File} {@var{bw} =} edge (@var{im}, @var{method}, @var{arg1}, @var{arg2})
## @deftypefnx{Function File} {[@var{bw}, @var{thresh}] =} edge (...)
## Detect edges in the given image using various methods. The first input @var{im}
## is the gray scale image in which edges are to be detected. The second argument
## controls which method is used for detecting the edges. The rest of the input
## arguments depend on the selected method. The first output @var{bw} is a 
## @code{logical} image containing the edges. Most methods also returns an automatically
## computed threshold as the second output.
##
## The @var{method} input argument can any of the following strings (the default
## value is "Sobel")
##
## @table @asis
## @item "Sobel"
## Finds the edges in @var{im} using the Sobel approximation to the
## derivatives. Edge points are defined as points where the length of
## the gradient exceeds a threshold and is larger than it's neighbours
## in either the horizontal or vertical direction. The threshold is passed to
## the method in the third input argument @var{arg1}. If one is not given, a
## threshold is automatically computed as 4*@math{M}, where @math{M} is the mean
## of the gradient of the entire image. The optional 4th input argument controls
## the direction in which the gradient is approximated. It can be either
## "horizontal", "vertical", or "both" (default).
##
## @item "Prewitt"
## Finds the edges in @var{im} using the Prewitt approximation to the
## derivatives. This method works just like "Sobel" except a different aproximation
## the gradient is used.
##
## @item "Roberts"
## Finds the edges in @var{im} using the Roberts approximation to the
## derivatives. Edge points are defined as points where the length of
## the gradient exceeds a threshold and is larger than it's neighbours
## in either the horizontal or vertical direction. The threshold is passed to
## the method in the third input argument @var{arg1}. If one is not given, a
## threshold is automatically computed as 6*@math{M}, where @math{M} is the mean
## of the gradient of the entire image. The optional 4th input argument can be
## either "thinning" (default) or "nothinning". If it is "thinning" a simple
## thinning procedure is applied to the edge image such that the edges are only
## one pixel wide. If @var{arg2} is "nothinning", this procedure is not applied.
##
## @item "Kirsch"
## Finds the edges in @var{im} using the Kirsch approximation to the
## derivatives. Edge points are defined as points where the length of
## the gradient exceeds a threshold and is larger than it's neighbours
## in either the horizontal or vertical direction. The threshold is passed to
## the method in the third input argument @var{arg1}. If one is not given, a
## threshold is automatically computed as @math{M}, where @math{M} is the mean
## of the gradient of the entire image. The optional 4th input argument controls
## the direction in which the gradient is approximated. It can be either
## "horizontal", "vertical", or "both" (default).
##
## @item "LoG"
## Finds edges in @var{im} by convolving with the Laplacian of Gaussian (LoG)
## filter, and finding zero crossings. Only zero crossings where the 
## filter response is larger than an automatically computed threshold are retained.
## The threshold is passed to the method in the third input argument @var{arg1}.
## If one is not given, a threshold is automatically computed as 0.75*@math{M},
## where @math{M} is the mean of absolute value of LoG filter response. The
## optional 4th input argument sets the spread of the LoG filter. By default
## this value is 2.
##
## @item "Zerocross"
## Finds edges in the image @var{im} by convolving it with the user-supplied filter
## @var{arg2} and finding zero crossings larger than the threshold @var{arg1}. If
## @var{arg1} is [] a threshold is computed as the mean value of the absolute
## filter response.
##
## @item "Canny"
## Finds edges using the Canny edge detector. The optional third input argument
## @var{arg1} sets the thresholds used in the hysteresis thresholding. If 
## @var{arg1} is a two dimensional vector it's first element is used as the lower
## threshold, while the second element is used as the high threshold. If, on the
## other hand, @var{arg1} is a single scalar it is used as the high threshold,
## while the lower threshold is 0.4*@var{arg1}. The optional 4th input argument
## @var{arg2} is the spread of the low-pass Gaussian filter that is used to smooth
## the input image prior to estimating gradients. By default this scale parameter
## is 2.
##
## @item "Lindeberg"
## Finds edges using in @var{im} using the differential geometric single-scale edge
## detector given by Tony Lindeberg. The optional third input argument @var{arg1}
## is the scale (spread of Gaussian filter) at which the edges are computed. By
## default this 2.
##
## @item "Andy"
## A.Adler's idea (c) 1999. Somewhat based on the canny method. The steps are
## @enumerate
## @item
## Do a Sobel edge detection and to generate an image at
## a high and low threshold.
## @item
## Edge extend all edges in the LT image by several pixels,
## in the vertical, horizontal, and 45 degree directions.
## Combine these into edge extended (EE) image.
## @item
## Dilate the EE image by 1 step.
## @item
## Select all EE features that are connected to features in
## the HT image.
## @end enumerate
## 
## The parameters for the method is given in a vector:
## @table @asis
## @item params(1)==0 or 4 or 8
## Perform x connected dilatation (step 3).
## @item params(2)
## Dilatation coeficient (threshold) in step 3.
## @item params(3)
## Length of edge extention convolution (step 2).
## @item params(4)
## Coeficient of extention convolution in step 2.
## @end table
## defaults = [8, 1, 3, 3]
##
## @end table
##
## @seealso{fspecial, nonmax_supress}
## @end deftypefn

function [bw, out_threshold, g45_out, g135_out] = edge (im, method, varargin)
  ## Get the image
  if (nargin == 0)
    error("edge: not enough input arguments");
  endif
  if ( !isgray(im) )
    error("edge: first input must be a gray-scale image");
  endif

  ## Get the method
  if (nargin == 1)
    method = "Sobel";
  endif
  if (!ischar(method))
    error("edge: second argument must be a string");
  endif
  method = lower(method);

  ## Perform the actual edge detection
  switch (method)
    #####################################
    ## S O B E L
    #####################################
    case "sobel"
      ## Get the direction argument
      direction = get_direction(varargin{:});
      ## Create filters;
      h1 = fspecial("sobel"); # horizontal
      h3 = h1'; # vertical
      ## Compute edge strength
      switch(direction)
        case "horizontal"
          strength = abs( conv2(im, h1, "same") );
        case "vertical"
          strength = abs( conv2(im, h3, "same") );
        case "both"
          strength = sqrt( conv2(im, h1, "same").^2 + ...
                           conv2(im, h3, "same").^2 );
      endswitch
      ## Get threshold
      if (nargin > 2 && isscalar(varargin{1}))
        thresh = varargin{1};
      else
        thresh = 2*mean(strength(:));
      endif
      ## Perform thresholding and simple thinning
      strength(strength<=thresh) = 0;
      bw = simple_thinning(strength);

    #####################################
    ## P R E W I T T
    #####################################
    case "prewitt"
      ## Get the direction argument
      direction = get_direction(varargin{:});
      ## Create filters;
      h1 = fspecial("prewitt"); # vertical
      h3 = h1'; # horizontal
      ## Compute edge strength
      switch(direction)
        case "vertical"
          strength = abs( conv2(im, h1, "same") );
        case "horizontal"
          strength = abs( conv2(im, h3, "same") );
        case "both"
          strength = sqrt( conv2(im, h1, "same").^2 + ...
                           conv2(im, h3, "same").^2 );
      endswitch
      ## Get threshold
      if (nargin > 2 && isscalar(varargin{1}))
        thresh = varargin{1};
      else
        thresh = 4*mean(strength(:));
      endif
      ## Perform thresholding and simple thinning
      strength(strength<=thresh) = 0;
      bw = simple_thinning(strength);
    
    #####################################
    ## K I R S C H
    #####################################
    case "kirsch"
      ## Get the direction argument
      direction = get_direction(varargin{:});
      ## Create filters;
      h1 = fspecial("kirsch"); # vertical
      h3 = h1'; # horizontal
      ## Compute edge strength
      switch(direction)
        case "vertical"
          strength = abs( conv2(im, h1, "same") );
        case "horizontal"
          strength = abs( conv2(im, h3, "same") );
        case "both"
          strength = sqrt( conv2(im, h1, "same").^2 + ...
                           conv2(im, h3, "same").^2 );
      endswitch
      ## Get threshold
      if nargin > 2 && isscalar(varargin{1})
        thresh = varargin{1};
      else
        thresh = mean(strength(:));
      endif
      ## Perform thresholding and simple thinning
      strength(strength<=thresh) = 0;
      bw = simple_thinning(strength);

    #####################################
    ## R O B E R T S
    #####################################
    case "roberts"
      ## Get the thinning argument (option)
      if (nargin == 4)
        option = varargin{2};
        if (!ischar(option))
          error("edge: 'option' must be a string");
        endif
        option = lower(option);
        if (!any(strcmp(option, {"thinning", "nothinning"})))
          error("edge: 'option' must be either 'thinning', or 'nothinning'");
        endif
      else
        option = "thinning";
      endif
      ## Create filters;
      h1 = [1 0; 0 -1]; 
      h2 = [0 1; -1 0]; 
      ## Compute edge strength
      g45  = conv2(im, h1, "same");
      g135 = conv2(im, h2, "same");
      strength = abs( g45 ) + abs( g135 );
      ## Get threshold
      if (nargin > 2 && isscalar(varargin{1}))
        thresh = varargin{1};
      else
        thresh = 6*mean(strength(:));
      endif
      ## Perform thresholding and simple thinning
      strength(strength<=thresh) = 0;
      if (strcmp(option, "thinning"))
        bw = simple_thinning(strength);
      else
        bw = (strength > 0);
      endif
      ## Check if g45 and g135 should be returned
      if (nargout == 4)
        g45_out  = g45;
        g135_out = g135;
      endif
    
    #####################################
    ## L A P L A C I A N   O F   G A U S S I A N
    #####################################
    case "log"
      ## Get sigma
      if (nargin == 4 && isscalar(varargin{2}))
        sigma = varargin{2};
      else
        sigma = 2;
      endif
      ## Create the filter
      s = ceil(3*sigma);
      %[x y] = meshgrid(-s:s);
      %f = (x.^2 + y.^2 - sigma^2) .* exp(-(x.^2 + y.^2)/(2*sigma^2));
      %f = f/sum(f(:));
      f = fspecial("log", 2*s+1, sigma);
      ## Perform convolution with the filter f
      g = conv2(im, f, "same");
      ## Get threshold
      if (nargin > 2 && isscalar(varargin{1}))
        thresh = varargin{1};
      else
        thresh = 0.75*mean(abs(g(:)));
      endif
      ## Find zero crossings
      zc = zerocrossings(g);
      bw = (abs(g) >= thresh) & zc;
    
    #####################################
    ## Z E R O   C R O S S I N G 
    #####################################
    case "zerocross"
      ## Get the filter
      if (nargin == 4 && ismatrix(varargin{2}))
        f = varargin{2};
      else
        error("edge: a filter must be given as the fourth argument when 'zerocross' is used");
      endif
      ## Perform convolution with the filter f
      g = conv2(im, f, "same");
      ## Get threshold
      if (nargin > 2 && isscalar(varargin{1}))
        thresh = varargin{1};
      else
        thresh = mean(abs(g(:)));
      endif
      ## Find zero crossings
      zc = zerocrossings(g);
      bw = (abs(g) >= thresh) & zc;

    #####################################
    ## C A N N Y 
    #####################################
    case "canny"
      ## Get sigma
      if (nargin == 4 && isscalar(varargin{2}))
        sigma = varargin{2};
      else
        sigma = 2;
      endif

      ## Change scale
      J = imsmooth(double(im), "Gaussian", sigma);

      ## Canny enhancer
      p = [1 0 -1]/2;
      Jx = conv2(J, p,  "same");
      Jy = conv2(J, p', "same");
      Es = sqrt( Jx.^2 + Jy.^2 );
      Eo = pi - mod (atan2 (Jy, Jx) - pi, pi);

      ## Get thresholds
      if (nargin > 2 && isscalar(varargin{1}))
        thresh = [0.4*varargin{1}, varargin{1}];
      elseif (nargin > 2 && ismatrix (varargin{1}) && length (varargin{1}(:)) == 2)
        thresh = varargin{1}(:);
      else
        tmp = mean(abs(Es(:)));
        thresh = [0.4*tmp, tmp];
      endif
      bw = nonmax_supress(Es, Eo, thresh(1), thresh(2));

    #####################################
    ## L I N D E B E R G 
    #####################################
    case "lindeberg"
      ## In case the user asks for more then 1 output argument
      ## we define thresh to be -1.
      thresh = -1;
      ## Get sigma
      if (nargin > 2 && isscalar(varargin{1}))
        sigma = varargin{1};
      else
        sigma = 2;
      endif
      ## Filters for computing the derivatives
      Px   = [-1 0 1; -1 0 1; -1 0 1];
      Py   = [1 1 1; 0 0 0; -1 -1 -1];
      Pxx  = conv2(Px,  Px, "full");
      Pyy  = conv2(Py,  Py, "full");
      Pxy  = conv2(Px,  Py, "full");
      Pxxx = conv2(Pxx, Px, "full");
      Pyyy = conv2(Pyy, Py, "full");
      Pxxy = conv2(Pxx, Py, "full");
      Pxyy = conv2(Pyy, Px, "full");
      ## Change scale
      L = imsmooth(double(im), "Gaussian", sigma);
      ## Compute derivatives
      Lx   = conv2(L, Px,   "same");
      Ly   = conv2(L, Py,   "same");
      Lxx  = conv2(L, Pxx,  "same");
      Lyy  = conv2(L, Pyy,  "same");
      Lxy  = conv2(L, Pxy,  "same");
      Lxxx = conv2(L, Pxxx, "same");
      Lyyy = conv2(L, Pyyy, "same");
      Lxxy = conv2(L, Pxxy, "same");
      Lxyy = conv2(L, Pxyy, "same");
      ## Compute directional derivatives
      Lvv  = Lx.^2.*Lxx + 2.*Lx.*Ly.*Lxy + Ly.^2.*Lyy;
      Lvvv = Lx.^3.*Lxxx + 3.*Lx.^2.*Ly.*Lxxy ...
           + 3.*Lx.*Ly.^2.*Lxyy + 3.*Ly.^3.*Lyyy;
      ## Perform edge detection
      bw = zerocrossings(Lvv) & Lvvv < 0;

    #####################################
    ## A N D Y
    #####################################
    case "andy"
      [bw, out_threshold] = andy (im, method, varargin{:});
    
    otherwise
      error("edge: unsupported edge detector: %s", method);
  endswitch
  
  if (nargout > 1)
    out_threshold = thresh;
  endif
endfunction

## An auxilary function that parses the 'direction' argument from 'varargin'
function direction = get_direction(varargin)
  if (nargin >= 2)
    direction = varargin{2};
    if (!ischar(direction))
      error("edge: direction must be a string");
    endif
    direction = lower(direction);
    if (!any(strcmp(direction, {"horizontal", "vertical", "both"})))
      error("edge :direction must be either 'horizontal', 'vertical', or 'both'");
    endif
  else
    direction = "both";
  endif
endfunction

## An auxilary function that performs a very simple thinning.
## Strength is an image containing the edge strength.
## bw contains a 1 in (r,c) if
##  1) strength(r,c) is greater than both neighbours in the
##     vertical direction, OR
##  2) strength(r,c) is greater than both neighbours in the
##     horizontal direction.
## Note the use of OR.
function bw = simple_thinning(strength)
  [r c] = size(strength);
  x = ( strength > [ zeros(r,1) strength(:,1:end-1) ] & ...
        strength > [ strength(:,2:end) zeros(r,1) ] );
  y = ( strength > [ zeros(1,c); strength(1:end-1,:) ] & ...
        strength > [ strength(2:end,:); zeros(1,c) ] );
  bw = x | y;
endfunction

## Auxilary function. Finds the zero crossings of the 
## 2-dimensional function f. (By Etienne Grossmann)
function z = zerocrossings(f)
  z0 = f<0;                 ## Negative
  [R,C] = size(f);
  z = zeros(R,C);
  z(1:R-1,:) |= z0(2:R,:);  ## Grow
  z(2:R,:) |= z0(1:R-1,:);
  z(:,1:C-1) |= z0(:,2:C);
  z(:,2:C) |= z0(:,1:C-1);

  z &= !z0;                  ## "Positive zero-crossings"?
endfunction

## The 'andy' edge detector that was present in older versions of 'edge'.
## The function body has simply been copied from the old implementation.
##   -- Soren Hauberg, march 11th, 2008
function [imout, thresh] = andy(im, method, thresh, param2)
   [n,m]= size(im);
   xx= 2:m-1;
   yy= 2:n-1;

   filt= [1 2 1;0 0 0; -1 -2 -1]/8;  tv= 2;
   imo= conv2(im, rot90(filt), 'same').^2 + conv2(im, filt, 'same').^2;
   if nargin<3 || thresh==[];
      thresh= sqrt( tv* mean(mean( imo(yy,xx) ))  );
   end
#     sum( imo(:)>thresh ) / prod(size(imo))
   dilate= [1 1 1;1 1 1;1 1 1]; tt= 1; sz=3; dt=3;
   if nargin>=4
      # 0 or 4 or 8 connected dilation
      if length(param2) > 0
         if      param2(1)==4 ; dilate= [0 1 0;1 1 1;0 1 0];
         elseif  param2(1)==0 ; dilate= 1;
         end
      end
      # dilation threshold
      if length(param2) > 2; tt= param2(2); end
      # edge extention length
      if length(param2) > 2; sz= param2(3); end
      # edge extention threshold
      if length(param2) > 3; dt= param2(4); end
      
   end
   fobliq= [0 0 0 0 1;0 0 0 .5 .5;0 0 0 1 0;0 0 .5 .5 0;0 0 1 0 0; 
                      0 .5 .5 0 0;0 1 0 0 0;.5 .5 0 0 0;1 0 0 0 0];
   fobliq= fobliq( 5-sz:5+sz, 3-ceil(sz/2):3+ceil(sz/2) );

   xpeak= imo(yy,xx-1) <= imo(yy,xx) & imo(yy,xx) > imo(yy,xx+1) ;
   ypeak= imo(yy-1,xx) <= imo(yy,xx) & imo(yy,xx) > imo(yy+1,xx) ;

   imht= ( imo >= thresh^2 * 2); # high threshold image   
   imht(yy,xx)= imht(yy,xx) & ( xpeak | ypeak );
   imht([1,n],:)=0; imht(:,[1,m])=0;

%  imlt= ( imo >= thresh^2 / 2); # low threshold image   
   imlt= ( imo >= thresh^2 / 1); # low threshold image   
   imlt(yy,xx)= imlt(yy,xx) & ( xpeak | ypeak );
   imlt([1,n],:)=0; imlt(:,[1,m])=0;

# now we edge extend the low thresh image in 4 directions

   imee= ( conv2( imlt, ones(2*sz+1,1)    , 'same') > tt ) | ...
         ( conv2( imlt, ones(1,2*sz+1)    , 'same') > tt ) | ...
         ( conv2( imlt, eye(2*sz+1)       , 'same') > tt ) | ...
         ( conv2( imlt, rot90(eye(2*sz+1)), 'same') > tt ) | ...
         ( conv2( imlt, fobliq            , 'same') > tt ) | ...
         ( conv2( imlt, fobliq'           , 'same') > tt ) | ...
         ( conv2( imlt, rot90(fobliq)     , 'same') > tt ) | ...
         ( conv2( imlt, flipud(fobliq)    , 'same') > tt );
#  imee(yy,xx)= conv2(imee(yy,xx),ones(3),'same') & ( xpeak | ypeak );
   imee= conv2(imee,dilate,'same') > dt; #

%  ff= find( imht==1 );
%  imout = bwselect( imee, rem(ff-1, n)+1, ceil(ff/n), 8);  
   imout = imee;

endfunction
