Fundamentals of Computer Science I: Media Computing (CS151.01 2008S)

Transforming Images


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

From Transforming Pixels to Transforming Images

If you think back to the end of the previous reading on transforming RGB images, you may recall that we were starting to write filters that transformed not just individual colors but whole images. Here is one set of commands that transforms a four-by-three image called canvas.

(image-transform-pixel! canvas 0 0 rgb-complement)
(image-transform-pixel! canvas 0 1 rgb-complement)
(image-transform-pixel! canvas 0 2 rgb-complement)
(image-transform-pixel! canvas 1 0 rgb-complement)
(image-transform-pixel! canvas 1 1 rgb-complement)
(image-transform-pixel! canvas 1 2 rgb-complement)
(image-transform-pixel! canvas 2 0 rgb-complement)
(image-transform-pixel! canvas 2 1 rgb-complement)
(image-transform-pixel! canvas 2 2 rgb-complement)
(image-transform-pixel! canvas 3 0 rgb-complement)
(image-transform-pixel! canvas 3 1 rgb-complement)
(image-transform-pixel! canvas 3 2 rgb-complement)

That's an awful lot of typing. (Even though it seems that we've been typing a lot over the past few days, we do want to type less, and will look for techniques for doing so.) It's also a lot to change if we decide to, say, make the image darker rather than complementing it. We can certainly make both issues a bit easier by using foreach!.

(foreach!
  (lambda (pos) 
    (image-transform-pixel! canvas (car pos) (cadr pos) rgb-complement))
  '((0 0) (1 0) (2 0) (3 0)
    (0 1) (1 1) (2 1) (3 1)
    (0 2) (1 2) (2 2) (3 2)))

However, neither technique can be easily adapted to images of different sizes or to typical-sized images. (In a 200x200 image, which is still fairly small, there are 40,000 positions. No one wants to type that many lines of code, particularly when it is that repetitive. Writing a list of that size is also not very tempting.) Both are also fairly slow. Both techniques are also impure: The affect the original image, and may not be possible or easy to undo.

So, is there a better way to write image filters? That is, how can we write filters that are faster, that are pure when we want purity, and that don't require thousands of lines of incredibly repetitive code? DrFu includes a pair of helpful procedures, (image-variant image transformation) and (image-transform! image transformation). The first builds a new image by setting each pixel in the new image to the result of applying the given transformation to every pixel in another image. The second changes an existing image by applying the transformation “in place”. You will explore the differences between the two in the lab.

You may find that the order of the parameters in image-variant is a bit different than you'd expect. In the case of map, we put the function first and the list second. In the case of image-variant, we put the function second. As you've noticed, we've made it custom to put the type mentioned in the function name first. However, the tradition with map is to make the function the first parameter. As we'll see again and again, there are multiple customs for how one writes or designs procedures, and sometimes these customs come into conflict. (At first, we used the name image-map for image-variant. However, we decided that the order of parameters would be inappropriate with the “map”, and chose this new name.)

Using image-variant (or image-transform!) 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.

Composing Transformations

We are almost done on our quest to learn how to write filters. We've learned some new functions provided by DrFu, such as the transformations. We've learned about one new technique, refactoring, which involves writing new functions that encapsulate common code. We've seen that Scheme permits procedures to take other procedures as parameters, and that this permission supports refactoring. Finally, we've learned a new kind of operation, map, which lets us do something to every component of a compound value. All this put together lets us write some simple filters. For example, we can write one line of Scheme code to make a a picture bluer.

> (define bluer-picture (image-variant picture rgb-bluer))

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, to first combine the transformations. Consider, for example, the problem of lightening an image and then increasing the red component. We can certainly write the following:

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

However, what we really want to do is make a new image in which each pixel in the result is lighter and redder than the pixel in the original. In mathematics, when you want to build a function that does the work of two functions, you compose those functions. In DrFu, the compose procedure lets you combine multiple procedures into one. Here's a new procedure that makes colors lighter and then redder.

> (define lr (compose rgb-redder rgb-lighter))
> (define sample (cname->rgb "blue violet"))
> (rgb->string (lr sample))
"183/111/175"

Using function composition, we can therefore rewrite the earlier transformation as

> (define modified-picture (image-variant picture lr))
or, as simply,
> (define modified-picture (image-variant picture (compose rgb-redder rgb-lighter)))

What's the big difference between this instruction and the series of two instructions? 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 could 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.

Because compose is a bit long to write, because a circle is used for the mathematical composition function, and because we sometimes want to compose more than two procedures, DrFu also provides a procedure, (o proc1 proc2 ... procn-1 procn), that composes all of the procedures (applying them from right to left).

Reviewing Key Concepts

In most of the initial readings for this course, you were learning some basic operations that you could use in writing algorithms. In this reading, while you learned about new operations, they were a different kind of operation. image-variant, image-transform!, and compose all provide a way to add control to your program. The first two do something to each pixel in an image, the last sequences operations within a single Scheme instruction.

Reference

(image-variant image fun)
DrFu 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)
DrFu procedure. Transform image in place by setting each pixel to the result of applying fun to that current pixel color.
(compose f g)
Traditional Higher-Order Procedure. Build a one-parameter procedure that applies g to its parameter, and then f to that result. ((compose f g) x) is the same as (f (g x)).
(o 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))))).

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright (c) 2007-8 Janet Davis, Matthew Kluber, and Samuel A. Rebelsky. (Selected materials copyright by John David Stone and Henry Walker and used by permission.)

This material is based upon work partially supported by the National Science Foundation under Grant No. CCLI-0633090. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.

This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.