Functional Problem Solving (CSC 151 2016S) : Readings
Primary: [Front Door] [Schedule] - [Academic Honesty] [Disabilities] [Email] - [FAQ] [Teaching & Learning] [Grading] [Taking Notes] [Rubric]
Current: [Assignment] [EBoard] [Lab] [Outline] [Reading]
Sections: [Assignments] [EBoards] [Labs] [Outlines] [Readings] - [Examples] [Handouts]
Reference: [Setup] [Remote] [VM] [Errors] - [Functions A-Z] [Functions By Topic] - [Racket] [Scheme Report (R5RS)] [R6RS] [TSPL4]
Related Courses: [Curtsinger (2016S)] [Davis (2013F)] [Rebelsky (2015F)] [Weinman (2014F)]
Misc: [SamR] [Glimmer Labs] [CS@Grinnell] [Grinnell] - [Issue Tracker (Course)]
This reading has been significantly rewritten for Fall 2015.
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 with 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 Mediascheme library that support more complex image computations.
We've been exploring how one expresses colors using the RGB representation, as well as an integer encoding of that representation. Once we understand that representation, what can we do next? One obvious thing is to compute new colors from old; given a color, we can often compute a related color (e.g., a darker version, a lighter version, the complement, something with increased red, and so on and so forth).
Why is it useful to be able to transform a color? Because a typical image is just made up of a lot of individual colors. And once you can transform one color, you can transform the whole image. 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: Mediascheme's basic operations for transforming colors and the ways to combine them into more complex color 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 your own color and image transformations.
We will start with a few basic transformations that Mediascheme 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 make 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"
(color-swatch sample)
(color-swatch sample darker-sample)
(color-swatch sample lighter-sample)
(color-swatch 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"
(color-swatch sample (irgb-redder sample))
(color-swatch sample (irgb-greener sample))
(color sample (irgb-bluer sample))
As the examples suggest, for some people, making a color slightly
redder, greener, or bluer can be 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.)
(color-swatch sample (irgb-redder (irgb-redder sample)))
(color-swatch sample (irgb-greener (irgb-greener sample)))
(color-swatch 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, Mediascheme 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"
(color-swatch sample (irgb-rotate sample))
(color-swatch sample (irgb-phaseshift sample))
(color-swatch sample (irgb-complement sample))
You are probably familiar with a painter's palette, which many painters use to mix colors. You will eventually develop your own ways to mix RGB colors. For now, we'll start with three mechanisms for mixing colors.
(irgb-add irgb1
irgb2) combines two IRGB colors by
adding the values of their individual components. If the sum of any
two components is above 255, it gets capped at 255. As you might
expect, this approach means that adding colors generally gives a
brighter color.
(irgb-average irgb1
irgb2) combines two IRGB colors by
averaging the values of their individual components.
(irgb-subtract irgb1
irgb2) combines two IRGB colors
by subtracting the second from the first. If the difference is below
0, we use 0. irgb-subtract is
often the inverse of irgb-add,
but not always. (You should consider when it's not.)
Here are some examples, in both code and image form.
>(define darkred (irgb 192 0 0))>(define medgrey (irgb 128 128 128))>(irgb->string (irgb-add darkred medgrey))"255/128/128">(irgb->string (irgb-average darkred medgrey))"160/64/64">(irgb->string (irgb-subtract darkred medgrey))"64/0/0">(irgb->string (irgb-subtract medgrey darkred))"0/128/128"
(color-swatch darkred (irgb-add darkred medgrey) medgrey)
(color-swatch darkred (irgb-average darkred medgrey) medgrey)
(color-swatch darkred (irgb-subtract darkred medgrey) medgrey)
(color-swatch medgrey (irgb-subtract medgrey darkred) darkred)
What do we want to do when we want to combine transformations? Say, what if we want to make a color both redder and darker? One option is to simply sequence the transformations, as we did in some examples before.
>(define redder-darker-sample (irgb-redder (irgb-darker sample)))
In most cases, that approach seems to work fairly well. However, if we want to apply the same combination of transformations to multiple colors, we can end up retyping a lot of the same code again and again.
>(define variant1 (irgb-complement (irgb-redder (irgb-redder (irgb-complement color1)))))>(define variant2 (irgb-complement (irgb-redder (irgb-redder (irgb-complement color2)))))>(define variant3 (irgb-complement (irgb-redder (irgb-redder (irgb-complement color3)))))>(define variant4 (irgb-complement (irgb-redder (irgb-redder (irgb-complement color4)))))
What can we do? If you think back to algebra, you might remember the composition operation, o. As we learned it, (f o g)(x) = f(g(x)). (Yes, that's the traditional style of parentheses for mathematics, not what we're now using in Scheme.)
Conveniently, our variant of Scheme includes a similar operation,
which is written as
and often pronounced “compose”. Let's look
at a simple example. In this example, you'll note that
compose means “add 1”
and increment means “multiply
by itself”. Just as we define the result of an expression,
we can similarly define the function created by composing two
functions.
double
>(increment 1)2>(square 2)4>(define is (compose square increment))>(is 1)4>(is 2)9>(define si (compose increment square))>(si 1)2>(si 2)5>(si 3)10
The Scheme function differs from
the mathematical composition operation in that the Scheme version lets
us compose as many operations as we want.
compose
>(define add4 (compose increment increment increment increment))>(define to-the-8th (compose square square square))>(add4 1)5>(add4 11)15>(to-the-8th 2)256>(to-the-8th 10)100000000
Why is this useful? Because it means we can define a new color transformation as a composition of other transformations. So, instead of writing the four very repetitious lines above, we can instead write the following.
>(define irgb-modify (compose irgb-complement irgb-redder irgb-redder irgb-complement))>(define variant1 (irgb-modify color1))>(define variant2 (irgb-modify color2))>(define variant3 (irgb-modify color3))>(define variant4 (irgb-modify color4))
Isn't that much nicer?
What should you take away from what we've just learned? You now
know a few more functions in Mediascheme, particularly functions that
transform colors. You've learned a few functions for combining
colors. You've also learned how to define a few of your own functions
by composing existing functions with .
You're now ready to write a few simple transformations of your own.
compose
For the immediate future, knowing the particular transformations will be helpful. Over the longer term, knowing about composition and the techniques that support composition will be even more helpful.
(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)
(irgb-add
irgb-color-1
irgb-color-2)
irgb-color-1 and
irgb-color-2. If any component sum
is greater than 255, uses 255 for the resulting component.
(irgb-average
irgb-color-1
irgb-color-2)
irgb-color-1 and
irgb-color-2.
(irgb-subtract
irgb-color-1
irgb-color-2)
irgb-color-2 from the corresponding
components of
irgb-color-1. If any component difference
is less than 0, uses 0 for the resulting component.
(compose
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))))).
We will sometimes write compose as
o.
As you may have noted, the expression (irgb-subtract medgrey
darkred) gave a greenish color. Explain why.
In the reading, we considered ways to combine procedures. When we wanted a procedure that took a value, added 1, and then squared it, we wrote something like the following.
>(define is (compose square increment))
Why do you think we didn't (or couldn't) just write the following instruction instead?
>(define is (square increment))
As you think about the components of an algorithm, what roles does composition serve?