Functional Problem Solving (CSC 151 2014F) : Readings
Primary: [Front Door] [Schedule] - [Academic Honesty] [Disabilities] [Email] - [FAQ] [Teaching & Learning] [Grading] [Rubric] - [Calendar]
Current: [Assignment] [EBoard] [Lab] [Outline] [Reading]
Sections: [Assignments] [EBoards] [Examples] [Handouts] [Labs] [Outlines] [Readings]
Reference: [Setup] [VM] [Errors] - [Functions A-Z] [Functions By Topic] - [Racket] [Scheme Report (R5RS)] [R6RS] [TSPL4]
Related Courses: [Davis (2013F)] [Rebelsky (2014S)] [Weinman (2014F)]
Misc: [Submit Questions] - [SamR] [Glimmer Labs] [CS@Grinnell] [Grinnell] - [Issue Tracker (Course)]
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.
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,
(
and
image-variant
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.
image-transform!
image
colortrans
)
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)
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, (
,
that composes all of the procedures (applying them from right to
left).
o
proc1
proc2
...
procn-1
procn
)
(image-variant kitten (o irgb-bluer irgb-bluer irgb-bluer))
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)
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.
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"))
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.
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.
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?)
(image-variant
image
fun
)
image
, each of whose pixels is computed
by applying fun
to the color of the
corresponding pixel in image
.
(image-transform!
image
fun
)
image
in place by setting
each pixel to the result of applying fun
to
that current pixel color.
(compose
f
g
)
((compose f g) x)
is the same as (f (g x))
.
(o
f1
f2
...
fn-1
fn
)
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)))))
.