Image gradients become inaccurate when downscaling using a variety of different methods
Question: We have a fairly complex image processing script written in Python which is
using PIL and numpy. For one of the steps, we have a very sensitive multi
channel gradients which is a lookup table. Once it has been created, it is
saved down to multiple different smaller resolutions. When this happens
however, the green channel, which has a gradient running left to right,
suddenly appears to lose percision. It is supposed to lose 1 of 255 values
every 50 pixels or so. Instead, it starts dropping by values of 2 at every 100
pixels. This causes huge issues and I can't figure out why PIL is doing it.
However, I do see jumps of 1 in other portions of the map so I don't think its
a simple as its missing one bit of precision. I also noticed on another
channel, it seemed like the whole map was shifted by 1 value. The entire thing
seems inaccurate once scaled, even when using the "Nearest" filter.

For the full size image, we create it from our numpy array with the following:

    
    
    image = Image.fromarray(imageIn.astype(np.uint8))
    

We then scale it down:

    
    
    new_image = image.resize(new_size, scaleFilter)
    

The scale is always half the largest and I have tried all available scale
options.

We then save it to a PNG as follows:

    
    
    new_image.save(file_name, 'PNG')
    

We save both the large one directly after step 1 with the same save command
and it is fine. After the scale, we have the issue on the green channel. Any
help would be great!

EDIT:

It now appears that it is likley an issue in SciPy. The following still causes
the issue:

    
    
        new_array = misc.imresize(imageIn, (x_size, y_size, 4), interp='nearest')
        misc.imsave(file_name,new_array)
    

I do not understand how I am even getting the distortions with nearest. I am
allocating this array as a float64, but it has to involve rounding issues
within the code

EDIT #2:

I took this a step further and tried OSX built in program sips to download it
and got the same distortion! I then tried it with Adobe After Effects and it
worked fine. I then installed imagemagick which now works fine. I will still
award the bounty to anyone who can explain why this is happening within all
these methods.

EDIT #3

Per the request, here is a section of a sprite map scaled and unscaled. During
creating these, I found the OSX's built in "Preview" application also causes
scaling issues when scaling down so I actually had to use photoshop to get the
original clip.

Original:

[![enter image description
here](http://i.stack.imgur.com/uY173.png)](http://i.stack.imgur.com/uY173.png)

Scaled with distortions. Try looking at the green channel along the horizontal
axis

[![enter image description
here](http://i.stack.imgur.com/Zo0Zk.png)](http://i.stack.imgur.com/Zo0Zk.png)

Note that these clippings are not of the exact same pixels, but cut from the
same area as you can see by the shape

EDIT #4

I have now tried doing this scaling via OpenGL within the application and I
have found it happens there too! This has to do with some fundamental issue of
doing bilinear interpolation with a fixed number of bits?


Answer: The following code appears to do the right thing when scaling by 50%, using
skimage:

    
    
    import numpy
    import skimage
    import skimage.io
    
    img = skimage.io.imread('uY173.png')
    
    import skimage.transform
    
    img50_order0 = skimage.img_as_ubyte( skimage.transform.rescale(img, 0.5, order=0, clip=True) )
    img50_order1 = skimage.img_as_ubyte( skimage.transform.rescale(img, 0.5, order=1, clip=True) )
    
    img50_lm = numpy.rint( skimage.transform.downscale_local_mean(img, (2,2,1), clip=True) )
    
    import scipy.ndimage.interpolation
    
    img50_nd = scipy.ndimage.interpolation.zoom(img, (0.5, 0.5, 1))
    
    # plot section of green channel along horizontal axis
    plot(img50_order0[50, :, 1])
    plot(img50_order1[50, :, 1])
    plot(img50_lm[50, :, 1])
    plot(img50_nd[50, :, 1])
    

This does not (as far as I can tell) depend on PIL under the hood. The source
image is read as uint8, processed and rounded in subtly different ways in each
one, resulting in a uint8 output. The difference between all of these is never
more than 1 though, and the steps are never size 2.

