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 examine the building blocks of one of the common kinds of algorithms used for RGB colors: Generating new colors from existing colors.
We have just started to learn about RGB colors, and so the operations we might do on images and colors are somewhat basic. How will we expand what we can do, and what we can write? In part, we will learn new Scheme techniques, applicable not just to image computation, but to any computation. In part, we will learn new functions in the MediaScript library that support more complex image computations. In part, we will write our own more complex functions.
We've been focusing primarily on how one might write algorithms to make new images. However, it is equally useful to manipulate existing images. So, what kinds of things might we do with existing images? One common algorithmic approach to images is the construction of filters, algorithms that systematically convert one image to another image. Complex filters can do a wide variety of things to an image, from making it look like the work of an impressionist painter to making it look like the image has been painted onto a sphere. However, it is possible to write simple filters with not much more Scheme than you know already.
Over the next few readings and labs we will consider filters that are constructed by transforming each color in an image using an algorithm that converts one RGB color to another. In the first RGB lab, you began to think about such algorithms as you computed the pseudo-complement of an RGB color or varied the components of the color. In this reading and the corresponding lab, we will consider the basic building blocks of filters: MediaScript's basic operations for transforming colors and the ways to combine them into more complex color transformations. In the lab, you will also explore how to write your own transformations. In the next reading, we will see how to use those transformations to transform whole images. After that, we'll explore how you write new transformations.
Rather than writing every transformation from scratch, we will start with a few basic transformations that MediaScript includes.
The simplest transformations are irgb-darker
and irgb-lighter
. These operations make a color
a little bit darker and a little bit lighter. If you apply them
repeatedly, you can darker and darker (or lighter and lighter) colors,
eventually reaching black (or white).
>
(define sample (color-name->irgb "blueviolet"))
>
(irgb->string sample)
"138/43/226"
>
(define darker-sample (irgb-darker sample))
>
(irgb->string darker-sample)
"122/27/210"
>
(define lighter-sample (irgb-lighter sample))
>
(irgb->string lighter-sample)
"154/59/242"
>
(define doubly-darker-sample (irgb-darker (irgb-darker sample)))
>
(irgb->string doubly-darker-sample)
"106/11/194"
sample
sample, darker-sample
sample, lighter-sample
sample, doubly-darker-sample
Note that these are pure procedures. When you compute a darker or lighter version of a color, the purpose is to create a new color. Hence, the original color is unchanged.
In addition to making the color uniformly darker or lighter, we can also
increase individual components using irgb-redder
,
irgb-greener
, and irgb-bluer
.
>
(define sample (color-name->irgb "blueviolet"))
>
(irgb->string sample)
"138/43/226"
>
(irgb->string (irgb-redder sample))
"170/43/226"
>
(irgb->string (irgb-greener sample))
"138/75/226"
>
(irgb->string (irgb-bluer sample))
"138/43/255"
sample, (irgb-redder sample)
sample, (irgb-greener sample)
sample, (irgb-bluer sample)
As the examples suggest, for some people, making a color slightly redder,
greener, or bluer is hard to detect. Sometimes it's easier to see the
changes if we make the transformations a few times. (Since the first
call to irgb-bluer
increases the blue component to
its largest value, we won't see further increases.)
sample, (irgb-redder (irgb-redder sample))
sample, (irgb-greener (irgb-greener sample))
sample, (irgb-bluer (irgb-bluer sample))
The irgb-rotate
procedure rotates the red, green,
and blue components of a color, setting red to green, green to blue,
and blue to red. It is intended mostly for fun, but it can also
help us think about the use of these components.
The irgb-phaseshift
procedure is another procedure
with less clear uses. It adds 128 to each component with a value
less than 128 and subtracts 128 from each component with a value
of 128 or more. While this is somewhat like the computation of a
pseudo-complement, it also differs in some ways. Hence, MediaScript also
provides an irgb-complement
procedure that computes
the pseudo-complement of an RGB color.
>
(define sample (color-name->irgb "blueviolet"))
>
(irgb->string sample)
"138/43/226"
>
(irgb->string (irgb-rotate sample))
"43/226/138"
>
(irgb->string (irgb-phaseshift sample))
"10/171/98"
>
(irgb->string (irgb-complement sample))
"117/212/29"
sample, (irgb-rotate sample)
sample, (irgb-phaseshift sample)
sample, (irgb-complement sample)
Now that we know some basic transformations to apply to colors, we
can use those transformations in a variety of ways. First, we can
use it to change one pixel in an image. How? We get the color of the
pixel, transform it, and then set the color of the pixel. For example,
here's how we might phase shift the top-left pixel in the image called
landscape
.
>
(image-set-pixel! landscape 0 0 (irgb-phaseshift (image-get-pixel landscape 0 0)))
What if we instead wanted to make pixel at (2,3) a bit redder? We'd write something like the following.
>
(image-set-pixel! landscape 2 3 (irgb-redder (image-get-pixel landscape 2 3)))
How about if we wanted to darken the top-left pixel of a different image,
one called portrait
? The instruction would be much the same.
>
(image-set-pixel! portrait 0 0 (irgb-darker (image-get-pixel portrait 0 0)))
As we just noted, each of these examples is quite similar. The examples
differ in the image, the position, and the transformation,
but the rest of the code is the same. (For example, we
need to call both image-set-pixel!
and
image-get-pixel
in the same way.) We also see
ourselves duplicating a lot. In each case, we need to write the name
of the image twice and the position twice. As you might guess, having
to repeat the same information again and again often leads to errors.
When computer programmers realize that they are writing nearly
identical expressions again and again and again, they tend to write
new functions that encapsulate the common portions. Many call
this process refactoring. The designers
of MediaScript certainly expected people to change pixels, and did
so themselves. To help programmers, they refactored the code
and devised a more concise way to change a pixel, which they
called (
. Hence, to do the same
three operations given above, using image-transform-pixel!
image
column
row
transformation
)image-transform-pixel!
,
we would write the following.
>
(image-transform-pixel! landscape 0 0 irgb-phaseshift)
>
(image-transform-pixel! landscape 2 3 irgb-redder)
>
(image-transform-pixel! portrait 0 0 irgb-darker)
This code is certainly a bit more concise, and perhaps even easier to
understand. However, behind the scenes, it does exactly the same thing
that the previous code. How is image-transform-pixel!
implemented? Let's look at the code from the MediaScript library.
;;; Procedure: ;;; image-transform-pixel! ;;; Parameters: ;;; image, an image identifier ;;; col, an integer ;;; row, an integer ;;; ctrans, a function from rgb colors to rgb colors ;;; Purpose: ;;; Transform one pixel in the image ;;; Produces: ;;; [Nothing; Called for the side effect] ;;; Preconditions: ;;; image names a valid image. ;;; 0 <= col < (image-width image) ;;; 0 <= row < (image-height image) ;;; For any rgb color, c, (rgb? (ctrans c)) ;;; Postconditions: ;;; Let c be (image-get-pixel image col row) prior to this call. ;;; After this call, (image-get-pixel image col row) is now (ctrans c). (define image-transform-pixel! (lambda (image col row ctrans) (image-set-pixel! image col row (ctrans (image-get-pixel image col row)))))
Is there anything surprising about the
image-transform-pixel!
procedure? We hope
you won't find it surprising, but some of you who have programmed
before may note something a bit puzzling - We've made one procedure
(irgb-phaseshift
, irgb-redder
,
or irgb-darker
) a parameter to another procedure
(image-transform-pixel!
). Not all programming
languages permit you to make procedures parameters, but those that do
can help you write more clearly and concisely, as in this example.
Using procedures as parameters adds a slight complexity to the language. When you want to call a procedure directly, you need to precede it with an open parenthesis. However, when you want to pass it as a parameter to another procedure, you do not precede it with a parenthesis.
If you think back to the beginning of this reading, you may recall that we
suggested that one reason to learn to transform colors is that by transforming
colors, you can also build filters. Do we have enough information to write
a filter for a four-by-three image? Certainly. Suppose we wanted to compute
the complement of this image. We could write a sequence of calls to
the image-transform-pixel!
procedure.
(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 0 3 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 1 3 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 2 3 irgb-complement)
That's certainly an awful lot of typing, even for a small image. In the next reading, we'll consider some disadvantages of this technique and learn how to get MediaScript to automatically figure out all of the calls for an image.
At the beginning of this reading, you learned a few basic procedures for
transforming colors. Are these the only ways that you can transform
colors? Certainly not! You are free to write your
own color transformations. For example, you might decide that
irgb-greener
does not make colors sufficiently
greener, since it only affects the green component. (Arguably, to make
something look greener, we might not only increase the green component,
but also decrease the red and blue components.) Hence, you could write
your own version as follows:
(define greener (lambda (color) (irgb (- (irgb-red color) 32) (+ (irgb-green color) 64) (- (irgb-blue color) 32))))
>
(define sample (color-name->irgb "blueviolet"))
>
(irgb->string sample)
"138/43/226"
>
(irgb->string (greener sample))
"106/107/194"
sample, (greener sample)
Of course, this is not the only way to define a procedure like
greener
. We could also define it in terms
of the existing procedures. For example, since
irgb-greener
seems to increment the green
component by 32 and irgb-darker
seems to
decrement all three components by 16, we could try something like
(define greener2 (lambda (color) (irgb-greener (irgb-greener (irgb-greener (irgb-darker (irgb-darker color)))))))
>
(define sample (color-name->irgb "blueviolet"))
>
(irgb->string sample)
"138/43/226"
>
(irgb->string (greener2 sample))
"106/107/194"
There are also color transformations you cannot build (at least not
easily) from the basic transformation. For example, suppose you
want to eliminate extreme colors. You might choose to bound each
component so that it is at least 64 and no more than 192. If we
rely on the bound
procedure from the lab on writing
procedures, we can write something like
(define irgb-bound (lambda (color) (irgb (bound (irgb-red color) 64 192) (bound (irgb-green color) 64 192) (bound (irgb-blue color) 64 192))))
>
(define sample (color-name->irgb "blueviolet"))
>
(irgb->string sample)
"138/43/226"
>
(irgb->string (irgb-bound sample))
"138/64/192"
>
(define black (color-name->irgb "black"))
>
(irgb->string black)
"0/0/0"
>
(irgb->string (irgb-bound black))
"64/64/64"
>
(context-list-colors "smoke")
("whitesmoke")
>
(define smoke (color-name->irgb "whitesmoke"))
>
(irgb->string smoke)
"245/245/245"
>
(irgb->string (irgb-bound smoke))
"192/192/192"
sample, (irgb-bound sample)
black, (irgb-bound black)
smoke, (irgb-bound smoke)
So, what should you take away from what we've just learned? You now know a few new functions in MediaScript, particularly functions that transform colors. You've now learned about a technique that computer scientists use, refactoring, which involves writing new functions that encapsulate common code. You've seen that Scheme permits procedures to take other procedures as parameters, and that this permission supports refactoring. You've also learned how to write your own transformations.
For the immediate future, knowing the particular transformations will be helpful. Over the longer term, knowing about refactoring and knowing how to use procedures as parameters will be even more helpful (just as knowing how to write your own procedures is more helpful than knowing any particular procedure).
(irgb-lighter
irgb-color
)
(irgb-darker
irgb-color
)
(irgb-redder
irgb-color
)
(irgb-greener
irgb-color
)
(irgb-bluer
irgb-color
)
(irgb-rotate
irgb-color
)
(irgb-phaseshift
irgb-color
)
(irgb-complement
irgb-color
)
(image-transform-pixel!
image
column
row
func
)
col
,row
)
in image
by applying
func
to its old color and setting that
pixel to the resulting color.
Suppose we want to use
to make a
pixel in image-transform!-pixel!
canvas
lighter. What is wrong with the
following expression? Why? What should the expression say?
(image-transform-pixel! canvas 0 0 (irgb-lighter))