2

Below is a code that reduces the resolution of a 2D numpy array (image) by binning small pixels into larger pixels. I am wondering if it can be made faster, or if there are alternatives that would be faster. Also, any suggestions in general are appreciated. For instance, if there is a code which is similar in speed, but which produces a smoother reduced image (for instance by using spline)

import numpy as np

def reduce_img ( img, bin_fac=1 ):
    assert( len( img.shape ) == 2 )
    imgX0 = img.shape[0] # slow axis
    imgX1 = img.shape[1] # fast axis

    r0 = imgX0 % bin_fac
    r1 = imgX1 % bin_fac

    img = np.copy( img) [:imgX0 - r0, :imgX1-r1]
    # bin the fast  axis 
    img_C = img.ravel(order='C')
    img = np.average( [ img_C[i::bin_fac] for i in xrange( bin_fac )   ],axis=0)
    img = img.reshape( (imgX0-r0, (imgX1-r1)/bin_fac ) , order='C')

    # bin the slow axis
    img_F = img.ravel(order='F')
    img = np.average( [ img_F[i::bin_fac] for i in xrange( bin_fac )   ],axis=0)
    img = img.reshape( ((imgX0-r0)/bin_fac, (imgX1-r1)/bin_fac ), order='F' )

    return img

Here is a result

>> from pylab import *
>> imshow( img ) 
>> show()

test image

>> img_r = reduce_img( img, bin_fac = 7 )
>> imshow( img_r )
>> show()

test image reduced

>> %timeit( reduce_img( img, bin_fac=7)  )
1000 loops, best of 3: 655 µs per loop
10
  • 1
    Are you perhaps looking for scipy.misc.imresize? Commented Mar 6, 2015 at 19:47
  • 1
    possible duplicate of Resampling a numpy array representing an image Commented Mar 6, 2015 at 19:48
  • 1
    I can't get your func. to even work. General definition of reshape us numpy.reshape(a, newshape, order='C') and you call it as if you've done img.reshape. The shape of your example image is (600, 400, 4) when, i.e., read in with np.imread. You try reshapeing it as it was only a tuple (x,y). Your new shape should be (x, y, 4). You call one reshape with np. , but not the second one. Additionally I don't think your generator expr. are doing what you think they're doing. If you think they take an np.average of a small block of pixels, they're not. (not like this at least). Commented Mar 7, 2015 at 2:33
  • @ljetibo, you are right, should be img = img.reshape( shape ) etc. I must have messed it up when copying it into stack overflow. Thanks for pointing that out! Commented Mar 7, 2015 at 15:19
  • Also, this is for a 2D image array, not a higher dimensional image such as RGB. Commented Mar 7, 2015 at 15:22

1 Answer 1

2

I'll start by first mentioning that your way of binning only seems rather unusual, which is what @ljetibo was referring to in the comments, I presume. I'll come back to this after the "optimization" talk.

First, you could improve your code slightly by getting rid of the superfluous call to np.copy, as you're only rebinding img to a view of the passed-in img. The ravel operation will return a copy then, unless the image shape was a multiple of the binning factor bin_fac.

Now, while the list comprehensions are fast, you're recreating a numpy array from a possibly non-contiguous list, which means you're copying memory from one place to another again. Those are all operations that eat away at the efficiency.

What you might be interested in is simply generating a highly memory-efficient view on the original image. That's where as_strided comes in:

from numpy.lib.stride_tricks import as_strided

def strided_rescale(g, bin_fac):
    strided = as_strided(g,
        shape=(g.shape[0]//bin_fac, g.shape[1]//bin_fac, bin_fac, bin_fac),
        strides=((g.strides[0]*bin_fac, g.strides[1]*bin_fac)+g.strides))
    return strided.mean(axis=-1).mean(axis=-1)  # order is NOT important! See notes..

Timing considerations show that this is usually a bit faster than the original method, showing improved performance as the binning factor increases:

In [263]: stp = 'from __main__ import img, strided_rescale, reduce_img'

In [264]: for n in range(1,21):
    a = timeit.timeit(stmt='strided_rescale(img, {})'.format(n), setup=stp, number=100)
    b = timeit.timeit(stmt='reduce_img(img, {})'.format(n), setup=stp, number=100)
    c = b*1./a
    d = np.ptp(strided_rescale(img, n) - reduce_img(img,n))
    print('{a:7f} | {b:7f} | {c:1.4f} | {d:1.4g}'.format(**locals()))
   .....:     
0.124911 | 0.277254 | 2.2196 | 0
0.303813 | 0.171833 | 0.5656 | 0
0.217745 | 0.188637 | 0.8663 | 0
0.162199 | 0.139770 | 0.8617 | 0
0.132355 | 0.138402 | 1.0457 | 0
0.121542 | 0.160275 | 1.3187 | 0
0.102930 | 0.162041 | 1.5743 | 0
0.090694 | 0.138881 | 1.5313 | 2.384e-07
0.097320 | 0.174690 | 1.7950 | 1.788e-07
0.082376 | 0.155261 | 1.8848 | 2.384e-07
0.084228 | 0.178397 | 2.1180 | 2.98e-07
0.070411 | 0.181175 | 2.5731 | 2.98e-07
0.075443 | 0.175605 | 2.3277 | 5.96e-08
0.068964 | 0.182602 | 2.6478 | 5.96e-08
0.067155 | 0.168532 | 2.5096 | 1.192e-07
0.056193 | 0.195684 | 3.4824 | 2.98e-07
0.063575 | 0.206987 | 3.2558 | 2.98e-07
0.078850 | 0.187697 | 2.3804 | 2.384e-07
0.053072 | 0.168763 | 3.1799 | 2.384e-07
0.047512 | 0.151598 | 3.1907 | 1.788e-07
# time a | time b   |  b/a   | peak-to-peak: check if both arrays are the same

I believe the observed minor differences in array equality are due to the copy operations where you're going from a numpy datatype back to a normal Python float and vice versa. I'm not 100% sure on this though.

Now that the optimization talk is over, let's go back to your binning method. With your current implementation, you have split up the image in square, non-overlapping zones. These submatrices needn't be square for the rest of this story, they could be rectangular (if the aspect ratio of the image can be altered) and the conclusions would still be valid. So, in each submatrix you are taking the row-wise mean, after which you take the mean of the resultant column vector. It can be easily shown mathematically that this is the same as taking the mean over the entire submatrix. That's good news, because it means in the strided_rescale function shown above, you could simply replace the return statement by:

return gstr.mean(axis=(-2,-1))

which will give you yet another (small) speed increase.

I thought the suggestion to use scipy.misc.imresize was a very good one, until I tried it on ndarrays with a dtype != np.uint8. And even then, the mode parameter has to be chosen correctly and it seems to be taking only the top-left corners of the sub-matrices:

In [39]: a = np.arange(16, dtype=np.uint8).reshape(4,4)

In [40]: a
Out[40]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]], dtype=uint8)

In [41]: imresize(a, (2,2), mode='P')
Out[41]: 
array([[ 0,  2],
       [ 8, 10]], dtype=uint8)

In [42]: imresize(a, (2,2), mode='L')
Out[42]: 
array([[0, 1],
       [6, 7]], dtype=uint8)

Which is probably not what you were after. So stride_tricks works well for the actual binning. If you want smooth resizing behaviour (e.g. by using spline interpolation), you'll be looking towards the Python Image Library and all functions that use it under the hood or e.g. OpenCV, which also provides resize behaviour as summarized in this post.

Sign up to request clarification or add additional context in comments.

1 Comment

Many thanks- These are the kind of tricks I was curious about!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.