CSC151.02 2010S Functional Problem Solving : Readings

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.) Think about what happens in a 200x200 image, which has 40,000 positions. It's also a lot to change if we decide to, say, make the image darker rather than complementing it.

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? MediaScript includes a pair of helpful procedures, (image-variant image colortrans) and (image-transform! image colortrans). The first builds a new image by setting each pixel in the new image to the result of applying the given color 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.

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.

As you may recall, we started this process in the previous reading, as we began to consider how one writes filters that create new images from old. We are now almost done on this quest. We've learned some new functions provided by MediaScript, 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. 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))

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

http://public-photo.net/displayimage-2485.html

Here are a few variants of the image.

(image-variant kitten rgb-redder)

(image-variant kitten rgb-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, 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 intermediate-picture (image-variant picture rgb-redder))
> (define modified-picture (image-variant intermediate-picture rgb-lighter))

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

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

However, even this more concise instruction still creates the intermediate, redder, version of the picture. What we really want to do is make just one new image in which the color transformation is “lighter and redder”, not one image which is redder and another which is lighter than the intermediate image. In mathematics, when you want to build a function that does the work of two functions, you compose those functions. In MediaScript, 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 (color-name->rgb "blueviolet"))
>; (rgb->string sample)
"138/43/226"
>; (rgb->string (lr sample))
"186/59/242"

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)))

(image-variant kitten (compose rgb-lighter rgb-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 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, MediaScript also provides a procedure, (o proc1 proc2 ... procn-1 procn), that composes all of the procedures (applying them from right to left).

(image-variant kitten (o rgb-bluer rgb-bluer rgb-bluer))

Writing Your Own Transformations

As we saw in the previous reading, instead of relying on the primary RGB transformations we can also write our own transformations. Let's start with a transformation that extracts just the red component of a color, setting the other two components to zero.

;;; Procedure:
;;;   rgb-only-red
;;; Parameters:
;;;   rgb, an RGB color
;;; Purpose:
;;;   Create a new RGB color that contains only the red component
;;;   of rgb.
;;; Produces:
;;;   red, an RGB color
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   (rgb-red red) = (rgb-red rgb)
;;;   (rgb-green red) = 0
;;;   (rgb-blue red) = 0
(define rgb-only-red
  (lambda (rgb)
    (rgb-new (rgb-red rgb) 0 0)))

(image-variant kitten rgb-only-red)

Similarly, we can write a transformation that sets the red component to zero.

;;; Procedure:
;;;   rgb-no-red
;;; Parameters:
;;;   rgb, an RGB color
;;; Purpose:
;;;   Create a new RGB color whose red component is zero.
;;; Produces:
;;;   not-red, an RGB color
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   (rgb-red not-red) = 0
;;;   (rgb-green not-red) = (rgb-green rgb)
;;;   (rgb-blue not-red) = (rgb-blue rgb)
(define rgb-no-red
  (lambda (rgb)
    (rgb-new 0 (rgb-green rgb) (rgb-blue rgb))))

(image-variant kitten rgb-no-red)

Anonymous Transformations

If you think back to our initial encounter with compose, we did something that may be a bit surprising. We first noted that image-variant takes a procedure as its second parameter. We then built our own procedure, lr, and used it to modify the picture.

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

That, in itself, should not be too surprising (except that we've found a new way to define functions, without using lambda). What may be surprising is what we did next. We replaced lr with the compose expression.

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

As you may recall from our discussion of Scheme evaluation, when the Scheme interpreter encounters a define, it effectively enters the name and the corresponding value into a table. Then, when it sees a name later, it looks up the name, and gets the corresponding value (in this case, an expression). In substituting the compose expression for lr, we sidestepped one phase of the Scheme interpreter. Why? Because it lets us avoid taking the time to name that expression.

We can even do the same thing with procedures defined using the more familiar lambda expression. Consider, for example, our instruction to build a variant of the sample image with only a red component.

(define rgb-only-red
  (lambda (rgb)
    (rgb-new (rgb-red rgb) 0 0)))
> (image-variant kitten rgb-only-red)

Instead of relying on the Scheme interpreter to substitute in the lambda expression, we can write the lambda expression ourselves. That is, we can write

> (image-variant kitten (lambda (rgb) (rgb-new (rgb-red rgb) 0 0)))

Now, when we want a different variant, say, one with only the blue component, we need not define a new procedure. We just plug in the body of that procedure into a call to image-variant.

(image-variant kitten (lambda (rgb) (rgb-new 0 0 (rgb-blue rgb))))

Similarly, here's a variant in which we swap the red and blue components.

(image-variant kitten (lambda (rgb) (rgb-new (rgb-blue rgb) 0 (rgb-red rgb))))

(Think about what Andy Warhol could have done with these variants!)

You'll note that in neither case did we name the procedure that we used to transform the colors in the image. Procedures that are used without naming them are called anonymous procedures. While there are a host of reasons for using anonymous procedures, we frequently use anonymous procedures when we expect to use a procedure only once. In the cases above, keeping only the blue component or swapping the red and blue components are not typical transformations. Hence, it is probably not worth the time and effort to write (and, as importantly, to document) new procedures.

Reference

(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 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-10 Janet Davis, Matthew Kluber, Samuel A. Rebelsky, and Jerod Weinman. (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.