diff --git a/skimage/transform/integral.py b/skimage/transform/integral.py index 3503a652..fd9f8725 100644 --- a/skimage/transform/integral.py +++ b/skimage/transform/integral.py @@ -1,7 +1,8 @@ import numpy as np +import collections -def integral_image(x): +def integral_image(img): """Integral image / summed area table. The integral image contains the sum of all elements above and to the @@ -13,13 +14,13 @@ def integral_image(x): Parameters ---------- - x : ndarray + img : ndarray Input image. Returns ------- S : ndarray - Integral image / summed area table. + Integral image/summed area table of same shape as input image. References ---------- @@ -27,44 +28,117 @@ def integral_image(x): ACM SIGGRAPH Computer Graphics, vol. 18, 1984, pp. 207-212. """ - return x.cumsum(1).cumsum(0) + S = img + for i in range(img.ndim): + S = S.cumsum(axis=i) + return S -def integrate(ii, r0, c0, r1, c1): +def integrate(ii, start, end, *args): """Use an integral image to integrate over a given window. Parameters ---------- ii : ndarray Integral image. - r0, c0 : int or ndarray - Top-left corner(s) of block to be summed. - r1, c1 : int or ndarray - Bottom-right corner(s) of block to be summed. + start : List of tuples, each tuple of length equal to dimension of `ii` + Coordinates of top left corner of window(s). + Each tuple in the list contains the starting row, col, ... index + i.e `[(row_win1, col_win1, ...), (row_win2, col_win2,...), ...]`. + end : List of tuples, each tuple of length equal to dimension of `ii` + Coordinates of bottom right corner of window(s). + Each tuple in the list containing the end row, col, ... index i.e + `[(row_win1, col_win1, ...), (row_win2, col_win2, ...), ...]`. + args: optional + For backward compatibility with versions prior to 0.11. + The earlier function signature was `integrate(ii, r0, c0, r1, c1)`, + where `r0`, `c0` are int(lists) specifying start coordinates + of window(s) to be integrated and `r1`, `c1` the end coordinates. Returns ------- S : scalar or ndarray Integral (sum) over the given window(s). + + Examples + -------- + >>> arr = np.ones((5, 6), dtype=np.float) + >>> ii = integral_image(arr) + >>> integrate(ii, [(1, 0)], [(1, 2)]) # sum from (1,0) -> (1,2) + array([ 3.]) + >>> integrate(ii, [(3, 3)], [(4, 5)]) # sum form (3,3) -> (4,5) + array([ 6.]) + >>> integrate(ii, [(1, 0), (3, 3)], [(1, 2), (4, 5)]) # sum from (1,0) -> (1,2) and (3,3) -> (4,5) + array([ 3., 6.]) """ - if np.isscalar(r0): - r0, c0, r1, c1 = [np.asarray([x]) for x in (r0, c0, r1, c1)] + rows = 1 + # handle input from new input format + if len(args) == 0: + if isinstance(start, collections.Iterable): + rows = len(start) + start = np.array(start) + end = np.array(end) + # handle deprecated input format + else: + if isinstance(start, collections.Iterable): + rows = len(start) + args = (start, end) + args + start = np.array(args[:int(len(args)/2)]).T + end = np.array(args[int(len(args)/2):]).T - S = np.zeros(r0.shape, ii.dtype) + total_shape = ii.shape + total_shape = np.tile(total_shape, [rows, 1]) - S += ii[r1, c1] + # convert negative indices into equivalent positive indices + start_negatives = start < 0 + end_negatives = end < 0 + start = (start + total_shape) * start_negatives + \ + start * ~(start_negatives) + end = (end + total_shape) * end_negatives + \ + end * ~(end_negatives) - good = (r0 >= 1) & (c0 >= 1) - S[good] += ii[r0[good] - 1, c0[good] - 1] + if np.any((end - start) < 0): + raise IndexError('end coordinates must be greater or equal to start') - good = r0 >= 1 - S[good] -= ii[r0[good] - 1, c1[good]] + # bit_perm is the total number of terms in the expression + # of S. For example, in the case of a 4x4 2D image + # sum of image from (1,1) to (2,2) is given by + # S = + ii[2, 2] + # - ii[0, 2] - ii[2, 0] + # + ii[0, 0] + # The total terms = 4 = 2 ** 2(dims) - good = c0 >= 1 - S[good] -= ii[r1[good], c0[good] - 1] + S = np.zeros(rows) + bit_perm = 2 ** ii.ndim + width = len(bin(bit_perm - 1)[2:]) - if S.size == 1: - return np.asscalar(S) + # Sum of a (hyper)cube, from an integral image is computed using + # values at the corners of the cube. The corners of cube are + # selected using binary numbers as described in the following example. + # In a 3D cube there are 8 corners. The corners are selected using + # binary numbers 000 to 111. Each number is called a permutation, where + # perm(000) means, select end corner where none of the coordinates + # is replaced, i.e ii[end_row, end_col, end_depth]. Similarly, perm(001) + # means replace last coordinate by start - 1, i.e + # ii[end_row, end_col, start_depth - 1], and so on. + # Sign of even permutations is positive, while those of odd is negative. + # If 'start_coord - 1' is -ve it is labeled bad and not considered in + # the final sum. + for i in range(bit_perm): # for all permutations + # boolean permutation array eg [True, False] for '10' + binary = bin(i)[2:].zfill(width) + bool_mask = [bit == '1' for bit in binary] + + sign = (-1)**sum(bool_mask) # determine sign of permutation + + bad = [np.any(((start[r] - 1) * bool_mask) < 0) + for r in range(rows)] # find out bad start rows + + corner_points = (end * (np.invert(bool_mask))) + \ + ((start - 1) * bool_mask) # find corner for each row + + S += [sign * ii[tuple(corner_points[r])] if(not bad[r]) else 0 + for r in range(rows)] # add only good rows return S diff --git a/skimage/transform/tests/test_integral.py b/skimage/transform/tests/test_integral.py index eb92a40b..e05ed397 100644 --- a/skimage/transform/tests/test_integral.py +++ b/skimage/transform/tests/test_integral.py @@ -17,15 +17,16 @@ def test_validity(): def test_basic(): - assert_equal(x[12:24, 10:20].sum(), integrate(s, 12, 10, 23, 19)) - assert_equal(x[:20, :20].sum(), integrate(s, 0, 0, 19, 19)) - assert_equal(x[:20, 10:20].sum(), integrate(s, 0, 10, 19, 19)) - assert_equal(x[10:20, :20].sum(), integrate(s, 10, 0, 19, 19)) + assert_equal(x[12:24, 10:20].sum(), integrate(s, (12, 10), (23, 19))) + assert_equal(x[:20, :20].sum(), integrate(s, (0, 0), (19, 19))) + assert_equal(x[:20, 10:20].sum(), integrate(s, (0, 10), (19, 19))) + assert_equal(x[10:20, :20].sum(), integrate(s, (10, 0), (19, 19))) def test_single(): - assert_equal(x[0, 0], integrate(s, 0, 0, 0, 0)) - assert_equal(x[10, 10], integrate(s, 10, 10, 10, 10)) + assert_equal(x[0, 0], integrate(s, (0, 0), (0, 0))) + assert_equal(x[10, 10], integrate(s, (10, 10), (10, 10))) + def test_vectorized_integrate(): r0 = np.array([12, 0, 0, 10, 0, 10, 30]) @@ -40,7 +41,10 @@ def test_vectorized_integrate(): x[0,0], x[10, 10], x[30:, 31:].sum()]) - assert_equal(expected, integrate(s, r0, c0, r1, c1)) + start_pts = [(r0[i], c0[i]) for i in range(len(r0))] + end_pts = [(r1[i], c1[i]) for i in range(len(r0))] + assert_equal(expected, integrate(s, r0, c0, r1, c1)) # test deprecated + assert_equal(expected, integrate(s, start_pts, end_pts)) if __name__ == '__main__':