# -*- coding: utf-8 -*-
# radialcenter_ImAnClass.py
"""
Author:   Raghuveer Parthasarathy
Created on Mon Oct 31 13:33:05 2022
Last modified on Mon Oct 31 13:33:05 2022

Description
-----------

Particle localization by radial symmetry
Python translation of MATLAB radialcenter.m

** Version for Image Analysis Class**
Same as radialcenter.py , but with sigma and meand2 outputs removed, 
   and output positions returned relative to image center.

Uses 0 indexing of positions (unlike MATLAB)
NOTE: *Does not* optimize for image stacks (like radialcenter_stk.m);
      just single image

Copyright 2011-2022, Raghuveer Parthasarathy, The University of Oregon

Calculates the center of a 2D intensity distribution.
Method: Considers lines passing through each half-pixel point with slope
parallel to the gradient of the intensity at that point.  Considers the
distance of closest approach between these lines and the coordinate
origin, and determines (analytically) the origin that minimizes the
weighted sum of these distances-squared.
Applies simple smoothing if size > 3x3

Inputs
  I  : 2D intensity distribution (i.e. a grayscale image)
       Size need not be an odd number of pixels along each dimension

Outputs
  xc, yc : the center of radial symmetry, px, relative to image center
           Note that y increases with increasing row number (i.e. "downward")

To do:
    - Test more (like MATLAB version)
    - Faster grid creation than meshgrid? (like in MATLAB code)
    
see notes August 19-25, Sept. 9, Sept. 19-20 2011
Raghuveer Parthasarathy
The University of Oregon
August 21, 2011 (begun)

"""

import numpy as np   # Will assume numpy is already imported as np !


def radialcenter(I):
    # The main function -- see header comments for details
    
    (Ny, Nx) = I.shape
    # grid coordinates are -n:n, where Nx (or Ny) = 2*n+1
    # grid midpoint coordinates are -n+0.5:n-0.5
    xm, ym = np.meshgrid(np.arange(-(Nx-1)/2.0 + 0.5, (Nx-1)/2.0+0.5), 
                         np.arange(-(Ny-1)/2.0 + 0.5, (Ny-1)/2.0+0.5))
    # Calculate derivatives along 45-degree shifted coordinates (u and v)
    # Note that y increases "downward" (increasing row number) -- we'll deal
    # with this when calculating "m" below.
    dIdu = I[0:Ny-1,1:]-I[1:,0:Nx-1]
    dIdv = I[0:Ny-1,0:Nx-1]-I[1:,1:]
    # Smoothing -- perhaps should be optional
    fdu = dIdu # will overwrite if smoothing
    fdv = dIdv
    if np.min((Nx, Ny))>3:
        # Only smooth if image is >3px in the smallest dimension
        # Smooth by simple 3x3 boxcar, which I'll code directly rather than
        #    calling a convolution.
        # Zero-pad (expand by 1 on each side)
        dIdu_pad = np.zeros((Ny+1,Nx+1)) # dIdu array is size Ny-1, Nx-1
        dIdv_pad = np.zeros((Ny+1,Nx+1)) # dIdv array is size Ny-1, Nx-1
        dIdu_pad[1:Ny, 1:Nx] = dIdu
        dIdv_pad[1:Ny, 1:Nx] = dIdv
        fdu = np.zeros_like(dIdu)
        fdv = np.zeros_like(dIdv)
        for j in range(Ny-1):
            for k in range(Nx-1):
                fdu[j,k] = np.mean(dIdu_pad[j:j+3,k:k+3])
                fdv[j,k] = np.mean(dIdv_pad[j:j+3,k:k+3])
    dImag2 = fdu*fdu + fdv*fdv # gradient magnitude, squared
    
    # Slope of the gradient .  Note that we need a 45 degree rotation of 
    # the u,v components to express the slope in the x-y coordinate system.
    # The negative sign "flips" the array to account for y increasing
    # "downward"
    m = -(fdv + fdu) / (fdu-fdv)
    # Not smoothed version: m = -(dIdv + dIdu) ./ (dIdu-dIdv)
    
    infslope = 9e9 #replace infinite slope values with this extremely large number
    m[np.isinf(m)] = infslope
    
    # Shorthand "b", which also happens to be the
    # y intercept of the line of slope m that goes through each grid midpoint
    b = ym - m*xm
    
    # Weighting: weight by square of gradient magnitude and inverse 
    # distance to gradient intensity centroid.
    sdI2 = np.sum(dImag2)
    xcentroid = np.sum(dImag2*xm)/sdI2
    ycentroid = np.sum(dImag2*ym)/sdI2
    w  = dImag2/np.sqrt((xm-xcentroid)*(xm-xcentroid) + 
                        (ym-ycentroid)*(ym-ycentroid))
    
    # if the intensity is completely flat, m will be NaN (0/0)
    # give these points zero weight (and set m, b = 0 to avoid 0*NaN=NaN)
    w[np.isnan(m)]=0
    b[np.isnan(m)]=0
    m[np.isnan(m)]=0
    
    # least-squares minimization to determine the translated coordinate
    # system origin (xc, yc) such that lines y = mx+b have
    # the minimal total distance^2 to the origin:
    # Unilke the MATLAB version, where I have a separate function
    #   for this (lsradialcenterfit), I'll just write the calculation here:
    # Note m, b, w are defined on a grid;  w are the weights for each point
    wm2p1 = w/(m*m+1)
    sw  = np.sum(wm2p1)
    smmw = np.sum(m*m*wm2p1)
    smw  = np.sum(m*wm2p1)
    smbw = np.sum(m*b*wm2p1)
    sbw  = np.sum(b*wm2p1)
    det = smw*smw - smmw*sw
    xc = (smbw*sw - smw*sbw)/det # relative to image center
    yc = (smbw*smw - smmw*sbw)/det # relative to image center
    
    
    return xc, yc

