How to downscale an image without losing discrete values?

Viewed 562

I have an image of a city with discrete colors (Green=meadow, black=buildings, white/yellow=roads). Using Pillow, I import the picture in my (Python) program and convert it to a Numpy array with discrete values for the colors (i.e. green pixels become 1's, black pixels become 2's, etc).

I want to downscale the resolution of the image (for computational purposes) while retaining as much information as possible. However, using Pillow's resize() method, colors deviate from these discrete values. How can I downscale this image while (most importantly) retaining the discrete colors and (also important) with losing as little information as possible?

Here an example of the image: https://i.imgur.com/6Tef55H.png

EDIT: per request, some code:

from PIL import Image
import Numpy as np

picture = Image.open(some_image.png)

width, height = picture.size

pic_array = np.zeros(width,height)

# Turn the image into discrete values
for i in range(0,width):
    for j in range(0,height):
        red, green, blue = picture.getpixel((i,j))
        if red == a and green == b and blue == c:
            #An example of how discrete colors are converted to values
            pic_array[i][j] = 1

Scaling can be done in two ways:

1) Scaling the original image using Pillow's resize library or 2) rescaling the final array using something like:

scaled_array = pic_array[0:width:5, 0:height,5]

Option 1 is "well" in terms of retaining information but loses discrete values, while option 2 does it the other way around.

1 Answers

I was interested in this question and wrote some code to try out some ideas - specifically the "mode" filter suggested by @jasonharper in the comments. So, I programmed it up.

First of all the input image is not 4 nicely defined classes, but actually has 6,504 different colours, so I made a palette of 4 colours using ImageMagick like this:

magick xc:black xc:white xc:yellow xc:green +append palette.png

Here it is enlarged - in reality is 4x1 pixels:

enter image description here

Then I mapped the colours in the image to the palette of 4 discrete colours:

magick map.png +dither -remap palette.png start.png

enter image description here

Then I tried this code to calculate the median and the mode of each 3x3 window:

#!/usr/bin/env python3

from PIL import Image
import numpy as np
from scipy import stats
from skimage.util import view_as_blocks

# Open image and make into Numpy array
im = Image.open('start.png')
na = np.array(im)

# Make a view as 3x3 blocks - crop anything not a multiple of 3
block_shape=(3,3)
view = view_as_blocks(na[:747,:], block_shape)
flatView = view.reshape(view.shape[0], view.shape[1], -1)   # now (249,303,9)

# Get median of each 3x3 block
resMedian = np.median(flatView, axis=2).astype(np.uint8)
Image.fromarray(resMedian*60).save('resMedian.png')        # arbitrary scaling by 60 for contrast

# Get mode of each 3x3 block
resMode   = stats.mode(flatView, axis=2)[0].reshape((249,303)).astype(np.uint8)
Image.fromarray(resMode*60).save('resMode.png')            # arbitrary scaling by 60 for contrast

Here is the result of the median filter:

enter image description here

And here is the result of the "mode" filter which is indeed better IMHO:

enter image description here

Here is animated comparison:

enter image description here

If anyone wants to take the code and adapt it to try new ideas, please feel free!

Related