Day 12 - image

Relevancy: 1.9 stable

The image crate is a library under development (well, not unlike the rest of the Rust ecosystem before 1.0) to read, manipulate and write images. It is part of the effort to develop an open source game engine in pure Rust - Piston, but of course the image crate can be used on its own.

At the moment image supports reading and writing JPG, GIF and PNG images, while a few other formats are read-only (TIFF, WEBP).


Let's start from something simple - read a JPEG image, flip it horizontally and save as PNG.

extern crate image;

use std::fs::File;

let img = image::open("data/in.png").expect("Opening image failed");
    let filtered = img.fliph();
    let mut out = File::create("out.png").unwrap(); out, image::PNG).expect("Saving image failed");

We used the open() function to create the img variable (which is a DynamicImage value). This is a wrapper for the load() function, which can read images from anything that implements the Read trait. However, there's no symmetrical shortcut to write an image, so we take advantage of the fact that File implements Write. In between, the fliph() method of the image does what it says on the cover. There are other transformations available, such as:

  • fliph()
  • flipv()
  • rotateN() where N is 90, 180 or 270
  • blur(sigma)
  • invert()
  • grayscale()

and a few others. All these return a new image, except invert().

Edge detection

The image API lets us run arbitrary 3x3 convolution filters. We can use it to create a very basic edge detection filter.

let kernel = [-1.0f32, -1.0, -1.0,
                  -1.0, 8.0, -1.0,
                  -1.0, -1.0, -1.0];
    let filtered = img.filter3x3(&kernel);

To honour the image processing tradition, our input image is a photo of Lena Söderberg. Here's the result:

edgy Lena

Directly manipulating pixels

A typical example of looping over image pixels is to add some noise to the image. The noise does not depend on surrounding pixels, so the inner loop is very simple - generate a Gaussian noise sample and add it to the current pixel.

let (width, height) = img.dimensions();
    let mut rng = rand::thread_rng();
    let normal = Normal::new(15.0, 15.0);
    let mut noisy = img.brighten(-25);
    for x in 0..(width) {
        for y in 0..(height) {
            let offset = normal.ind_sample(&mut rng) as u8;
            let px = img.get_pixel(x, y).map(|v| if v <= 255 - offset { v + offset } else { 255 });
            noisy.put_pixel(x, y, px);

We need to use the GenericImage and Pixel traits to introduce some extra methods we're going to use later.

Each run of the inner loop generates a sample from the normal distribution. We then pick one pixel from the original image with get_pixel(), add the offset to every channel (that's what the map() method does for types implementing Pixel trait) and store the pixel in the new image.

noisy Lena


To create a thumbnail from the image, use it's resize() method. It takes three arguments: width and height of the thumbnail (but the original aspect ratio will be preserved, so one of these dimensions might be ignored) and a variant of the FilterType enum. This value dictates what interpolation to use when resizing. See for example the GIMP documentation to learn more about various methods. I personally like Lanczos interpolation, unless the result looks really bad.

let thumbnail = img.resize(120, 120, FilterType::Lanczos3);

One more thing - if your code using the image crate seems to be pretty slow, double check that you run in release mode (with compiler optimizations). For Cargo, that means cargo run --release. In my case the change from the default (no optimization) to release mode resulted in an 8-10x increase in speed.