Skip to main content

Transforming Images

We explore how to expand the power of color transformations, first by applying them to images rather than to individual colors then by further combining them into new transformations.

From Transforming Colors to Transforming Images

You may have been asking why we have been focusing on transforming individual colors, other than that using the transformations helps us better understand the underlying representation. Here’s one reason: Many image filters are written by applying a transformation to each color in the image.

So, how do we write image filters? That is, how do we generalize color transformations to image filters? MediaScript includes a helpful procedure, (image-variant image colortrans), that builds a new image by setting the color at each position in the new image to the result of applying the given color transformation to the color at the corresponding position in the original image.

Using image-variant and the color transformations we learned in the previous reading, we can now transform images in a few basic ways: we can lighten images, darken images, complement images (and perhaps even compliment the resulting images), and so on and so forth.

Let’s consider a few examples. We’ll start with this public domain image of a kitten, which we will refer to as kitten.

An image of a kitten

Here are two variants of the image.

A redder version of the same kitten image as above (image-variant kitten irgb-redder)

A lighter version of the same kitten image (image-variant kitten irgb-lighter)

Composing Transformations

But what if we want more interesting filters, ones that can’t be described with just a single built-in transformation? One thing that we can do is to combine transformations. There are two ways to transform an image using more than one transformation: You can do each transformation in sequence, or you can use function composition, an old mathematical trick that you learned how to do in Scheme in the last reading. Consider, for example, the problem of lightening an image and then increasing the red component. We can certainly write the following sequence of definitions.

> (define intermediate-picture (image-variant picture irgb-redder))
> (define modified-picture (image-variant intermediate-picture irgb-lighter))

However, it is not necessary to name the intermediate image. We can instead choose to nest the calls to image-variant, using something like this definition.

> (define modified-picture (image-variant (image-variant picture irgb-redder) irgb-lighter))

However, even this more concise instruction still creates the intermediate (redder but not lighter) version of the picture. Can we make each color in the image both redder and lighter?

In the previous reading, we learned that we can define a new transformation by combining other transformations with the composition function, compose. Using that function, we can write the following instructions.

> (define irgb-fun (compose irgb-lighter irgb-redder))
> (image-variant picture irgb-fun)

But even that is a bit verbose. Do we really want to name irgb-fun when we only use it once? No. Fortunately, Scheme lets us use the function created by compose without naming it, just as it lets us use most expressions without naming them.

> (define modified-picture (image-variant picture (compose irgb-lighter irgb-redder)))

An image of a kitten made both redder and bluer (image-variant kitten (compose irgb-lighter irgb-redder))

What’s the difference between this instruction and the nested calls to image-variant? In effect, we’ve changed the way you sequence operations. That is, rather than having to write multiple instructions, in sequence, to get something done, we can instead insert information about the sequencing into a single instruction. By using composition, along with nesting, we can then express our algorithms more concisely and often more clearly. It is also likely to be a bit more efficient, since we make one new image, rather than two.

Detour: Loading Images

It may make sense to be able to transform an image by transforming every color in it. But where do we get the images to start with? Mediascheme include an image-load function that loads an image from the computer and returns an integer that we can use to identify the image in calls to image-show and image-variant. The image-load function that takes one parameter, a string that gives the full path to the file containing the image. (You may recall that “string” is the term we use for a sequence of text in Scheme, and that we surround strings with double quotation marks. For example, "/home/rebelsky/Desktop/kitten.jpg".

For example, here’s a sequence of operations that show the image of the kitten that we use in this reading.

> (define pic (image-load "/home/rebelsky/Desktop/kitten.jpg"))
> (image-show pic)

You’ll note that there are two separate “names” for the image. In the file system, we name it "/home/rebelsky/Desktop/kitten.jpg". However, the Scheme program has no natural way of understanding whether that is intended to be the name of a file, or a color name, or a student name, or something else. The image-load command tells DrRacket what the string represents and converts it into a form that we can then use. And, is our habit whenever we compute a value, we name that computed value, in this case as pic.

Detour: Saving Images

It’s useful to be able to load images so that we can manipulate them. It’s even more useful to be able to save images that we’ve created. The procedure (image-save image filename) saves an image to a specified file. You should provide the full path name to the file, surrounding it by double-quotation marks. For example, you might use something like "/home/student/images/masterpiece.png". The suffix you give to the file name determines the type of file that is saved - jpg, gif, png, and so forth.

Here’s a simple sequence of operations to make and save a strange version of our kitten.

> (define pic (image-load "/home/rebelsky/Desktop/kitten.jpg"))
> (image-show pic)
> (define strange (image-variant pic (compose rgb-complement rgb-rotate)))
> (image-show strange)
> (image-save strange "/home/student/Desktop/strange-kitten.png")

Note that you can use image-save with any image we make, whether it’s created with image-load, image-variant, or any of the other image-making and image-modifying commands we will learn.


So far, so good. We know how to load images with image-load. We know how to make new versions of existing images using image-variant. We know how to save the result using image-save. Are we missing anything?

It turns out that we’re missing a few things. Right now, the only way we can make a variant of an image is using one of the unary (single-parameter) procedures, either the built-in procedures, such as irgb-redder, or ones we create with the composition operator, compose. But we know other procedures transforming colors, such as irgb-subtract, that are not unary. Can we use them?

It doesn’t make sense to write (irgb-subtract picture (irgb 0 0 255)), since picture is not an integer-encoded RGB color. (Unfortunately, Mediascheme will let us do so because both colors and images are represented as integers, and it doesn’t know what the integer represents.) It also doesn’t make sense to use irgb-subtract as the second parameter to image-variant, as in (image-variant pic irgb-subtract), because we don’t have a place to specify the color we are subtracting. In this case, DrRacket will issue an error message.

> (define pic (image-load "/home/rebelsky/Desktop/kitten.jpg"))
> (image-variant pic irgb-subtract)
share/lib/gigls/guard.rkt:28:6: irgb-subtract: expects 2 parameters, given 1
  in (irgb-subtract 3037011)

We might be tempted to write something like (image-variant pic (irgb-subtract (irgb 0 0 255)). However, that will also cause problems, since it looks like we are calling irgb-subtract on a single value, and not the two values it is supposed to take.

> (image-variant pic (irgb-subtract (irgb 0 0 255)))
share/lib/gigls/guard.rkt:28:6: irgb-subtract: expects 2 parameters, given 1
  in (irgb-subtract 255)

In fact, it’s probably good that we get an error message here, since it’s not clear whether (irgb 0 0 255) is supposed to be the first or second parameter to irgb-subtract - are we subtracting (irgb 0 0 255) from each color, or are we subtracting each color from (irgb 0 0 255)?

To handle situations like this, our variant of Scheme includes a special procedure, section, that lets you fill in some parameters to a function. It takes the form (section procedure param-info …). When we want to fill in a particular parameter, we write the value we want. When we want to leave a parameter blank, we write the special symbol <>. For example, here’s a function that subtracts the blue component from every color.

> (define irgb-subtract-blue (section irgb-subtract <> (irgb 0 0 255)))
> (image-show (image-variant pic irgb-subtract-blue))

An image of a kitten with all blue removed

If, instead, we want to subtract the current color from white (which is how we computed the pseudo-complement), we can swap the place that we put the special symbol.

> (define irgb-sub-from-white (section irgb-subtract (irgb 255 255 255) <>))
> (image-show (image-variant pic irgb-sub-from-white))

An image of a kitten with each color subtracted from white

As in the case of unary functions created with compose, we don’t have to name the function we create.

> (image-show (image-variant pic (section irgb-average (irgb 0 0 127) <>)))

An image of a kitten where each color was averaged with (irgb 0 0 127)

Self Checks

Brief Preparation

a. If you have not already done so, type the following in a terminal window.


b. Follow the steps for setting up the GIMP+Mediascheme combination from the lab on RGB colors.

c. Create a new relatively small image using color-grid. Use a variety of colors, as well as black and white. For example,

(require gigls/unsafe)
(define sample (color-grid 10 5 3 
                           "red" "white" "black"
                           "yellow" "green" "violet"
                           (irgb 128 0 128) (irgb 128 0 255) (irgb 255 0 128)))

d. Load the kitten image from the reading:

(define picture (image-load "/home/rebelsky/MediaScheme/Images/kitten.png"))

Check 1: Exploring Variants

a. What do you expect to happen when you use image-variant to complement each pixel in sample, using the following instruction?

> (image-show (image-variant sample irgb-complement))

b. Check your answer experimentally.

c. What do you expect to have happen if you use image-variant to complement each color in picture? (You would use nearly the same instruction, substituting picture for sample.)

d. Check your answer experimentally.

e. What do you expect to happen when you use image-variant and compose to doubly complement each color in sample, using the following instruction?

> (image-show (image-variant canvas (compose irgb-complement irgb-complement)))

f. Check your answer experimentally.

Check 2: Undoing Transformations

a. Create a variant of sample using irgb-redder.

b. You may have noticed that in check 2, we were able to undo the complement transformation by complementing again. Is there an easy way to undo the redden operation? (You do not have to write code; just explain how to do it.)

c. Are there transformations or sequences of transformations that would be impossible to undo? (That is, can you do something to an image such that there is nothing that you can do to the revised image that will bring back the original image?)


(image-variant image fun) MediaScheme GIMP Procedure.
Create a new image of the same width and height as image, each of whose pixels is computed by applying fun to the color of the corresponding pixel in image.
(image-transform! image fun) MediaScheme GIMP Procedure.
Transform image in place by setting each pixel to the result of applying fun to that current pixel color.
(compose f1 f2 ... fn-1 fn) Traditional Higher-Order Procedure.
Build a one-parameter procedure that applies each f, in turn, starting with fn and working backwards. The composition, when applied to a value, x, produces the same result as (f1 (f2 (... (fn-1 (fn x))...))).
(image-load filename) MediaScheme GIMP Procedure.
Load an image from a file. The name of the file is a string (and, unless a named value, typically surrounded by quotation marks).
(image-save image fname) MediaScheme GIMP Procedure.
Save image in the specified file (which should provide the full path to the file). The type of the image (JPEG, GIF, PNG, etc.) is determined by the suffix of the file name.
(section procedure value-or-hole-1 ... value-or-hole-n) Mediascheme Higher-order Procedure/Macro.
Creates a new procedure by specifying some of the parameters to procedure, leaving the rest as parameters for the result procedure. Holes (parameters for the result proedure) are indicated with the <> symbol.