Search code examples
pythonimagerequestphotomosaic

I need to make a "mosaic" - but very simple


The best code for mosaic I've found you can see at this page: https://github.com/codebox/mosaic

However, the code doesn't work well on my Windows computer, and also I think the code is too advanced for what it should do. Here are my requirements I've posted on reddit:

1) The main photo already has reduced number of colors (8)

2) I have already every image associated with colour needed to be replaced (e.g. number 1 is supposed to replace black pixels, number 2 replaces green pixels...)

3) I need to enlarge the photo by the small photo's size (9 x 9 small photos will produce 81 times bigger image), which should push the pixels "2n" points away from each other, but instead of producing a n x n same-coloured area around every single one of them (this is how I believe enlarging works in general, correct me if I'm wrong), it will just colour the white spaces with unrecognized colour, which is not associated with any small photo (let's call that colour C)

4) Now all it needs is to run through all non-C coloured pixels and put an image centered on that pixel, which would create the mosaic.


Since I'm pretty new to Python (esp. graphics) and need it just for one use, could someone help me with creating that code? I think that code I got inspired with is too complicated. Two things I don't need:

1) "approximation" - if the enlargement is lesser than needed for 100% quality (e.g. the pictures are 9x9, but every side of the original photo can be only 3 times larger, then the program needs to merge some pixels of different colours together, leading to quality loss)

2) association colour - picture: my palette of pictures is small and of colours as well, I can do it manually

For the ones who didn't get what I mean, here is my idea: https://ibb.co/9GNhqBx


Solution

  • I had a quick go using pyvips:

    #!/usr/bin/python3
    
    import sys
    import os
    import pyvips
    
    if len(sys.argv) != 4:
        print("usage: tile-directory input-image output-image")
        sys.exit(1)
    
    # the size of each tile ... 16x16 for us
    tile_size = 16
    
    # load all the tile images, forcing them to the tile size
    print(f"loading tiles from {sys.argv[1]} ...")
    for root, dirs, files in os.walk(sys.argv[1]):
        tiles = [pyvips.Image.thumbnail(os.path.join(root, name), tile_size, 
                                        height=tile_size, size="force") 
                 for name in files]
    
    # drop any alpha
    tiles = [image.flatten() if image.hasalpha() else image
             for image in tiles]
    
    # copy the tiles to memory, since we'll be using them many times
    tiles = [image.copy_memory() for image in tiles]
    
    # calculate the average rgb for an image, eg. image -> [12, 13, 128]
    def avg_rgb(image):
        m = image.stats()
        return [m(4,i)[0] for i in range(1,4)]
    
    # find the avg rgb for each tile
    tile_colours = [avg_rgb(image) for image in tiles]
    
    # load the main image ... we can do this in streaming mode, since we only 
    # make a single pass over the image
    main = pyvips.Image.new_from_file(sys.argv[2], access="sequential")
    
    # find the abs of an image, treating each pixel as a vector
    def pyth(image):
        return sum([band ** 2 for band in image.bandsplit()]) ** 0.5
    
    # calculate a distance map from the main image to each tile colour
    distance = [pyth(main - colour) for colour in tile_colours]
    
    # make a distance index -- hide the tile index in the bottom 16 bits of the
    # distance measure
    index = [(distance[i] << 16) + i for i in range(len(distance))]
    
    # find the minimum distance for each pixel and mask out the bottom 16 bits to
    # get the tile index for each pixel
    index = index[0].bandrank(index[1:], index=0) & 0xffff
    
    # replicate each tile image to make a set of layers, and zoom the index to
    # make an index matching the output size
    layers = [tile.replicate(main.width, main.height) for tile in tiles]
    index = index.zoom(tile_size, tile_size)
    
    # now for each layer, select pixels matching the index
    final = pyvips.Image.black(main.width * tile_size, main.height * tile_size)
    for i in range(len(layers)):
        final = (index == i).ifthenelse(layers[i], final)
    
    print(f"writing {sys.argv[3]} ...")
    final.write_to_file(sys.argv[3])
    

    I hope it's easy to read. I can run it like this:

    $ ./mosaic3.py smallpic/ mainpic/Use\ this.jpg x.png
    loading tiles from smallpic/ ...
    writing x.png ...
    $
    

    It takes about 5s on this 2015 laptop and makes this image:

    enter image description here

    I had to shrink it for upload, but here's a detail (bottom left of the first H):

    enter image description here

    Here's a google drive link to the mosaic, perhaps it'll work: https://drive.google.com/file/d/1J3ofrLUhkuvALKN1xamWqfW4sUksIKQl/view?usp=sharing

    And here's this code on github: https://github.com/jcupitt/mosaic