8

I am working on a project where I want to take a picture of a colored grid as an input (made with Lego bricks in this example) and return a much smaller modified picture.

Here is an example input:

Input

Below is a very small 8x8 image that would be the outcome:

Output

Here is a much larger version of the expected outcome::

Big Output

Here is my code so far: It only works with black and white images.

from PIL import Image
import re

black = [(110,110,110),(0,0,0)] #The highest value and the lowest RGB value for the color black

img = Image.open("input.jpg") #The input image
size = (8,8) #The dimensions of the output image

out = img.resize(size,resample=Image.LANCZOS) #Resize the image

for y in range(size[0]): #loop through every pixel
    for x in range(size[1]):

        if out.getpixel((x,y)) <= black[0] and out.getpixel((x,y)) >= black[1]: #check to see if the pixel is within the accepted black values

            out.putpixel((x,y), (0,0,0)) #Give the current pixel true color
        else:
            #otherwise make the pixel black
            out.putpixel((x,y), (255,255,255)) #Give the current pixel true color

"""Save the pixelated image"""
out.save("output.jpg")

And the output returned by my code:

Actual Output

My program works fine for black and white images, but I need help changing it to work with several colors (red, orange, yellow, light green, dark green, light blue, dark blue, purple, black and white).

Thanks in advance!

Josh B.
  • 414
  • 5
  • 14

3 Answers3

10

You're doing a few things wrong.

First of all, you should use PNG, not JPG for your output. JPG introduces so many artifacts, that small images like your output get completely degenerated.

Then, you should reduce your palette. It's much easier to work with input containing no noise.

First of all, boring initialization:

from PIL import Image
import operator
from collections import defaultdict
import re

input_path = 'input.jpg'
output_path = 'output.png'
size = (4,4)

Then we declare the palette - this should contain colors of all possible LEGO bricks. I sampled the values below from your image, but you can use black and white like you do in your code, or any colors you want as long as they're similar to colors in the source image:

palette = [
    (45,  50,  50),  #black
    (240, 68,  64),  #red
    (211, 223, 223), #white
    (160, 161, 67),  #green
    (233, 129, 76),  #orange
]
while len(palette) < 256:
    palette.append((0, 0, 0))

The code below will declare palette for PIL, since PIL needs flat array rather than array of tuples:

flat_palette = reduce(lambda a, b: a+b, palette)
assert len(flat_palette) == 768

Now we can declare an image that will hold the palette. We'll use it to reduce the colors from the original image later.

palette_img = Image.new('P', (1, 1), 0)
palette_img.putpalette(flat_palette)

Here we open the image and quantize it. We scale it to size eight times bigger than needed, since we're going to sample the average output later.

multiplier = 8
img = Image.open(input_path)
img = img.resize((size[0] * multiplier, size[1] * multiplier), Image.BICUBIC)
img = img.quantize(palette=palette_img) #reduce the palette

After this, our image looks like this:

quantized image

We need to convert it back to RGB so that we can sample pixels now:

img = img.convert('RGB')

Now we're going to construct our final image. To do this, we'll sample how many pixels of each palette color each square in the bigger image contains. Then we'll choose the color that occurs most often.

out = Image.new('RGB', size)
for x in range(size[0]):
    for y in range(size[1]):
        #sample at get average color in the corresponding square
        histogram = defaultdict(int)
        for x2 in range(x * multiplier, (x + 1) * multiplier):
            for y2 in range(y * multiplier, (y + 1) * multiplier):
                histogram[img.getpixel((x2,y2))] += 1
        color = max(histogram.iteritems(), key=operator.itemgetter(1))[0]
        out.putpixel((x, y), color)

Finally, we save the output:

out.save(output_path)

The result:

small image

Upscaled by 1600%:

big image

rr-
  • 14,303
  • 6
  • 45
  • 67
  • Hello, I just got around to using the code, and it's throwing me some errors that look like they are related to the version of python. I am using Python 3.4.2. What version was this code written in? – Josh B. May 30 '15 at 20:51
  • 1
    Python 3.4.3, but I use Pillow rather than PIL (it's a more active fork). There might be minimal API changes. – rr- May 30 '15 at 21:43
  • 2
    Thanks for the info. I'm glad to say that I got it all working now. – Josh B. May 31 '15 at 02:59
4

Just for fun, I tackled this with ImageMagick - which is also callable from Python...

First off, I create a little custom palette to match your colours - your white is not very white and your green is different from ImageMagick's idea of green so I used hex for them instead of colour names.

convert xc:black xc:red xc:"rgb(200,200,200)" xc:"rgb(168,228,23)"  xc:orange +append palette.png

If I scale that palette up, it looks like this:

enter image description here

Then I resize your image down to 4x4 and map the result to the custom palette and scale it back up so you can see it like this:

convert lego.jpg -resize 4x4! +dither -remap palette.png -scale 1600 result.png

and here is the result

enter image description here

The white is off to match the "white" in your original.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
0

Pixelator does the trick. It is a package based off or rr-s answer. It also has some added benefits for AI and ML applications.

In bash

pip install pixelator==0.1

in python

from pixelator import pixelator
palette = [
    (45,  50,  50),  #black
    (240, 68,  64),  #red
    (211, 223, 223), #white
    (160, 161, 67),  #green
    (233, 129, 76),  #orange
]

sensitivity_multiplier = 10

size = (4,4)

output=pixelator(
    in_path='./images/input.jpg',
    palette=palette,
    size=size,
    sensitivity_multiplier=sensitivity_multiplier
    )

output.resize_out_img().save_out_img(path='./images/output.jpg', overwrite=True)

Update:

Pixelator 1.0.0 is out and based off of open cv2...

In Bash:

pip install pixelator==1.0.0

In python:

from pixelator import Pixelator
# Use the input filename provided
image = Pixelator(filename='./images/input.jpg')
# Define a custom palette
palette = [
    [50,50,45],     #black
    [64,68,240],    #red
    [223,223,211],  #white
    [67,161,160],   #green
    [76,129,233],   #orange
]


# Pixelate the image to a 4x4 multi colored array
pixelated_image = image.pixelate(
    width=4,
    height=4,
    palette=palette
)
# Write to `output.png` scaled up to a 300x300 image (to be easily viewed)
pixelated_image.write(filename='./images/output_test_2.jpg', width=300, height=300)
conmak
  • 1,200
  • 10
  • 13