Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

ENH: add sum_angle() and sum_polar() functions to twodim_base.py #230

Open
wants to merge 1 commit into from

5 participants

Robert Jordens Ralf Gommers Charles Harris Travis E. Oliphant Stefan van der Walt
Robert Jordens

sum_angle() computes the sum of a 2-d array along an angled axis
sum_polar() along radial lines or along azimuthal circles

Robert Jordens jordens add sum_angle() and sum_polar() functions.
sum_angle() computes the sum of a 2-d array along an angled axis
sum_polar() along radial lines or along azimuthal circles
ba7766e
Ralf Gommers
Owner

Hi @jordens, I can see how this would be useful, but adding new functions should really be discussed on the numpy-discussion mailing list first. Could you please bring it up there?

Charles Harris charris commented on the diff
numpy/lib/twodim_base.py
((4 lines not shown))
+
+
+def sum_angle(m, angle, aspect=1., binsize=None):
+ """
+ Compute the sum of a 2-d array along an rotated axis.
+
+ Parameters
+ ----------
+ m : array_like, shape(N, M)
+ 2-d input array to be summed
+ angle : float
+ The angle of the summation direction defined such that:
+ ``sum_angle(m, angle=0) == sum(m, axis=0)``
+ ``sum_angle(m, angle=pi/2) == sum(m, axis=1)``
+ aspect : float, optional
+ The input bin aspect ratio (second dimension/first dimension)
Charles Harris Owner
charris added a note

Dimension or component?

Robert Jordens
jordens added a note

Dimension. Or I don`t understand what you mean by component.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Charles Harris
Owner

Correct me if I am wrong, but this looks like it is intended to work on images. Is that the case?

Robert Jordens

Images are certainly a major application. But this is also interesting in the analysis of matrices that represent graphs.

Charles Harris
Owner

I was asking because I thought this might be more appropriate for one of the image processing packages. In particular, it seems related to the Hough transform. I assume the application to graphs uses the adjacency matrix. In any case, it might be worth putting up a post on the scipy mailing list as well as here since that is where graph and image algorithms are located.

Travis E. Oliphant
Owner

The sum_angle function seems equivalent to the scipy.misc.radon function which is deprecated (but a very useful function). Because your implementation is so straightforward and well documented it could be useful in NumPy as well, but I agree with Chuck that you should ask on the SciPy list as well.

Robert Jordens

Yes. angle_sum is a Hough or Radon transform. The implementation in scipy has a couple of problems: It is based on PIL and does not seem to work with floats. It does not conserve the sum over all entries (independent of padding...). Finally, the interpolations used in PIL do not really make sense in this application. It is also 40% slower than this one.

Scikits-image has taken the radon transform from scipy: https://github.com/scikits-image/scikits-image/blob/master/skimage/transform/radon_transform.py I believe that one does not suffer the problems the PIL-based one has. But it looks even slower.

I'll raise this on the scipy/scikits list.

Stefan van der Walt
Owner

@jordens Could we use your sum_angle in scikits-image? I'd love to improve the execution speed of the radon and hough transforms (actually, I think the hough tf already uses the bincount trick, so it may not benefit). Perhaps you would be interested in working on a PR together?

Robert Jordens

@stefanv Sure. Feel free. I will help if I can find the time.

OTOH I would still love to see these added to numpy and agree with Travis' reasoning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 7, 2012
  1. Robert Jordens

    add sum_angle() and sum_polar() functions.

    jordens authored
    sum_angle() computes the sum of a 2-d array along an angled axis
    sum_polar() along radial lines or along azimuthal circles
This page is out of date. Refresh to see the latest.
Showing with 231 additions and 2 deletions.
  1. +231 −2 numpy/lib/twodim_base.py
233 numpy/lib/twodim_base.py
View
@@ -5,11 +5,12 @@
__all__ = ['diag','diagflat','eye','fliplr','flipud','rot90','tri','triu',
'tril','vander','histogram2d','mask_indices',
'tril_indices','tril_indices_from','triu_indices','triu_indices_from',
- ]
+ 'sum_angle', 'sum_polar']
from numpy.core.numeric import asanyarray, equal, subtract, arange, \
zeros, greater_equal, multiply, ones, asarray, alltrue, where, \
- empty, diagonal
+ empty, diagonal, sin, cos, arctan2, pi, floor
+from numpy.lib.function_base import bincount
def fliplr(m):
"""
@@ -881,3 +882,231 @@ def triu_indices_from(arr, k=0):
if not (arr.ndim == 2 and arr.shape[0] == arr.shape[1]):
raise ValueError("input array must be 2-d and square")
return triu_indices(arr.shape[0],k)
+
+
+def sum_angle(m, angle, aspect=1., binsize=None):
+ """
+ Compute the sum of a 2-d array along an rotated axis.
+
+ Parameters
+ ----------
+ m : array_like, shape(N, M)
+ 2-d input array to be summed
+ angle : float
+ The angle of the summation direction defined such that:
+ ``sum_angle(m, angle=0) == sum(m, axis=0)``
+ ``sum_angle(m, angle=pi/2) == sum(m, axis=1)``
+ aspect : float, optional
+ The input bin aspect ratio (second dimension/first dimension)
Charles Harris Owner
charris added a note

Dimension or component?

Robert Jordens
jordens added a note

Dimension. Or I don`t understand what you mean by component.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ binsize : float, optional
+ The output bin size in units of the first input dimension step
+ size. If no binsize is given, it defaults to the "natural bin
+ size" which is the larger projection of the two input step sizes
+ onto the output dimension (the axis perpendicular to the
+ summation axis).
+
+ Returns
+ -------
+ out : ndarray, shape(K)
+ The sum of `m` along the axis at `angle`
+
+ See Also
+ --------
+ sum_polar : similar method summing azimuthally or radially
+
+ Notes
+ -----
+ The summation angle is relative to the first dimension.
+
+ For 0<=angle<=pi/2 the value at [0,0] ends up in the first bin and
+ the value at [-1,-1] ends up in the last bin. Up to rounding, the
+ center value will always end up in the center bin.
+
+ For angle=3/4*pi the summation is along the diagonal.
+ For angle=1/4*pi the summation is along the antidiagonal.
+
+ The origin of the rotation is the [0,0] index. This determines the
+ bin rounding.
+
+ There is no interpolation and artefacts are likely if this function
+ is interpreted as an image processing function.
+
+ The full array sum is always strictly conserved:
+ ``sum_angle(m, t).sum() == m.sum()``
+
+ .. versionadded:: 1.4.0
+
+ Examples
+ --------
+ >>> m = np.arange(9.).reshape((3, 3))
+ >>> np.all(sum_angle(m, 0) == m.sum(axis=0))
+ True
+ >>> np.all(sum_angle(m, np.pi/2) == m.sum(axis=1))
+ True
+ >>> np.all(sum_angle(m, np.pi) == m.sum(axis=0)[::-1])
+ True
+ >>> np.all(sum_angle(m, 3*np.pi/2) == m.sum(axis=1)[::-1])
+ True
+ >>> np.all(sum_angle(m, 2*np.pi) == m.sum(axis=0))
+ True
+ >>> np.all(sum_angle(m, -np.pi/2) ==
+ ... sum_angle(m, 3*np.pi/2))
+ True
+ >>> d1 = np.array([0, 4, 12, 12, 8]) # antidiagonal
+ >>> d2 = np.array([2, 6, 12, 10, 6]) # diagonal
+ >>> np.all(sum_angle(m, np.pi/4) == d1)
+ True
+ >>> np.all(sum_angle(m, 3*np.pi/4) == d2)
+ True
+ >>> np.all(sum_angle(m, 5*np.pi/4) == d1[::-1])
+ True
+ >>> np.all(sum_angle(m, 7*np.pi/4) == d2[::-1])
+ True
+ >>> np.all(sum_angle(m, 0, aspect=2, binsize=1) ==
+ ... np.array([9, 0, 12, 0, 15]))
+ True
+ >>> np.all(sum_angle(m, 0, aspect=.5, binsize=1) ==
+ ... np.array([9, 12+15]))
+ True
+ >>> np.all(sum_angle(m, 0, aspect=.5) == m.sum(axis=0))
+ True
+ >>> np.all(sum_angle(m, np.pi/2, aspect=2, binsize=1) ==
+ ... m.sum(axis=1))
+ True
+ >>> m2 = np.arange(1e6).reshape((100, 10000))
+ >>> np.all(sum_angle(m2, 0) == m2.sum(axis=0))
+ True
+ >>> np.all(sum_angle(m2, np.pi/2) == m2.sum(axis=1))
+ True
+ >>> sum_angle(m2, np.pi/4).shape
+ (10099,)
+ >>> sum_angle(m2, np.pi/4).sum() == m2.sum()
+ True
+ """
+ m = asanyarray(m)
+ if m.ndim != 2:
+ raise ValueError("Input must be 2-d.")
+ if binsize is None:
+ binsize = max(abs(cos(angle)*aspect), abs(sin(angle)))
+ # first axis needs to be inverted due to the angle convention
+ m = m[::-1]
+ i, j = arange(m.shape[0])[:, None], np.arange(m.shape[1])[None, :]
+ k = (cos(angle)*aspect/binsize)*j - (sin(angle)/binsize)*i
+ cx, cy = (0, 0, -1, -1), (0, -1, 0, -1)
+ km = k[cx, cy].min()
+ # kp = k[cx, cy].max()
+ # minlength=kp-km
+ k = floor(k-(km-.5)).astype(int)
+ return bincount(k.ravel(), m.ravel())
+
+
+def sum_polar(m, center, direction, aspect=1., binsize=None):
+ """
+ Compute the sum of a 2-d array radially or azimuthally.
+
+ Parameters
+ ----------
+ m : array_like, shape(N, M)
+ 2-d input array to be summed
+ center : tuple(float, float)
+ The center of the summation measured from the [0, 0] index
+ in units of the two input step sizes
+ direction : "radial" or "azimuthal"
+ Summation direction
+ aspect : float, optional
+ The input bin aspect ratio (second dimension/first dimension)
+ binsize : int, optional
+ The output bin size. If None is given, and direction="radial"
+ then binsize=2*pi/100, else binsize=min(1, aspect).
+
+ Returns
+ -------
+ out : ndarray, shape(K)
+ The radial or azimuthal sum of `m`
+
+ See Also
+ --------
+ sum_angle : similar method summing along angled parallel lines
+
+ Notes
+ -----
+ The index of `out` is the binned radius or the binned angle, both
+ according to `binsize`.
+
+ Angles are measured from the positive second index axis towards the
+ negative first index axis. They correspond to mathematically
+ positive angles in the index coordinates of m[::-1] -- or the [0, 0]
+ index in the lower left.
+
+ If direction="azimuthal" then the length of the output is determined
+ by the maximal distance to the center. The radius-bins are [0,
+ binsize), [binsize, 2*binsize), ... up to [r, r+binsize) for some
+ value r with max_radius-binsize <= r < max_radius.
+
+ If direction="radial" the length is always 2*pi/binsize. This is not
+ the same as arctan2(i, j) which would distinguish +pi and -pi! The
+ azimuthal bins are therefore [-pi, -pi+binsize), [-pi+binsize,
+ 2*binsize), ... up to [p-binsize, p) for some p with pi-binsize <= p
+ < pi. The values at +pi end up in the first bin. See ``arctan2``
+ for the behaviour in other special cases.
+
+ There is no interpolation and artefacts are likely if this function
+ is interpreted as an image processing function.
+
+ The full array sum is always strictly conserved:
+ ``sum_polar(m, ...).sum() == m.sum()``
+
+ .. versionadded:: 1.4.0
+
+ Examples
+ --------
+ >>> m = np.arange(1., 10.).reshape((3, 3))
+ >>> sum_polar(m, (0, 0), "radial").sum() == m.sum()
+ True
+ >>> sum_polar(m, (0, 0), "azimuthal").sum() == m.sum()
+ True
+ >>> sum_polar(m, (1, 1), "radial").sum() == m.sum()
+ True
+ >>> sum_polar(m, (1, 1), "azimuthal").sum() == m.sum()
+ True
+ >>> sum_polar(m, (1, 1), "radial", binsize=np.pi/4)
+ array([ 4., 1., 2., 3., 11., 9., 8., 7.])
+ >>> sum_polar(m, (1, 1), "azimuthal", binsize=1.)
+ array([ 5., 40.])
+ >>> sum_polar(m, (1, 1), "azimuthal", binsize=2**.5/2)
+ array([ 5., 20., 20.])
+ >>> sum_polar(m, (.5, .5), "azimuthal", binsize=1)
+ array([ 12., 24., 9.])
+ >>> sum_polar(m, (0, 0), "radial", binsize=np.pi/8)
+ array([ 0., 0., 0., 0., 0., 0., 0., 0., 6., 6., 22.,
+ 0., 11., 0., 0., 0.])
+ >>> sum_polar(m, (0, 0), "radial", binsize=np.pi/2)
+ array([ 0., 0., 34., 11.])
+ >>> m2 = np.arange(123*345).reshape((123, 345))
+ >>> sum_polar(m2, (67, 89), "radial", binsize=2*np.pi/1011).shape[0]
+ 1011
+ """
+ m = asanyarray(m)
+ if m.ndim != 2:
+ raise ValueError("Input must be 2-d.")
+ i, j = arange(m.shape[0])[:, None], np.arange(m.shape[1])[None, :]
+ i, j = i-center[0], j-center[1]
+ if direction == "azimuthal":
+ k = (j**2*aspect**2+i**2)**.5
+ if binsize is None:
+ binsize = min(1., aspect)
+ minlength = None
+ elif direction == "radial":
+ k = arctan2(i, j*aspect)+pi
+ if binsize is None:
+ binsize = 2*pi/100
+ minlength = int(2*pi/binsize)+1
+ else:
+ raise ValueError("direction needs to be 'radial' or 'azimuthal'")
+ k = (k/binsize).astype(int)
+ r = bincount(k.ravel(), m.ravel(), minlength)
+ if direction == "radial":
+ assert r.shape[0] == minlength, (r.shape, minlength)
+ r[0] += r[-1]
+ r = r[:-1]
+ return r
Something went wrong with that request. Please try again.