Link Search Menu Expand Document

Script 08 - Images


Learning Objectives

With this script you

  • know how to work with images,
  • understand the concept of image animations, and
  • understand how to manipulate images.

Images

Now it’s finally time to exchange our beloved ellipses with images.

Loading and Displaying Images

The steps to display an image are as follows:

  • Add an image file to your project
  • Load that image in p5’s preload() function and save the image in a global variable
  • Display the image variable with the image() function call.

You can also follow these explanations in this tutorial in OpenProcessing.

Add An Image File To Your Project

Since we are working in the online editor of OpenProcessing, we need to upload our images before we can access them in our program. To do this we click at the three dots on the right, then on “Files” and then we choose our image via the “select” window or by directly dropping the file there.

image_upload

Next, we we create a variable to store the image in.

https://openprocessing.org/sketch/1256794

let imgPanda;

function setup() {
    createCanvas(600, 600);
    background(255);
}

function draw() {}

Load The Image File

As you already know the setup() function is called before our draw() loop starts. But Javascript is asynchronous, meaning that it allows multiple things to happen at the same time. So if we would load our image in setup(), it could happen that the image file is still not fully loaded before it’s needed in another line of code. Therefore it’s better to use another built-in function of p5: preload().

The loadImage() function loads an image from the given path. Because we uploaded our file to this sketch in OpenProcessing, we can just use the filename “panda.jpg”. We could also put in a URL to an image file here.

// https://openprocessing.org/sketch/1256794
// Display Image - STEP 4

let imgPanda;

function preload() {
    imgPanda = loadImage("panda.jpg");
}

function setup() {
    createCanvas(600, 600);
    background(255);
}

function draw() {
}

When we run our sketch, p5 calls preload() first, and then pauses execution until loadImage has finished loading our image file.

Display The Image

Now we need display the image in our draw() function with the image command:

image(img, x, y, [width], [height])

As input parameters we use:

  • Image variable: panda
  • Position on the x axis (upper-left corner): 50
  • Position on the y axis (upper-left corner): 100
  • Optional: width of the displayed image
  • Optional: height of the displayed image

Note that the reference point for positioning the images is the upper-left corner (for ellipses it is the center). If you want to use the image’s center as reference, you can enable that with the imageMode() function.

Since we leave out width and height, the image is displayed in its full dimensions.

// https://openprocessing.org/sketch/1256794
// Display Image - STEP 5

let imgPanda;

function preload() {
    imgPanda = loadImage("panda.jpg");
}

function setup() {
    createCanvas(600, 600);
    background(255);
}

function draw() {
    image(imgPanda, 50, 100);
}

Image Animation

The parameters of an images, e.g. its position coordinates, can be animated in the same way as regular shapes.

//https://openprocessing.org/sketch/1256818
// Animated Images STEP 3

// We need to know how many
// ciclres we want to create
let numCircles = 100;

let positionX = [];
let positionY = [];

let stepX = [];
let stepY = [];

let hue = [];
let sizeCircle = [];

let pullForce = 0.01;

let img;

function preload() {
    img = loadImage("panda_face.png");
}

function setup() {
    createCanvas(1000, 1000);

    colorMode(HSB, 360, 100, 100);
    background(0, 0, 100);
    fill(200, 100, 100);
    noStroke();

    // Initalization of the values
    for (let i = 0; i < numCircles; i++) {
        positionX[i] = random(width);
        positionY[i] = random(height);

        stepX[i] = random(-5, 5);
        stepY[i] = random(-5, 5);

        hue[i] = random(360);
        sizeCircle[i] = random(5, 100);
    }
}


function draw() {

    background(0, 0, 100);

    for (let i = 0; i < numCircles; i++) {
        if (positionX[i] < 0 || positionX[i] >= width) {
            stepX[i] *= -1;
        }

        if (positionY[i] < 0 || positionY[i] >= height) {
            stepY[i] *= -1;
        }

        positionX[i] += stepX[i];
        positionY[i] += stepY[i];

        if (mouseIsPressed) {
            // Make the ellipse be attracted
            // to the mouse

            // Get the difference between mouse and position
            let dx = mouseX - positionX[i];
            // Add that difference (scaled) to the position
            positionX[i] += dx * pullForce;

            let dy = mouseY - positionY[i];
            positionY[i] += dy * pullForce;
        }

        fill(hue[i], 100, 100);
        //ellipse(positionX[i], positionY[i], sizeCircle[i], sizeCircle[i]);
        image(img, positionX[i], positionY[i], sizeCircle[i], sizeCircle[i]);
    }
}

Did you notice that the images are now bouncing off earlier on the top and left border than the ellipses? And on the right and bottom they are even leaving the screen before bouncing back.

This is because of the different reference points of ellipses (center) and images (upper-left corner). We could now either change the part where we detect if the ellipses reached the sketch boundaries or use imageMode(CENTER) in setup() to change that behavior.

Spritesheet Animation

Another way of bringing images to live is to use a series of still images and display them in a fast sequence. This technique was “invented” in 1872 by Eadweard Muybridge. He was commissioned to prove whether a horse lifted all four legs off the ground at once when it ran. To do so, he set up a series of cameras along a track and took pictures in quick succession as a horse ran by. This process allowed him to capture 16 pictures of the horse’s run. In one of the pictures, the horse did indeed have all four legs off the ground.

ch06_08.png

Muybridge later repeated the experiment and placed each photo onto a device that could project the photos in rapid succession to give the illusion of the horse running, creating the first movie projector!

To rebuild Muybridge’s principle of animation in p5, we first of all need an image sequence. We are going to use here Muybridge’s original and divide it in separate files as frame-0.jpg, frame-1.jpg, frame-2.jpg, …, frame-14.jpg.

Instead of loading all files separately such as

// Spritesheet Animation - STEP 4
// https://openprocessing.org/sketch/1256845


let imgArray = []; // Image array

function preload() {
    // Store images in array
    imgArray.push(loadImage("frame-0.jpg"));
    imgArray.push(loadImage("frame-1.jpg"));
    imgArray.push(loadImage("frame-2.jpg"));
    imgArray.push(loadImage("frame-3.jpg"));
    ...
}

we can loop over all files based on their name. We can use the iterator i as a part of the images file name. Because our program doesn’t know how many images we have, we define numberImg ourself and use it as the iteration limit in the for loop.

// Spritesheet Animation - STEP 5
// https://openprocessing.org/sketch/1256845

let imgArray = []; // Image array
let numberImg = 15; // Number of images

function preload() {
    // Store images in array
    for (let i = 0; i < numberImg; i++) {
        imgArray.push(loadImage("frame-" + i + ".jpg"));
    }
}

We create a new variable to keep track of the image that is currently displayed. Then we can count up imgIndex, from 0 to numberImg and then back to 0.

// Spritesheet Animation - STEP 7
// https://openprocessing.org/sketch/1256845

let imgArray = []; // Image array
let numberImg = 15; // Number of images
let imgIndex = 0; // Index of the image currently displayed

function preload() {
    // Store images in array
    for (let i = 0; i < numberImg; i++) {
        imgArray.push(loadImage("frame-" + i + ".jpg"));
    }
}

function setup() {
    createCanvas(600, 600);
    background(255);
}

function draw() {
    // Display the image at current index
    image(imgArray[imgIndex], 0, 0);

    // We want to iterate the image for each draw call.
    // For that, we count up imgIndex
    
    imgIndex++; // Next image

    if (imgIndex == numberImg) { // Reached last image
        imgIndex = 0 // Back to first image
    }
}

To change the speed of the animation we need to control how often imgIndex is increased. A simple solution would to just lower the framerate to a lower value with the frameRate() command.

But most of the time this is not very useful, because now the whole program runs at 10 frames per second. What if we want to display something else in an other pace? We need a way to control how often imgIndex counts up.

The solution is to use the modulo operator, which returns for a division with a whole number the rest of that division, on the frameCount variable. frameCount contains the number of frames that have been displayed since the program started. So (frameCount % 5 == 0) is true when the current frame number is dividable by 5 without remainders, which means every 5th frame.

With that functionality we can increase imgIndex only every 5th frame.

// Spritesheet Animation - STEP 10
// https://openprocessing.org/sketch/1256845

let imgArray = []; // Image array
let numberImg = 15; // Number of images
let imgIndex = 0; // Index of the image currently displayed

function preload() {
    // Store images in array
    for (let i = 0; i < numberImg; i++) {
        imgArray.push(loadImage("frame-" + i + ".jpg"));
    }
}

function setup() {
    createCanvas(600, 600);
    background(255);

}

function draw() {
    // Display the image at current index
    image(imgArray[imgIndex], 0, 0);

    // We want to iterate the image for each draw call.
    // For that, we count up imgIndex
    
    if (frameCount % 5 == 0) { // Every 5th frame
        imgIndex++; // Next image

        if (imgIndex == numberImg) { // Reached last image
            imgIndex = 0 // Back to first image
        }
    }
}

Ideally replace the modulo number with the a global variable, e.g., animationSlowDown to have easy access for changing the speed.

You can also follow these explanations in this tutorial in OpenProcessing.

Modifying the Size

To resize an image to a new width w and height h, use resize(w, h).

To make the image scale proportionally, use 0 as the value for the width or height parameter.

// Resize Image
// https://openprocessing.org/sketch/1256862

let img;
img = loadImage('panda.jpg');

img.resize(200, 0); // Scales the image to a width of 200px, keeping its original proportions

Modifying the Image Data

Images can be tinted to specified colors or made transparent by using the tint() command.

tint(v1, v2, v3, [alpha]);

Depending on the color mode (RGB or HSB) v1, v2 and v3 are values for red, green and blue or hue, saturation, brightness.

tint() can also be used for making the image transparent. To apply transparency to an image without affecting its color, use white as the tint color and specify an alpha value, from no transparency 0 to full transparency 255 in the default alpha range.

tint(255, 128); // The image is 50% transparent

Use noTint() to remove the current fill value for displaying images and revert to displaying images with their original hues.

// https://openprocessing.org/sketch/1256866
// Tint Image

let panda;

function preload() {
    panda = loadImage("panda.jpg");
}

function setup() {
    createCanvas(600, 600);
}

function draw() {
    image(panda, 0, 0);

    tint(0, 150, 200, 128);

    image(panda, 100, 100);

    noTint();

    image(panda, 200, 200);
}

Reading Pixel Data

get(x, y) returns the color of the image at the specific pixel at the position x, y.

With the optional parameters w and h you can return a cutout of the image.

get(x, y, w, h);

Be careful: You have to consider a possible offset of the image on the canvas, because get() always relates to positions within the image.

Using the mouseX and mouseY positions as the parameters for getting the color values we can make this interactive. Because the position parameter of get() is relative to the image coordinates and not the sketch itself, we specifically need to consider the offset.

// https://openprocessing.org/sketch/1256869
// Reading Pixel Data - Step 5

let img;

let offsetX = 50;
let offsetY = 10;

function preload() {
    img = loadImage("kitty.jpg");
}

function setup() {
    createCanvas(600, 475);
    background(255);
}

function draw() {
    image(img, offsetX, offsetY);

    let pixelColor = img.get(mouseX - offsetX, mouseY - offsetY);

    stroke(0);
    fill(pixelColor);
    rect(50, 360, 500, 100);
}

Setting Pixel Data

set(x, y, color) sets the color of the pixel with the coordinate x, y.

Before we are able to set pixel data, we need to load the pixel data with loadPixels(). After manipulating update the images pixel data with updatePixels() to see the effect.

let img;
img = loadImage('panda.jpg');

let pixelColor = color(255, 0, 0); // Define pixelColor as a red color value

img.loadPixels(); // Load the pixel data of the image
img.set(100, 200, pixelColor); // Set the pixel at coordinate 100, 200 to pixelColor
img.updatePixels(); // Update the pixel data of the image

Exactly like get(), set() also relates to the position within the image, so remember to consider a possible offset of the image on the canvas when for example also using the mouse position.

// https://openprocessing.org/sketch/1256883
//Setting Pixel Data - STEP 5
let img;

let offsetX = 50;
let offsetY = 10;

function preload() {
    img = loadImage("nemo.jpg");
}

function setup() {
    createCanvas(600, 475);
    background(255);
}

function draw() {
    image(img, offsetX, offsetY);

    let pixelColor = color(255, 0, 0);

    img.loadPixels();
    img.set(mouseX - offsetX, mouseY - offsetY, pixelColor);
    img.updatePixels();
}

Examples

Pointillism

pointillism.png

It is actually quite easy to create a fake pointillism style in p5:

pointillism.png

Any ideas on how to do this?

What do we see?

  • Single pixel are represented as circles
  • Circles are placed on top of each other

Steps

  • Pick a random point
  • Look up the RGB color in the source image
  • Draw a circle at the pixel’s position with the pixel’s color

We use the get() function to get the color value of the image at the random coordinates x, y and store them that value. Then we define a brushSize and use it as the size of the ellipses, which we draw at the coordinates x, y.

To make our ellipses transparent, we need to extract the red, green and blue values of our color c and define our own alpha value of 100.

// https://openprocessing.org/sketch/1256887
// Pointillism

let img;

let brushSize = 16;

function preload() {
    img = loadImage("lake.png");
}

function setup() {
    createCanvas(800, 425);
    background(255);
    noStroke();
}

function draw() {

    let x = random(img.width);
    let y = random(img.height);

    let c = img.get(x, y);

    fill(red(c), green(c), blue(c), 100);
    ellipse(x, y, brushSize, brushSize);


}

Stretching

Again, what do we see?

pingu_stretch_01

What we see

  • The pixel colors on the vertical line at mouseX should extend as horizontal lines up to the left edge.

Steps

  • Iterate through the column where the mouse is at
    • As the image has no offset this is just mouseX
  • Detect the pixel’s color with get()
  • Draw a line from x = 0 to the column (mouseX) in the detected color
// Stretching - STEP 3
// https://openprocessing.org/sketch/1257072


let img;

function preload() {
    img = loadImage("penguin.jpg");
}

function setup() {
    createCanvas(500, 750);

    // For some reason this is needed
    // to prevent transparency
    strokeWeight(2);
}

function draw() {

    image(img, 0, 0);


    // Go through all lines of the image from top to bottom
    for (let i = 0; i < img.height; i++) {

        let c = img.get(mouseX, i);

        // To define a line color
        // stroke() is used
        stroke(c);

        // Draw a line from the
        // left edge of the sketch window
        // up to the mouse x position
        line(0, i, mouseX, i);
    }

}

Tiling

tiling.jpg

We can use the get() command to create a new image variable, that holds just a part (rectangle region) of our source image.

let imgPart = img.get(x, y, w, h);

  • x, y: coordinates of imgPart relative to img
  • w: width of imgPart
  • h: height of imgPart

Using mouseX and mouseY we can interactively choose the displayed image region.

// https://openprocessing.org/sketch/1257098
// Tiling - STEP 3

function draw() {

    let imgPart = img.get(mouseX, mouseY, 100, 100);

    // Draw the cut out square
    image(imgPart, 0, 0);

}

Now, we will tile that image region on a grid with a 2D loop:

// https://openprocessing.org/sketch/1257098
// Tiling - STEP 5
let img;

// Number of rows and columns
// for the grid
let numRows = 5;
let numCols = 5;

// Will be computed in reference to 
// window width and height
let gridStepX;
let gridStepY;

function preload() {
    img = loadImage("pugs.jpg");

}

function setup() {
    createCanvas(408, 408);
    background(255);

    // Compute the size of a tile
    gridStepX = width / numRows;
    gridStepY = height / numCols;

}

function draw() {

    let imgPart = img.get(mouseX, mouseY, 100, 100);

    // Draw the cut out square
    image(imgPart, 0, 0);

    // "Walk" with the step size in x
    for (let x = 0; x < width; x += gridStepX) {
        // "Walk" with the step size in y
        for (let y = 0; y < width; y += gridStepY) {

            // Draw the cut out square
            image(imgPart, x, y);
        }
    }

}

We are having an issue with mouse positions as the right and bottom border of the image for coordinates for which we can not cut out a whole tile anymore. I leave this to the interested reader to find a solution 😁

Brownian Motion Lines

Look at the following effect. Can you understand its underlying logic?

fishies brownian_01

The idea is to draw with each draw() iteration a new line of a certain length with starts at the end of the previously drawn line and ends at a random point. For that we need to define some variables:

  • range is the maximum range from one point to the next. In the end it defines the length of our lines.
  • lastX and lastY are the variables that store the coordinates where the last line ended, thus where the new line begins.

With each call of our draw loop we draw one new line. Therefore we always need to define new coordinates where our current line should end: nextX and nextY. It is based on a random value between -range and range. After each draw call we need to set our lastX and lastY coordinates to nextX and nextY to start the next line where we ended the last.

// Brownian Motion Lines - STEP 4
// https://openprocessing.org/sketch/1256917

function draw() {
    //image(img, 0, 0);

    // Compute new end value
    let nextX = lastX + random(-range, range);
    let nextY = lastY + random(-range, range);

    line(lastX, lastY, nextX, nextY);

    lastX = nextX;
    lastY = nextY;

}

Now we need to draw the lines in the color of the pixels at the randomly generated coordinate. For this we use the get() command and store the pixel value as a new variable pix, which we then use as the stroke color.

// Brownian Motion Lines - STEP 5
// https://openprocessing.org/sketch/1256917

function draw() {

    // Compute new end value
    let nextX = lastX + random(-range, range);
    let nextY = lastY + random(-range, range);

    let pix = img.get(nextX, nextY);
    stroke(pix);
    line(lastX, lastY, nextX, nextY);

    lastX = nextX;
    lastY = nextY;
}

To prevent our lines to the leave the sketch we need to use the constrain() command, which constrains a value between a minimum and maximum value. As minimum and maximum we set the borders of our sketch, 0 and width or height.

// Brownian Motion Lines - STEP 6
// https://openprocessing.org/sketch/1256917

function draw() {
    //image(img, 0, 0);

    // Compute new end value
    let nextX = lastX + random(-range, range);
    let nextY = lastY + random(-range, range);

    // Constrain all points to borders of the sketch
    nextX = constrain(nextX, 0, width);
    nextY = constrain(nextY, 0, height);

    let pix = img.get(nextX, nextY);
    stroke(pix);
    line(lastX, lastY, nextX, nextY);

    lastX = nextX;
    lastY = nextY;
}

Summary

  • Use the preload() function to make sure your image files are fully loaded before working with them
let img;

function preload() {
    img = loadImage("myImage.jpg");
}
  • By default images have their upper-left corner as reference point (imageMode(CORNER)), which you can change by calling
imageMode(CENTER);
  • Animate images e.g. by changing their position like any other shape
  • Store images in arrays and display them sequentially to animate image series

  • Use get(x, y) and set(x, y, color) to return or set the color of the image at a specific pixel

Use the reference 🚒


The End

🏇 📷 🖼️