DEV Community

Cover image for Photo mosaics with Rust
Will Dady
Will Dady

Posted on • Originally published at willdady.com

Photo mosaics with Rust

When I was a kid I remember seeing a poster of Yoda from Star Wars which was made up from various screenshots from the movie. I remember being facinated how several tiny images could be arranged to create a larger image and that how the colour of each small image contributed to the larger picture overall.

The idea of arranging small coloured tiles to create images has been around since 300 BC and artwork created in such a way is called a Mosaic which Wikipedia describes as:

a piece of art or image made from the assembling of small pieces of colored glass, stone, or other materials.

While the technique of arranging coloured tiles to form an image has been around for hundreds of years, photo mosaics are a relatively new take on the art style.

Photo mosaics

The first mainstream use of a photo mosaic I recall was on one of the official posters for the 1998 film staring Jim Carrey, The Truman Show.

A poster for The Truman Show

A photo mosaic poster for The Truman Show (1998)

You can see in the following close-up of the poster, specifically Jim Carrey's right eye, the image is made up of various scenes from the film. Interestingly each tile is tinted rather than solely relying on the actualy colour in the frame. It's a forgivable workaround considering there probably isn't that many fleshy-beige coloured scenes in the movie to sample from.

A close-up of the poster for The Truman Show (1998)

A close-up of the poster for The Truman Show (1998)

Writing a photo-mosaic generator

As I've been learning the Rust programming language lately I thought creating a photo-mosaic generator could be a fun project. Rust is a relatively new systems programming language akin to C/C++ in terms of performance. What makes Rust unique is the way it manages memory compared to other low-level languages.

I knew a generator like this would require a large set of tile images to sample from. The original plan was for it to only create mosaics from emoji which is how it got it's name emosaic (emoji + mosaic = emosaic) but as I progressed it made more sense to keep in generic and let the user provide their own pool of tile images.

Similar to my previous (and first) Rust project swirlr, I used dependencies from crates.io, Rust's equivalent to NPM for Node.js. The main libraries I used are image, for loading an manipulating images and sampling pixels, and clap a command line argument parser.

Emosaic works by taking a directory of images used as tiles and a source image.

emosaic /path/to/tiles/ source.png

The output file will be saved to the current directory as output.png though a custom output path can be provided via the -o option. Output is always in PNG format.

The app works by first reading all images in the tiles directory. Each image's pixels are looped over to find the average colour of each image. I found it necessary to exclude any tiles where more than 50% of it's pixels are completely transparent as these didn't look good in the final output. The image's path and average colour is then added to a Tile struct. Each Tile struct was then added to a TileSet struct which represented all available tiles to choose from in the next step.

Next the source image is loaded and for each pixel in the source image we call the TileSet's closest_tile(&self, rgba: Rgba<u8>) -> Tile method. This method works by naively looping over each tile and comparing the distance between colours. We say "distance" as we calculate how close one colour is to another by treating each as a point in 3D space RGB -> XYZ (alpha is ignored). The resulting Tile is then added to a Map keyed by the pixel colour that way subsequent lookups can simply check if the Tile for a given colour has already been found, this is a particularly useful optimization as lossy formats like JPEG will repeat colours often.

Once the tile is found it's copied to the same row/column coordinates in the output image factoring in the desired tile size. The tile size can be configured as the optional command line option -t which defaults to 16. For example a 100x100 source image yields an output image of 1600x1600 (100 x 16 = 1600) so it's important the source image is small.

Further improvements

Im pretty happy with how this turned out and it's super fast thanks to the low-level nature of Rust. For example, the Marilyn Monroe image above is generated from a directory of 2625 tiles, each with dimensions of 64x64 and a source image with with dimensions of 100x100. The command completes in about 2.9 seconds on a 2017 Macbook Pro.

100x100 source image

100x100 source image

Some further optimisations could be made such as caching the TileSet to disk that way the program doesn't need to read potentially thousands of tile images every time the app is run. Also at the time of writing it does not provided an option to 'tint' the output image like the Truman Show example above.

Overall I'm really liking Rust. It has a very steep learning curve but it's rewarding once the borrow checker stops complaining. It's nice to feel confident that if the app compiles it will run safely. I find small projects like this are a great way to get familiar with a new programming language.

Feel free to checkout the complete source code for emosaic on github.

GitHub logo willdady / emosaic

Mosaic image generator written in Rust!

emosaic

Mosaic generator written in Rust!

Building

To build make sure you have rust installed.

cargo build --release

Once compiled, the binary can be found at target/release/emosaic in the repository root.

Usage

The command expects a path to a directory containing square 'tile' images and a source image.

emosaic /path/to/tile/images/ source.png

Modes

The strategy used to generate the mosaic is controlled by the -m, --mode option.

1to1 (Default)

For each pixel in the source image a tile with the nearest matching average color will be emitted.

Assuming a source image with dimensions 100x100 and default tile size of 16 the output image will be 1600x1600.

4to1

For every 2x2 pixels one tile will be emitted. Tiles are divided into 2x2 segments and the average colour of each segment is stored. The tile with the nearest average color in each segment to the target pixels will be chosen. This mode…

Discussion (0)