% PHASECONG3 - Computes edge and corner phase congruency in an image.
%
% This function calculates the PC_2 measure of phase congruency.  
% This function supersedes PHASECONG2 and PHASECONG being faster and requires
% less memory
%
% There are potentially many arguments, here is the full usage:
%
%   [M m or ft pc EO, T] = phasecong3(im, nscale, norient, minWaveLength, ...
%                           mult, sigmaOnf, k, cutOff, g, noiseMethod)
%
% However, apart from the image, all parameters have defaults and the
% usage can be as simple as:
%
%    M = phasecong3(im);
% 
% Arguments:
%              Default values      Description
%
%    nscale           4    - Number of wavelet scales, try values 3-6
%    norient          6    - Number of filter orientations.
%    minWaveLength    3    - Wavelength of smallest scale filter.
%    mult             2.1  - Scaling factor between successive filters.
%    sigmaOnf         0.55 - Ratio of the standard deviation of the Gaussian 
%                            describing the log Gabor filter's transfer function 
%                            in the frequency domain to the filter center frequency.
%    k                2.0  - No of standard deviations of the noise energy beyond
%                            the mean at which we set the noise threshold point.
%                            You may want to vary this up to a value of 10 or
%                            20 for noisy images 
%    cutOff           0.5  - The fractional measure of frequency spread
%                            below which phase congruency values get penalized.
%    g                10   - Controls the sharpness of the transition in
%                            the sigmoid function used to weight phase
%                            congruency for frequency spread.                        
%    noiseMethod      -1   - Parameter specifies method used to determine
%                            noise statistics. 
%                              -1 use median of smallest scale filter responses
%                              -2 use mode of smallest scale filter responses
%                               0+ use noiseMethod value as the fixed noise threshold 
%
% Returned values:
%    M          - Maximum moment of phase congruency covariance.
%                 This is used as a indicator of edge strength.
%    m          - Minimum moment of phase congruency covariance.
%                 This is used as a indicator of corner strength.
%    or         - Orientation image in integer degrees 0-180,
%                 positive anticlockwise.
%                 0 corresponds to a vertical edge, 90 is horizontal.
%    ft         - Local weighted mean phase angle at every point in the
%                 image.  A value of pi/2 corresponds to a bright line, 0
%                 corresponds to a step and -pi/2 is a dark line.
%    pc         - Cell array of phase congruency images (values between 0 and 1)   
%                 for each orientation
%    EO         - A 2D cell array of complex valued convolution result
%    T          - Calculated noise threshold (can be useful for
%                 diagnosing noise characteristics of images).  Once you know
%                 this you can then specify fixed thresholds and save some
%                 computation time.
%
%   EO{s,o} = convolution result for scale s and orientation o.  The real part
%   is the result of convolving with the even symmetric filter, the imaginary
%   part is the result from convolution with the odd symmetric filter.
%
%   Hence:
%       abs(EO{s,o}) returns the magnitude of the convolution over the
%       image at scale s and orientation o.
%       angle(EO{s,o}) returns the phase angles.
%   
% Notes on specifying parameters:  
%
% The parameters can be specified as a full list eg.
%  >> [M m or ft pc EO] = phasecong3(im, 5, 6, 3, 2.5, 0.55, 2.0, 0.4, 10);
%
% or as a partial list with unspecified parameters taking on default values
%  >> [M m or ft pc EO] = phasecong3(im, 5, 6, 3);
%
% or as a partial list of parameters followed by some parameters specified via a
% keyword-value pair, remaining parameters are set to defaults, for example:
%  >> [M m or ft pc EO] = phasecong3(im, 5, 6, 3, 'cutOff', 0.3, 'k', 2.5);
% 
% The convolutions are done via the FFT.  Many of the parameters relate to the
% specification of the filters in the frequency plane.  The values do not seem
% to be very critical and the defaults are usually fine.  You may want to
% experiment with the values of 'nscales' and 'k', the noise compensation factor.
%
% Notes on filter settings to obtain even coverage of the spectrum
% sigmaOnf       .85   mult 1.3
% sigmaOnf       .75   mult 1.6     (filter bandwidth ~1 octave)
% sigmaOnf       .65   mult 2.1  
% sigmaOnf       .55   mult 3       (filter bandwidth ~2 octaves)
%
% See Also:  PHASECONG, PHASECONG2, PHASESYM, GABORCONVOLVE, PLOTGABORFILTERS

% References:
%
%     Peter Kovesi, "Image Features From Phase Congruency". Videre: A
%     Journal of Computer Vision Research. MIT Press. Volume 1, Number 3,
%     Summer 1999 http://mitpress.mit.edu/e-journals/Videre/001/v13.html
%
%     Peter Kovesi, "Phase Congruency Detects Corners and
%     Edges". Proceedings DICTA 2003, Sydney Dec 10-12

% April 1996     Original Version written 
% August 1998    Noise compensation corrected. 
% October 1998   Noise compensation corrected.   - Again!!!
% September 1999 Modified to operate on non-square images of arbitrary size. 
% May 2001       Modified to return feature type image. 
% July 2003      Altered to calculate 'corner' points. 
% October 2003   Speed improvements and refinements. 
% July 2005      Better argument handling, changed order of return values
% August 2005    Made Octave compatible
% May 2006       Bug in checkargs fixed
% Jan 2007       Bug in setting radius to 0 for odd sized images fixed.
% April 2009     Scaling of covariance values fixed. (Negligible change to results) 
% May 2009       Noise compensation simplified reducing memory and
%                computation overhead.  Spread function changed to a cosine,
%                eliminating parameter dThetaOnSigma and ensuring even
%                angular coverage.  Frequency width measure slightly
%                improved.
% November 2010  Cosine angular spread function corrected (it was 2x as wide
%                as it should have been)
% September 2017 Correction to setting up matrices of frequency values for
%                odd sized images.

% Copyright (c) 1996-2017 Peter Kovesi
% Centre for Exploration Targeting
% The University of Western Australia
% peter.kovesi at uwa edu au
% 
% Permission is hereby granted, free of charge, to any person obtaining a copy
% of this software and associated documentation files (the "Software"), to deal
% in the Software without restriction, subject to the following conditions:
% 
% The above copyright notice and this permission notice shall be included in 
% all copies or substantial portions of the Software.
%
% The Software is provided "as is", without warranty of any kind.

function [M, m, or, featType, PC, EO, T, pcSum] = phasecong3(varargin)
    
% Get arguments and/or default values    
    [im, nscale, norient, minWaveLength, mult, sigmaOnf, ...
                   k, cutOff, g, noiseMethod] = checkargs(varargin(:));     

    epsilon         = .0001;          % Used to prevent division by zero.
    [rows,cols] = size(im);
    imagefft = fft2(im);              % Fourier transform of image

    zero = zeros(rows,cols);
    EO = cell(nscale, norient);       % Array of convolution results.  
    PC = cell(norient,1);
    covx2 = zero;                     % Matrices for covariance data
    covy2 = zero;
    covxy = zero;

    EnergyV = zeros(rows,cols,3);     % Matrix for accumulating total energy
                                      % vector, used for feature orientation
                                      % and type calculation

    pcSum = zeros(rows,cols);                                      
    % Pre-compute some stuff to speed up filter construction
    
    % Set up X and Y matrices of frequency values.  The following code adjusts
    % things appropriately for odd and even values of rows and columns.
    if mod(cols,2)
        xrange = [-(cols-1)/2:(cols-1)/2]/cols;
    else
        xrange = [-cols/2:(cols/2-1)]/cols; 
    end
    
    if mod(rows,2)
        yrange = [-(rows-1)/2:(rows-1)/2]/rows;
    else
        yrange = [-rows/2:(rows/2-1)]/rows; 
    end
    
    [x,y] = meshgrid(xrange, yrange);
    
    radius = sqrt(x.^2 + y.^2);       % Matrix values contain *normalised* radius from centre.
    theta = atan2(-y,x);              % Matrix values contain polar angle.
                                      % (note -ve y is used to give +ve
                                      % anti-clockwise angles)
                                  
    radius = ifftshift(radius);       % Quadrant shift radius and theta so that filters
    theta  = ifftshift(theta);        % are constructed with 0 frequency at the corners.
    radius(1,1) = 1;                  % Get rid of the 0 radius value at the 0
                                      % frequency point (now at top-left corner)
                                      % so that taking the log of the radius will 
                                      % not cause trouble.
    sintheta = sin(theta);
    costheta = cos(theta);
    clear x; clear y; clear theta;    % save a little memory
    
    % Filters are constructed in terms of two components.
    % 1) The radial component, which controls the frequency band that the filter
    %    responds to
    % 2) The angular component, which controls the orientation that the filter
    %    responds to.
    % The two components are multiplied together to construct the overall filter.
    
    % Construct the radial filter components...
    % First construct a low-pass filter that is as large as possible, yet falls
    % away to zero at the boundaries.  All log Gabor filters are multiplied by
    % this to ensure no extra frequencies at the 'corners' of the FFT are
    % incorporated as this seems to upset the normalisation process when
    % calculating phase congrunecy.
    lp = lowpassfilter([rows,cols],.45,15);   % Radius .45, 'sharpness' 15

    logGabor = cell(1,nscale);

    for s = 1:nscale
        wavelength = minWaveLength*mult^(s-1);
        fo = 1.0/wavelength;                  % Centre frequency of filter.
        logGabor{s} = exp((-(log(radius/fo)).^2) / (2 * log(sigmaOnf)^2));  
        logGabor{s} = logGabor{s}.*lp;        % Apply low-pass filter
        logGabor{s}(1,1) = 0;                 % Set the value at the 0 frequency point of the filter
                                              % back to zero (undo the radius fudge).
    end
    
    %% The main loop...
    for o = 1:norient                    % For each orientation...
        % Construct the angular filter spread function
        angl = (o-1)*pi/norient;           % Filter angle.
        % For each point in the filter matrix calculate the angular distance from
        % the specified filter orientation.  To overcome the angular wrap-around
        % problem sine difference and cosine difference values are first computed
        % and then the atan2 function is used to determine angular distance.
        ds = sintheta * cos(angl) - costheta * sin(angl);    % Difference in sine.
        dc = costheta * cos(angl) + sintheta * sin(angl);    % Difference in cosine.
        dtheta = abs(atan2(ds,dc));                          % Absolute angular distance.
        % Scale theta so that cosine spread function has the right wavelength and clamp to pi    
        dtheta = min(dtheta*norient/2,pi);
        % The spread function is cos(dtheta) between -pi and pi.  We add 1,
        % and then divide by 2 so that the value ranges 0-1
        spread = (cos(dtheta)+1)/2;        
        
        sumE_ThisOrient   = zero;          % Initialize accumulator matrices.
        sumO_ThisOrient   = zero;       
        sumAn_ThisOrient  = zero;      
        Energy            = zero;      

        for s = 1:nscale,                  % For each scale...
            filter = logGabor{s} .* spread;      % Multiply radial and angular
                                                 % components to get the filter. 
                                                 
            % Convolve image with even and odd filters returning the result in EO
            EO{s,o} = ifft2(imagefft .* filter);      

            An = abs(EO{s,o});                         % Amplitude of even & odd filter response.
            sumAn_ThisOrient = sumAn_ThisOrient + An;  % Sum of amplitude responses.
            sumE_ThisOrient = sumE_ThisOrient + real(EO{s,o}); % Sum of even filter convolution results.
            sumO_ThisOrient = sumO_ThisOrient + imag(EO{s,o}); % Sum of odd filter convolution results.
            
            % At the smallest scale estimate noise characteristics from the
            % distribution of the filter amplitude responses stored in sumAn. 
            % tau is the Rayleigh parameter that is used to describe the
            % distribution.
            if s == 1 
                if noiseMethod == -1     % Use median to estimate noise statistics
                    tau = median(sumAn_ThisOrient(:))/sqrt(log(4));   
                elseif noiseMethod == -2 % Use mode to estimate noise statistics
                    tau = rayleighmode(sumAn_ThisOrient(:));
                end
                maxAn = An;
            else
                % Record maximum amplitude of components across scales.  This is needed
                % to determine the frequency spread weighting.
                maxAn = max(maxAn,An);   
            end
        end                                       % ... and process the next scale

        % Accumulate total 3D energy vector data, this will be used to
        % determine overall feature orientation and feature phase/type
        EnergyV(:,:,1) = EnergyV(:,:,1) + sumE_ThisOrient;
        EnergyV(:,:,2) = EnergyV(:,:,2) + cos(angl)*sumO_ThisOrient;
        EnergyV(:,:,3) = EnergyV(:,:,3) + sin(angl)*sumO_ThisOrient;
        
        % Get weighted mean filter response vector, this gives the weighted mean
        % phase angle.
        XEnergy = sqrt(sumE_ThisOrient.^2 + sumO_ThisOrient.^2) + epsilon;   
        MeanE = sumE_ThisOrient ./ XEnergy; 
        MeanO = sumO_ThisOrient ./ XEnergy; 
        
        % Now calculate An(cos(phase_deviation) - | sin(phase_deviation)) | by
        % using dot and cross products between the weighted mean filter response
        % vector and the individual filter response vectors at each scale.  This
        % quantity is phase congruency multiplied by An, which we call energy.

        for s = 1:nscale,       
            E = real(EO{s,o}); O = imag(EO{s,o});    % Extract even and odd
                                                     % convolution results.
            Energy = Energy + E.*MeanE + O.*MeanO - abs(E.*MeanO - O.*MeanE);
        end

        %% Automatically determine noise threshold
        %
        % Assuming the noise is Gaussian the response of the filters to noise will
        % form Rayleigh distribution.  We use the filter responses at the smallest
        % scale as a guide to the underlying noise level because the smallest scale
        % filters spend most of their time responding to noise, and only
        % occasionally responding to features. Either the median, or the mode, of
        % the distribution of filter responses can be used as a robust statistic to
        % estimate the distribution mean and standard deviation as these are related
        % to the median or mode by fixed constants.  The response of the larger
        % scale filters to noise can then be estimated from the smallest scale
        % filter response according to their relative bandwidths.
        %
        % This code assumes that the expected reponse to noise on the phase congruency
        % calculation is simply the sum of the expected noise responses of each of
        % the filters.  This is a simplistic overestimate, however these two
        % quantities should be related by some constant that will depend on the
        % filter bank being used.  Appropriate tuning of the parameter 'k' will
        % allow you to produce the desired output. 
        
        if noiseMethod >= 0     % We are using a fixed noise threshold
            T = noiseMethod;    % use supplied noiseMethod value as the threshold
        else
            % Estimate the effect of noise on the sum of the filter responses as
            % the sum of estimated individual responses (this is a simplistic
            % overestimate). As the estimated noise response at succesive scales
            % is scaled inversely proportional to bandwidth we have a simple
            % geometric sum.
            totalTau = tau * (1 - (1/mult)^nscale)/(1-(1/mult));
            
            % Calculate mean and std dev from tau using fixed relationship
            % between these parameters and tau. See
            % http://mathworld.wolfram.com/RayleighDistribution.html
            EstNoiseEnergyMean = totalTau*sqrt(pi/2);        % Expected mean and std
            EstNoiseEnergySigma = totalTau*sqrt((4-pi)/2);   % values of noise energy
            
            T =  EstNoiseEnergyMean + k*EstNoiseEnergySigma; % Noise threshold
        end
        
        % Apply noise threshold,  this is effectively wavelet denoising via
        % soft thresholding.
        Energy = max(Energy - T, 0);         
        
        % Form weighting that penalizes frequency distributions that are
        % particularly narrow.  Calculate fractional 'width' of the frequencies
        % present by taking the sum of the filter response amplitudes and dividing
        % by the maximum amplitude at each point on the image.   If
        % there is only one non-zero component width takes on a value of 0, if
        % all components are equal width is 1.
        width = (sumAn_ThisOrient./(maxAn + epsilon) - 1) / (nscale-1);    

        % Now calculate the sigmoidal weighting function for this orientation.
        weight = 1.0 ./ (1 + exp( (cutOff - width)*g)); 

        % Apply weighting to energy and then calculate phase congruency
        PC{o} = weight.*Energy./sumAn_ThisOrient;   % Phase congruency for this orientatio

        pcSum = pcSum+PC{o};
        
        % Build up covariance data for every point
        covx = PC{o}*cos(angl);
        covy = PC{o}*sin(angl);
        covx2 = covx2 + covx.^2;
        covy2 = covy2 + covy.^2;
        covxy = covxy + covx.*covy;
        
    end  % For each orientation

    %% Edge and Corner calculations
    % The following is optimised code to calculate principal vector
    % of the phase congruency covariance data and to calculate
    % the minimumum and maximum moments - these correspond to
    % the singular values.
    
    % First normalise covariance values by the number of orientations/2
    covx2 = covx2/(norient/2);
    covy2 = covy2/(norient/2);
    covxy = 4*covxy/norient;   % This gives us 2*covxy/(norient/2)
    denom = sqrt(covxy.^2 + (covx2-covy2).^2)+epsilon;
    M = (covy2+covx2 + denom)/2;          % Maximum moment
    m = (covy2+covx2 - denom)/2;          % ... and minimum moment
    
    % Orientation and feature phase/type computation
    or = atan2(EnergyV(:,:,3), EnergyV(:,:,2));
    or(or<0) = or(or<0)+pi;       % Wrap angles -pi..0 to 0..pi
    or = round(or*180/pi);        % Orientation in degrees between 0 and 180
    
    OddV = sqrt(EnergyV(:,:,2).^2 + EnergyV(:,:,3).^2);
    featType = atan2(EnergyV(:,:,1), OddV);  % Feature phase  pi/2 <-> white line,
                                             % 0 <-> step, -pi/2 <-> black line

                                             
%%------------------------------------------------------------------
% CHECKARGS
%
% Function to process the arguments that have been supplied, assign
% default values as needed and perform basic checks.
    
function [im, nscale, norient, minWaveLength, mult, sigmaOnf, ...
          k, cutOff, g, noiseMethod] = checkargs(arg)

    nargs = length(arg);
    
    if nargs < 1
        error('No image supplied as an argument');
    end    
    
    % Set up default values for all arguments and then overwrite them
    % with with any new values that may be supplied
    im              = [];
    nscale          = 4;     % Number of wavelet scales.    
    norient         = 6;     % Number of filter orientations.
    minWaveLength   = 3;     % Wavelength of smallest scale filter.    
    mult            = 2.1;   % Scaling factor between successive filters.    
    sigmaOnf        = 0.55;  % Ratio of the standard deviation of the
                             % Gaussian describing the log Gabor filter's
                             % transfer function in the frequency domain
                             % to the filter center frequency.    
    k               = 2.0;   % No of standard deviations of the noise
                             % energy beyond the mean at which we set the
                             % noise threshold point. 
    cutOff          = 0.5;   % The fractional measure of frequency spread
                             % below which phase congruency values get penalized.
    g               = 10;    % Controls the sharpness of the transition in
                             % the sigmoid function used to weight phase
                             % congruency for frequency spread.  
    noiseMethod     = -1;    % Choice of noise compensation method. 
    
    % Allowed argument reading states
    allnumeric   = 1;       % Numeric argument values in predefined order
    keywordvalue = 2;       % Arguments in the form of string keyword
                            % followed by numeric value
    readstate = allnumeric; % Start in the allnumeric state
    
    if readstate == allnumeric
        for n = 1:nargs
            if isa(arg{n},'char')
                readstate = keywordvalue;
                break;
            else
                if     n == 1, im            = arg{n}; 
                elseif n == 2, nscale        = arg{n};              
                elseif n == 3, norient       = arg{n};
                elseif n == 4, minWaveLength = arg{n};
                elseif n == 5, mult          = arg{n};
                elseif n == 6, sigmaOnf      = arg{n};
                elseif n == 7, k             = arg{n};
                elseif n == 8, cutOff        = arg{n};
                elseif n == 9, g             = arg{n};
                elseif n == 10,noiseMethod   = arg{n};
                end
            end
        end
    end

    % Code to handle parameter name - value pairs
    if readstate == keywordvalue
        while n < nargs
            
            if ~isa(arg{n},'char') || ~isa(arg{n+1}, 'double')
                error('There should be a parameter name - value pair');
            end
            
            if     strncmpi(arg{n},'im'      ,2), im =        arg{n+1};
            elseif strncmpi(arg{n},'nscale'  ,2), nscale =    arg{n+1};
            elseif strncmpi(arg{n},'norient' ,4), norient =   arg{n+1};
            elseif strncmpi(arg{n},'minWaveLength',2), minWaveLength = arg{n+1};
            elseif strncmpi(arg{n},'mult'    ,2), mult =      arg{n+1};
            elseif strncmpi(arg{n},'sigmaOnf',2), sigmaOnf =  arg{n+1};
            elseif strncmpi(arg{n},'k'       ,1), k =         arg{n+1};
            elseif strncmpi(arg{n},'cutOff'  ,2), cutOff   =  arg{n+1};
            elseif strncmpi(arg{n},'g'       ,1), g        =  arg{n+1};         
            elseif strncmpi(arg{n},'noiseMethod' ,4), noiseMethod =  arg{n+1};                     
            else   error('Unrecognised parameter name');
            end

            n = n+2;
            if n == nargs
                error('Unmatched parameter name - value pair');
            end
            
        end
    end
    
    if isempty(im)
        error('No image argument supplied');
    end

    if ndims(im) == 3
        warning('Colour image supplied: converting image to greyscale...')
        im = double(rgb2gray(im));
    end
    
    if ~isa(im, 'double')
        im = double(im);
    end

    if nscale < 1
        error('nscale must be an integer >= 1');
    end
    
    if norient < 1 
        error('norient must be an integer >= 1');
    end    

    if minWaveLength < 2
        error('It makes little sense to have a wavelength < 2');
    end          

    if cutOff < 0 || cutOff > 1
        error('Cut off value must be between 0 and 1');
    end
    
%%-------------------------------------------------------------------------
% RAYLEIGHMODE
%
% Computes mode of a vector/matrix of data that is assumed to come from a
% Rayleigh distribution.
%
% Usage:  rmode = rayleighmode(data, nbins)
%
% Arguments:  data  - data assumed to come from a Rayleigh distribution
%             nbins - Optional number of bins to use when forming histogram
%                     of the data to determine the mode.
%
% Mode is computed by forming a histogram of the data over 50 bins and then
% finding the maximum value in the histogram.  Mean and standard deviation
% can then be calculated from the mode as they are related by fixed
% constants.
%
% mean = mode * sqrt(pi/2)
% std dev = mode * sqrt((4-pi)/2)
% 
% See
% http://mathworld.wolfram.com/RayleighDistribution.html
% http://en.wikipedia.org/wiki/Rayleigh_distribution
%

function rmode = rayleighmode(data, nbins)
    
    if nargin == 1
        nbins = 50;  % Default number of histogram bins to use
    end

    mx = max(data(:));
    edges = 0:mx/nbins:mx;
    n = histc(data(:),edges); 
    [dum,ind] = max(n); % Find maximum and index of maximum in histogram 

    rmode = (edges(ind)+edges(ind+1))/2;




