Functional Problem Solving (CSC 151 2014F) : 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 irgb-complement)
(image-transform-pixel! canvas 0 1 irgb-complement)
(image-transform-pixel! canvas 0 2 irgb-complement)
(image-transform-pixel! canvas 1 0 irgb-complement)
(image-transform-pixel! canvas 1 1 irgb-complement)
(image-transform-pixel! canvas 1 2 irgb-complement)
(image-transform-pixel! canvas 2 0 irgb-complement)
(image-transform-pixel! canvas 2 1 irgb-complement)
(image-transform-pixel! canvas 2 2 irgb-complement)
(image-transform-pixel! canvas 3 0 irgb-complement)
(image-transform-pixel! canvas 3 1 irgb-complement)
(image-transform-pixel! canvas 3 2 irgb-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 irgb-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 irgb-redder)

(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, 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 irgb-redder))
> (define modified-picture (image-variant intermediate-picture irgb-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 irgb-redder) irgb-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 irgb-redder irgb-lighter))
> (define sample (color-name->irgb "blueviolet"))
> (irgb->string sample)
"138/43/226"
> (irgb->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 irgb-redder irgb-lighter)))

(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 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 irgb-bluer irgb-bluer irgb-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:
;;;   irgb-only-red
;;; Parameters:
;;;   color, an integer-encoded RGB color
;;; Purpose:
;;;   Create a new integer-encoded RGB color that contains only the 
;;;   red component of the given color.
;;; Produces:
;;;   red, an integer-encoded RGB color
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   (irgb-red red) = (irgb-red rgb)
;;;   (irgb-green red) = 0
;;;   (irgb-blue red) = 0
(define irgb-only-red
  (lambda (color)
    (irgb (irgb-red color) 0 0)))

(image-variant kitten irgb-only-red)

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

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

(image-variant kitten irgb-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 irgb-redder irgb-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 irgb-redder irgb-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 irgb-only-red
  (lambda (color)
    (irgb (irgb-red color) 0 0)))
> (image-variant kitten irgb-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 (color) (irgb (irgb-red color) 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 (color) (irgb 0 0 (irgb-blue color))))

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

(image-variant kitten (lambda (color) (irgb (irgb-blue color) 0 (irgb-red color))))

(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.

Self Checks

Brief Preparation

a. Create a new 4x3 image, call it canvas and set a few pixels in canvas to colors of your choice. Leave others black or white. Do not show it; you'll want to see the pixel transformations. For example:

(require gigls/unsafe)
(context-set-bgcolor! "black")
(define canvas (image-new 4 3))
(image-set-pixel! canvas 0 0 (irgb-new 255   0    0)) ; Set top-left to red
(image-set-pixel! canvas 0 1 (irgb-new   0 255    0)) ; Set below it to green
(image-set-pixel! canvas 3 0 (irgb-new   0 128  128)) ; Set top-right to cyan
(image-set-pixel! canvas 2 2 (irgb-new   0   0    0)) ; Set "middle" to black

b. 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 canvas, using the following instruction?

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

b. Check your answer experimentally.

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

d. Check your answer experimentally.

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

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

f. Check your answer experimentally.

Check 2: Transforming “In Place

Although the reading emphasizes image-variant, MediaScheme also provides the image-transform! procedure, which changes the image, rather than creating a new image.

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

> (image-transform! canvas irgb-complement)

b. Check your answer experimentally. (Hint: you'll finally need to show danvas.)

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

d. Check your answer experimentally. Likewise, you should show the image after complementing.

e. What do you expect to have happen if you once again complement each pixel in picture?

f. Check your answer experimentally.

Check 3: Undoing Transformations

a. What do you expect to have happen if you use image-transform! to redden each pixel in canvas?

b. Check your answer experimentally.

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

d. 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?)

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