11 Jan 2023

Python Playground Chapter 7: Photomosaics

In October 2022, I started reading Python Playground and wrote an article to mark the day I started.

As of today, I finished reading and coding for the seventh chapter of Python Playground. This chapter was particularly fun to work with because I ended up creating a gift for my parents’ anniversary using its code.

A photomosaic is essentially a photo made up of photos. A large number of photos are arranged in tiles to form the target photo. The photos are arranged by matching the average color of the photo at that region and the average color of the tile photo.

Matching the Tile colors with the Target

If you imagine a point P (in red) on a 3D grid where the axes represent the colors Red(x), Green(y) and Blue(z), then finding how close another color is to P is done by measuring the Euclidean distance between the two points.

$d(P, Q) = \sqrt{(x2-x1)^{2}+(y2-y1)^{2}+(z2-z1)^{2}}$

The values of P and Q represent the average color of the tile, which is found by dividing the sum of the pixels by the number of pixels in the tile.

def getAvRGB(img):
    im = np.array(img)
    w, h, d = im.shape
    return tuple(np.average(im.reshape(w*h, d), axis=0))

The target image is first divided into a number of tiles based on the user input. For the images in this article, I’ve used a grid size of 128x128 pixels. The average color of each tile is found and then matched with the average color of the images being used to create the mosaic using the above formula.

def get_best_match(input_avg, avgs):
    avg = input_avg
    index = 0
    min_index = 0
    min_dist = float("inf")
    for val in avgs:
        dist = ((val[0] - avg[0])*(val[0] - avg[0]) +
                (val[1] - avg[1])*(val[1] - avg[1]) +
                (val[2] - avg[2])*(val[2] - avg[2]))

        if dist < min_dist:
            min_dist = dist
            min_index = index

        index += 1

    return min_index

The mosaic is then created by pasting the image into the tile using PIL. The reuse condition will allow images to be reused multiple times throughout the mosaic.

def create_mosaic(target, input_images, grid_size, reuse=False):

    targets = splitImg(target, grid_size)

    output = []
    count = 0
    batch_size = int(len(targets)/10)

    avgs = []
    for img in input_images:
        avgs.append(getAvRGB(img))

        print("processed %d of %d..." % (count, len(targets)))

        count += 1

    count = 0

    for img in targets:
        avg = getAvRGB(img)

        match_index = get_best_match(avg, avgs)
        output.append(input_images[match_index])

        if count > 0 and batch_size > 0 and count % batch_size == 0:
            print("processed %d of %d..." % (count, len(targets)))

        count += 1
        if not reuse:
            input_images.remove(match_index)

    print('creating mosaic...')
    mosaic = create_grid(output, grid_size)
    return mosaic

The Result

Using a set of over 800 images taken over the last four years, I was able to create these:

Me testing out the Canon E06 Camera

A photo of my dad made of photos of my dad

That’s all I have for now. Live long and prosper.

References

Python Playground

Electronut Labs

My Code For This Chapter

Koch Snowflake

3D Plotter used earlier