Programming Digital Media

Pixels

The core Python language doesn't provide any way of making digital images. You can extend Python with modules that contain a related group of software components. Python provides a large number of modules already. One of the skills you develop as a Python programmer is a familiarity with various modules and the kinds of capabilities they provide.

Writing pixel-based scripts

I have created a simple Python module called "Image" that we'll use to begin our work with digital images. After a description of the Image module there are a set of example scripts you can download. Depending upon whether you learn from example or by description, you should take a look at the scripts first to see how much of their structure makes sense.

Loading the Image module

To load the Image module for use in the script, you use the import command.

    import Image 
This makes the components of the Image module available in your script by using the module name as a prefix, separated from the component with a period character (.). The Image class in the Image module is therefore called Image.Image — the symbol "Image" is used for both the name of the module and the name of the class.

You can also explicitly specify the components of a module that you want to load. To load the Image class and the function fit from the Image module, you can write:

    from Image import Image, fit 
By loading the specific symbols (and not the entire module) you can refer to them without the module name as prefix. The circle.py script is the first script in the examples below to use this.

Making an instance of an Image

When you create an instance of a class, you use the class name as if it were a function. You make an instance of the Image class in two different ways by using two different types of arguments. In the first way, you create an image from scratch by specifying the image size when you create it. The arguments are the image's width, height, and the number of channels. For example, to create an image that is 400 pixels wide, 300 pixels high, with 3 channels (for red, green and blue), you would write:

    p = Image(400, 300, 3) 
The variable p is now an instance of the Image class. For example, black.py creates an Image instance that is 200 pixels square.

In the second way of creating an Image instance, you can acquire the pixel data from an image by using its filename as the argument when you create the instance:

    p = Image('mantis.tif') 
The size of the image instance is determined by the pixel size of the image in the file that you specified. For example, the script negate.py creates a negative image from the input image file.

Saving an Image instance to a file

To save an image to a file, you use the save method. For example, to save an image to a file called "black.tif", you would use the filename as the argument to save.

    p.save('black.tif') 
The script black.py is the simplest image script we can write — it just defines a new image and writes it out. By default, the pixel values are all 0, that is, black.

Creating an Image with an initial color

We can also create a new image instance with an initial color by specifiying the channel colors when we create it:

    p = Image.Image(200, 200, 3, .4, .5, .6) 
We create an image in this way in the blue.py script.

Setting pixel values

To set a pixel value, you use the put function that is part of the Image class. Functions that are part of a class are called methods and are separated from the instance variable with a period (.) character. For example, for the instance p the put method is called:

    p.put 
You specify the x and y positions in the image as integers beginning with 0 in the lower left corner. The red, green and blue color primary values, called the image's channels, are all specified as a fraction from 0.0 to 1.0.

For example,

    p.put(100, 50, .2, .3, .7) 
puts a red value of .2, a green value of .3 and a blue value of .7 at the pixel that is 101 from the left and 51 from the bottom. (Remember that the first pixel in the lower left corner is 0,0.)

If you want to assign alpha values, you need to create a four-channel image:

    p = Image(200, 200, 4) 

You can also put the channel values into a pixel separately. For example, the following is the same as the previous p.put command:

    p.put_r(100, 50, .2) 
    p.put_g(100, 50, .3) 
    p.put_b(100, 50, .7) 
The half.py script uses put_b to change the blue value by comparing the x value with the half the width of the image.

Getting the channel values of a pixel

You can assign the color channel values to variables using the get method. It returns the red, green and blue values as a list, so you can assign them to three variables in one statement:
    red, green, blue = p.get(100, 50) 
Just like putting the channel values individually, you can get the channel values separately, too:
    red   = p.get_r(100, 50) 
    green = p.get_g(100, 50) 
    blue  = p.get_b(100, 50) 
If the image has an alpha channel, you can use the geta method to get four channels:
    red, green, blue, alpha = p.geta(100, 50) 
The get_a method will get just the alpha value like get_r, get_g, and get_b.

Processing all the pixels of an Image

For many image operations, we want to process all the pixels of the image. The size of the image is available in three instance variables that you reference with the period character just like method functions. For an instance p, the width is p.width, the height is p.height, and the number of channels is p.depth.

We can use the range Python function to create a list of integers. With a nested loop for all the combinations of x values and y values we can specify all the pixel coordinates for the image.

In the example scripts below, you will always see this structure which loops through all the x,y positions in the image.

    # The variable "p" is an Image instance 
    for x in range(p.width): 
        for y in range(p.height): 
            # Use x and y here. 

Installing the software and images

To install the module, you need to copy two files to the image directory we created.

Files to download to your image directory:

    Image.py
    _image.so

For all of the Python scripts below, you can click on the script name or the image to download it. Depending upon your browser, you may need to move it from the directory to which it was downloaded or redefine the download destination. For example, Safari defines the directory to which files are downloaded in its "Preferences" menu. For this tutorial, you could change your download directory to ~/pdm/image.

For the scripts that modify existing images, you will also need to download those image files from these links:

    mantis.tif
    frog.tif
    frogmatte.tif

Some of the scripts that modify images use images that are produced by previous scripts, so you'll need to run those first to make the images.

Once you've downloaded the two Python module files and the scripts, you can run a script by using its filename as an argument to Python:

    python color.py 
Try changing the various numeric values in the scripts to see the result. Change the name of the filename in the save method. For the scripts that modify existing images, use these new images by changing the name of the input image.

black.py



import Image 
 
p = Image.Image(200, 200, 3) 
p.save('black.tif') 

blue.py



import Image 
 
p = Image.Image(200, 200, 3, .4, .5, .6) 
p.save('blue.tif') 

half.py



import Image 
 
p = Image.Image(200, 200, 3, .4, .5, .6) 
 
midpoint = p.width / 2 
 
for y in range(p.height): 
    for x in range(p.width): 
        if x < midpoint: 
            p.put_b(x, y, .5) 
        else: 
            p.put_b(x, y, .7) 
 
p.save('half.tif') 

square.py



import Image 
 
width = 200 
height = 200 
square_x = 80 
square_y = 130 
square_size = 80 
 
radius = square_size / 2 
xmin = square_x - radius 
ymin = square_y - radius 
xmax = square_x + radius 
ymax = square_y + radius 
 
bg_r, bg_g, bg_b = .4, .5, .6 
sq_r, sq_g, sq_b = .7, .6, .5 
 
p = Image.Image(width, height, 3) 
 
for y in range(height): 
    for x in range(width): 
        if x > xmin and x < xmax and \ 
           y > ymin and y < ymax: 
            p.put(x, y, sq_r, sq_g, sq_b) 
        else: 
            p.put(x, y, bg_r, bg_g, bg_b) 
 
p.save('square.tif') 

circle.py



from Image import Image, dist 
 
width = 200 
height = 200 
circle_x = 110 
circle_y = 90 
radius = 80 
 
bg_r, bg_g, bg_b = .4, .5, .6 
c_r, c_g, c_b = .9, .6, .3 
 
p = Image(width, height, 3) 
 
for y in range(height): 
    for x in range(width): 
        distance_to_center = dist(x, y, circle_x, circle_y) 
        if distance_to_center < radius: 
            p.put(x, y, c_r, c_g, c_b) 
        else: 
            p.put(x, y, bg_r, bg_g, bg_b) 
 
p.save('circle.tif') 

ramp.py



from Image import Image, fit 
 
width = 200 
height = 200 
 
p = Image(width, height, 3) 
 
for y in range(height): 
    for x in range(width): 
        r = fit(x, 0, width, 0, 1) 
        g = fit(y, 0, height, 0, 1) 
        b = fit(x, 0, width, 0, 5) % 1 
        p.put(x, y, r, g, b) 
 
p.save('ramp.tif') 

checkerboard.py



from Image import Image 
 
width = 200 
height = 200 
size = 20 
 
p_r, p_g, p_b = 1, 0, 0 
q_r, q_g, q_b = 1, 1, 0 
 
p = Image(width, height, 3) 
 
for y in range(height): 
    for x in range(width): 
        if ((x / size + y / size) % 2) == 0: 
            p.put(x, y, p_r, p_g, p_b) 
        else: 
            p.put(x, y, q_r, q_g, q_b) 
 
p.save('checkerboard.tif') 

random.py



from Image import Image 
from whrandom import random 
 
width = 200 
height = 200 
 
p = Image(width, height, 3) 
 
red = random() 
green = random() 
blue = random() 
 
for y in range(height): 
    for x in range(width): 
        if random() < .01: 
            red = random() 
        if random() < .01: 
            green = random() 
        if random() < .01: 
            blue = random() 
        p.put(x,y, red, green, blue) 
 
p.save('random.tif') 

luminance.py



from Image import Image 
 
p = Image('mantis.tif') 
 
r_part = .21 
g_part = .72 
b_part = .07 
 
for y in range(p.height): 
    for x in range(p.width): 
        red, green, blue = p.get(x, y) 
        luminance = red * r_part + green * g_part + blue * b_part 
        p.put(x, y, luminance, luminance, luminance) 
 
p.save('luminance.tif') 

negate.py



from Image import Image 
 
p = Image('mantis.tif') 
 
for y in range(p.height): 
    for x in range(p.width): 
        red, green, blue = p.get(x, y) 
        red = 1.0 - red 
        green = 1.0 - green 
        blue = 1.0 - blue 
        p.put(x, y, red, green, blue) 
 
p.save('negate.tif') 

multiply.py



from Image import Image 
 
red_factor = .4 
green_factor = .5 
blue_factor = 1.1 
 
p = Image('mantis.tif') 
 
for y in range(p.height): 
    for x in range(p.width): 
        red, green, blue = p.get(x, y) 
        red = red * red_factor 
        green = green * green_factor 
        blue = blue * blue_factor 
        p.put(x, y, red, green, blue) 
 
p.save('multiply.tif') 

contrast.py



from Image import Image, fit 
 
p = Image('mantis.tif') 
 
new_red_min = .3 
new_red_max = .7 
new_green_min = .4 
new_green_max = .9 
new_blue_min = .4 
new_blue_max = .8 
 
for y in range(p.height): 
    for x in range(p.width): 
        red, green, blue = p.get(x, y) 
        red = fit(red, new_red_min, new_red_max, 0, 1) 
        green = fit(green, new_green_min, new_green_max, 0, 1) 
        blue = fit(blue, new_blue_min, new_blue_max, 0, 1) 
        p.put(x, y, red, green, blue) 
 
p.save('contrast.tif') 

scatter.py



from Image import Image, clamp 
from whrandom import randrange 
 
p = Image('mantis.tif') 
q = Image(p.width, p.height, p.depth) 
 
x_variation = 10 
y_variation = 5 
 
for y in range(p.height): 
    for x in range(p.width): 
        x_offset = randrange(-x_variation, x_variation) 
        old_x = int(clamp(x + x_offset, 0, p.width - 1)) 
        y_offset = randrange(-y_variation, y_variation) 
        old_y = int(clamp(y + y_offset, 0, p.height - 1)) 
        red, green, blue = p.get(old_x, old_y) 
        q.put(x, y, red, green, blue) 
 
q.save('scatter.tif') 

resize.py



from Image import Image, fit 
 
p = Image('mantis.tif') 
q = Image(400, 100, 3) 
 
for y in range(q.height): 
    for x in range(q.width): 
        old_x = int(fit(x, 0, q.width - 1, 0, p.width - 1)) 
        old_y = int(fit(y, 0, q.height - 1, 0, p.height - 1)) 
        red, green, blue = p.get(old_x, old_y) 
        q.put(x, y, red, green, blue) 
 
q.save('resize.tif') 

imultiply.py



from Image import Image 
 
p = Image('checkerboard.tif') 
q = Image('circle.tif') 
r = Image(p.width, p.height, p.depth) 
 
for y in range(p.height): 
    for x in range(p.width): 
        r1, g1, b1 = p.get(x, y) 
        r2, g2, b2 = q.get(x, y) 
        red = r1 * r2 
        green = g1 * g2 
        blue = b1 * b2 
        r.put(x, y, red, green, blue) 
 
r.save('imultiply.tif') 

combine.py



from Image import Image 
 
p = Image('circle.tif') 
q = Image('random.tif') 
combiner = Image('checkerboard.tif') 
r = Image(p.width, p.height, p.depth) 
 
for y in range(p.height): 
    for x in range(p.width): 
        r1, g1, b1 = p.get(x, y) 
        r2, g2, b2 = q.get(x, y) 
        rc, gc, bc = combiner.get(x, y) 
        red = (r1 * rc) + (r2 * (1.0 - rc)) 
        green = (g1 * gc) + (g2 * (1.0 - gc)) 
        blue = (b1 * bc) + (b2 * (1.0 - bc)) 
        r.put(x, y, red, green, blue) 
 
r.save('combine.tif') 

composite.py



from Image import Image 
 
frog = Image('frog.tif') 
frog_matte = Image('frogmatte.tif') 
background = Image('random.tif') 
 
composite = Image(frog.width, frog.height, frog.depth) 
 
for y in range(frog.height): 
    for x in range(frog.width): 
        r_fg, g_fg, b_fg = frog.get(x,y) 
        r_matte, g_matte, b_matte = frog_matte.get(x,y) 
        r_bg, g_bg, b_bg = background.get(x,y) 
 
        matte = r_matte 
        matte_inverse = 1.0 - r_matte 
 
        r = r_fg * matte + r_bg * matte_inverse 
        g = g_fg * matte + g_bg * matte_inverse 
        b = b_fg * matte + b_bg * matte_inverse 
 
        composite.put(x, y, r, g, b) 
 
composite.save('composite.tif')