Primary: [Front Door] [Syllabus] [Assignments] [Labs] [Readings]
References: [A-Z] [Primary] [Scheme Report (R5RS)] [Scheme Reference]
Related Courses: [CSC151 2007S (Rebelsky)] [CSC151 2008S (Davis)] | [CSC151 2008F (Davis)] [CSC151 2008F (Weinman)] | [CSC151 2009S (Davis)] [CSC151 2009S (Weinman)]
Summary: We consider ways to store a representation of an image in a file. In particular, we consider how to store an image by writing each pixel, in turn, to the a file, and how to restore such an image from a file. Along the way, we revisit the issue of how to represent certain kinds of values.
For much of this semester, we've been looking at how one creates
or modifies existing images. But what happens after an image is
created? Typically, people save their images to disk to look at them
later, to update them later, or to share them with others.
Right now, we treat saving and loading images as primitive
operations. You load an image with image-load
.
You save an image using the menu
item in The GIMP or with (
.
image-save
image
filename
)
Of course, these procedures obscure the underlying file representation of images. Now that you know how to write files, you can also write your own procedures that save and restore images in a representation you design. Doing so requires that you think about how to store colors and how to map the pixels in an image into colors (yes, there is an obvious answer), and that you can restore images from whatever form you've chosen.
Let's start with the core operation necessary for writing images:
writing and reading individual colors. Writing a color should be
fairly straightforward: We just use the write
procedure to write the color value. We could then use the
read
function to restore the color. Let's
try that.
>
(define out (open-output-file "colors.txt"))
>
(write (rgb-new 255 0 255) out)
>
(close-output-port out)
What's in that file? We can look by opening it in MediaScript, by
opening it in a text editor, or with some of the simple input functions,
such as read
.
16711935
Since the file looks okay, we can try reading the color back from the file.
>
(define in (open-input-file "colors.txt"))
>
(define color (read in))
>
(close-input-port in)
>
(rgb->string color)
"255/0/255"
Things look like they're working fine, so let's try something a little bit more complex. Let's write two colors to the file.
>
(delete-file "colors.txt")
>
(define out (open-output-file "colors.txt"))
>
(write (rgb-new 0 0 255) out)
>
(write (rgb-new 255 0 255) out)
>
(close-output-port out)
Okay, what does the file look like now? Let's see.
25516711935
That's a bit odd, isn't it? We see the 255
, which seems to
represent
blue, and we see the 16711935
, which seems to represent purple,
but there's nothing to designate where the first color ends and the second
color begins. This could just as easily be 2551 and 6711935 or 255167 and
11935 or .... So, what does our Scheme interpreter think it is?
>
(define in (open-input-file "colors.txt"))
>
(define color (read in))
>
color
25516711935>
(rgb->string color)
rgb->string: expects type <rgb> for 1st parameter, given 25516711935 in (rgb->string 25516711935)>
(define color (read in))
>
color
#<eof>
>
(close-input-port in)
No, that doesn't look very good, does it? We've combined two colors into one thing, but that thing isn't even a color. That's certainly not going to be a useful technique if we want to restore the original colors in our image. However, the solution is simple: We just put a space or carriage return between colors.
>
(delete-file "colors.txt")
>
(define out (open-output-file "colors.txt"))
>
(write (rgb-new 0 0 255) out)
>
(newline out)
>
(write (rgb-new 255 0 255) out)
>
(newline out)
>
(close-output-port out)
Now, what does the file look like?
255 16711935
That's much better. Now, let's make sure that we can read the values back from the file.
>
(define in (open-input-file "colors.txt"))
>
(define color (read in))
>
color
255
>
(rgb->string color)
"0/0/255"
>
(define color (read in))
>
color
16711935
>
(rgb->string color)
>
(define color (read in))
>
color
<eof>
>
(close-input-port in)
Okay, that's much better. Let's now encapsulate those ideas into two procedures, one to write colors and one to read colors. We'll start with the one that writes colors.
;;; Procedure: ;;; rgb-write ;;; Parameters: ;;; color, an rgb color ;;; port, the port to which to write the color ;;; Purpose: ;;; Write the color to the specified port ;;; Preconditions: ;;; port is open for writing. ;;; Postconditions: ;;; When read with rgb-read, the data just appended to the file ;;; will give color. (define rgb-write (lambda (color port) (write color port) (newline port)))
Let's see what happens when we write a few colors to a file.
>
(delete-file "colors.txt")
>
(define colors (open-output-file "colors.txt"))
>
(rgb-write (color->rgb "aquamarine") colors)
>
(rgb-write (color->rgb "gold") colors)
>
(rgb-write (color->rgb "firebrick") colors)
>
(rgb-write (color->rgb "darkkhaki") colors)
>
(rgb-write (color->rgb "lightseagreen") colors)
>
(rgb-write (color->rgb "teal") colors)
>
(rgb-write (color->rgb "bisque") colors)
>
(close-output-port colors)
Okay, what does the file look like?
8388564 16766720 11674146 12433259 2142890 32896 16770244
That's not very readable, is it? Arguably, it doesn't need to be,
as long as the computer can read it back. And the computer can read it
back. Here's a procedure that does just that. Since we've written with
write
, we can simply read the value back directly
with read
. For uniformity (and so we can change
the implementation later), we write a separate
rgb-read
function.
;;; Procedure: ;;; rgb-read ;;; Parameters: ;;; port, the name of an input port ;;; Purpose: ;;; Reads an RGB color from the file. ;;; Produces: ;;; color, an RGB color (or <eof>, if the port is at the end of ;;; the file) ;;; Preconditions: ;;; port is open for reading. ;;; port is at the place in a file after which the next data was ;;; written by rgb-write. ;;; Postconditions: ;;; color is the color written by the call to rgb-write mentioned ;;; in the preconditions. (define rgb-read (lambda (port) (read port)))
Now, let's try reading back from the file.
>
(define colors (open-input-file "colors.txt"))
>
(define color (rgb-read colors))
>
color
8388564
>
(rgb->string color)
"127/255/212"
>
(rgb->color-name color)
"aquamarine"
>
(define color (rgb-read colors))
>
color
16766720
>
(rgb->string color)
"255/215/0"
>
(rgb->color-name color)
"gold"
>
(define color (rgb-read colors))
>
color
11674146
>
(rgb->string color)
"178/34/34"
>
(rgb->color-name color)
"firebrick"
>
(define color (rgb-read colors))
>
color
12433259
>
(rgb->string color)
"189/183/107"
>
(rgb->color-name color)
"darkkhaki"
>
(define color (rgb-read colors))
>
color
2142890
>
(rgb->string color)
"32/178/170"
>
(rgb->color-name color)
"lightseagreen"
>
(define color (rgb-read colors))
>
color
32896
>
(rgb->string color)
"0/128/128"
>
(rgb->color-name color)
"teal"
>
(define color (rgb-read colors))
>
color
16770244
>
(rgb->string color)
"255/228/196"
>
(rgb->color-name color)
"bisque"
>
(define color (rgb-read colors))
>
color
#<eof>
>
(close-input-port colors)
As the preceding suggests, we can certainly print the
values from the file using rgb->string
and
rgb->color-name
. But what if we wanted the file
to be readable by both human and computer? If our goal is to make
things readable, we should write colors in a way that at least some
humans can read them. For example, we can instead write the red,
green, and blue components, separated by spaces.
(define rgb-write (lambda (color port) (write (rgb-red color) port) (display " " port) (write (rgb-green color) port) (display " " port) (write (rgb-blue color) port) (newline port)))
The output file produced from this procedure is clearly much more readable.
127 255 212 255 215 0 178 34 34 189 183 107 32 178 170 0 128 128 255 228 196
Of course, we now need a slightly more complicated mechanism for
reading. In particular, we need to read each component separately,
and then combine them together. The following procedure does just that.
(See, there's a reason we wrote and used rgb-read
above,
rather than just using read
.)
(define rgb-read (lambda (port) (let* ((red (read port)) (green (read port)) (blue (read port))) (rgb-new red green blue))))
However, there is a bug in this procedure. Can you tell what it is? We'll give you a minute to think about it.
The problem is that we have no way to tell when we've reached the end of the file.
Now what should we do? We should check to make sure that none of
the components is the eof object. Fortunately, because of the
design of read
, if red
is the
eof object, then so are green
and blue
.
Similarly, if green
is the eof object, then so is
blue
. Hence, we only need to check if blue
is the eof object. What should we do if it is? Well, the standard
is that procedures that read values from files return the eof object
at the end of file, so we will also return the eof
object (conveniently stored in blue
).
(define rgb-read (lambda (port) (let* ((red (read port)) (green (read port)) (blue (read port))) (if (eof-object? blue) blue (rgb-new red green blue)))))
We now know how to write colors, but what about whole images? Let's start with a four-by-three image. What should we do? Write the first row, then the second row, and then the third row.
>
(define stored-image (open-output-file "small-image.pixmap"))
>
(rgb-write (image-get-pixel canvas 0 0) stored-image)
>
(rgb-write (image-get-pixel canvas 1 0) stored-image)
>
(rgb-write (image-get-pixel canvas 2 0) stored-image)
>
(rgb-write (image-get-pixel canvas 3 0) stored-image)
>
(rgb-write (image-get-pixel canvas 0 1) stored-image)
>
(rgb-write (image-get-pixel canvas 1 1) stored-image)
>
(rgb-write (image-get-pixel canvas 2 1) stored-image)
>
(rgb-write (image-get-pixel canvas 3 1) stored-image)
>
(rgb-write (image-get-pixel canvas 0 2) stored-image)
>
(rgb-write (image-get-pixel canvas 1 2) stored-image)
>
(rgb-write (image-get-pixel canvas 2 2) stored-image)
>
(rgb-write (image-get-pixel canvas 3 2) stored-image)
Of course, this works only for a four-by-three image. As has been our practice throughout this course, we should think about how to generalize what we just did, so that (a) we can avoid writing repetitious code and (b) we can write code that will work for different sizes of images. What happened in the preceding? We wrote the first row of the image and then we wrote the second row. How did we know when we were done with the first row? When we'd written the last column in that row. What did we do next? We added 1 to the row and reset the column to 0. How did we know that we were done with the image? When we'd finished the last row.
In effect, we need to recurse over two values, the current column and the current row. As we just noted, most of the time we just increment the column. When we reach the end of a row (when the column is greater than or equal to the width), we move on to the next row (incrementing the row and resetting the column).
We'll also find it useful to name the width and height
of the image and to name the open port. We can do all three with a
let
.
Putting it all together, we get
;;; Procedure: ;;; image-write-pixmap ;;; Parameters: ;;; image, an image ;;; filename, a string ;;; Purpose: ;;; Writes the pixmap information on image to the file. ;;; Produces: ;;; [Nothing; Called for the side effect.] ;;; Preconditions: ;;; filename is a valid file name. ;;; Postconditions: ;;; The file named by filename now contains a sequence of integers, ;;; one for each RGB color in image. (define image-write-pixmap (lambda (image filename) (let ((width (image-width image)) (height (image-height image)) (port (open-output-file filename))) (let kernel ((col 0) (row 0)) (cond ((>= row height) (close-output-port port) image) ((>= col width) (kernel 0 (+ row 1))) (else (rgb-write (image-get-pixel image col row) port) (kernel (+ col 1) row)))))))
We've saved all of the pixels from the image into a file. Now we need a way to read them back. Once again, let's start by writing code that does each pixel by hand. That is, we'll read a color and then set the appropriate pixel to that color. How do we know what pixel to set? We set them in the same order that we wrote them (we do them a row at a time, traversing each row from left to right).
>
(define picture (open-input-file "small-image.pixmap"))
>
(image-set-pixel! canvas 0 0 (rgb-read picture))
>
(image-set-pixel! canvas 1 0 (rgb-read picture))
>
(image-set-pixel! canvas 2 0 (rgb-read picture))
>
(image-set-pixel! canvas 3 0 (rgb-read picture))
>
(image-set-pixel! canvas 0 1 (rgb-read picture))
>
(image-set-pixel! canvas 1 1 (rgb-read picture))
>
(image-set-pixel! canvas 2 1 (rgb-read picture))
>
(image-set-pixel! canvas 3 1 (rgb-read picture))
>
(image-set-pixel! canvas 0 2 (rgb-read picture))
>
(image-set-pixel! canvas 1 2 (rgb-read picture))
>
(image-set-pixel! canvas 2 2 (rgb-read picture))
>
(image-set-pixel! canvas 3 2 (rgb-read picture))
>
(close-input-port picture)
Of course, nothing about the file tells us the size of the image. Hence, we could just as easily read the file back into a 6x2 image.
>
(define picture (open-input-file "small-image.pixmap"))
>
(image-set-pixel! canvas 0 0 (rgb-read picture))
>
(image-set-pixel! canvas 1 0 (rgb-read picture))
>
(image-set-pixel! canvas 2 0 (rgb-read picture))
>
(image-set-pixel! canvas 3 0 (rgb-read picture))
>
(image-set-pixel! canvas 4 0 (rgb-read picture))
>
(image-set-pixel! canvas 5 0 (rgb-read picture))
>
(image-set-pixel! canvas 0 1 (rgb-read picture))
>
(image-set-pixel! canvas 1 1 (rgb-read picture))
>
(image-set-pixel! canvas 2 1 (rgb-read picture))
>
(image-set-pixel! canvas 3 1 (rgb-read picture))
>
(image-set-pixel! canvas 4 1 (rgb-read picture))
>
(image-set-pixel! canvas 5 1 (rgb-read picture))
>
(close-input-port picture)
Is it a problem that we can read an image into a different shape of image? Let's say “No” for right now, since it lets us get some interesting effects by reading images back from files into a different region. (You'll try doing so on the lab.) We could also choose to write the size of the image to the beginning of the file.
We are now ready to generalize. As in the case of reading pixmaps, we need to open ports, determine width and height, and then read one color at a time.
;;; Procedure: ;;; image-read-pixmap! ;;; Parameters: ;;; image, an image ;;; filename, a string ;;; Purpose: ;;; Read pixmap data from the specified file, storing the results ;;; into image. ;;; Produces: ;;; [Nothing; called for the side effect] ;;; Preconditions: ;;; filename names a file. ;;; That file was created by image-write-pixmap. ;;; That file contains (image-width image)x(image-height image) ;;; colors. ;;; Postconditions: ;;; image has been updated to contain the image described by file. (define image-read-pixmap! (lambda (image filename) (let ((width (image-width image)) (height (image-height image)) (port (open-input-file filename))) (let kernel ((col 0) (row 0)) (cond ((>= row height) (let ((next-color (rgb-read port))) (close-input-port port) (if (not (eof-object? next-color)) (throw "image-read-pixmap!: Data remain in file after image was competely read"))) image) ((>= col width) (kernel 0 (+ row 1))) (else (let ((next-color (rgb-read port))) (cond ((eof-object? next-color) (close-input-port port) (throw "image-read-pixmap!: Premature end of file.")) (else (image-set-pixel! image col row next-color) (kernel (+ col 1) row))))))))))
You'll note that we've added a bit of error checking here. If the file is the wrong size (too few colors or too many colors), it still updates the image. In the lab, we consider how to ensure that the image has exactly the same dimensions.
If you try the procedures above, you'll find that they're a bit pokey.
We can speed them up with two helpful image iteration procedures,
image-scan
and
image-calculate-pixels!
image-scan
scans through the image,
row by row, and applies a function to the column, row, and color for
each pixel. image-calculate-pixels!
also scans
through the image, this time setting the pixel at each position to
the result of applying a function to the column and row of the pixel.
(In contrast to image-compute-pixels!
, which
can visit the pixels in any order it finds convenient,
image-calculate-pixels!
is guaranteed to do a
row-by-row scan.
;;; Procedure: ;;; image-write-pixmap ;;; Parameters: ;;; image, an image ;;; filename, a string ;;; Purpose: ;;; Writes the pixmap information on image to the file. ;;; Produces: ;;; [Nothing; Called for the side effect.] ;;; Preconditions: ;;; filename is a valid file name. ;;; Postconditions: ;;; The file named by filename now contains a sequence of integers, ;;; one for each RGB color in image. (define image-write-pixmap (lambda (image filename) (let ((width (image-width image)) (height (image-height image)) (port (open-output-file filename))) (int-write width port) (int-write height port) (image-scan image (lambda (col row color) (rgb-write color port))) (close-output-port port) image))) ;;; Procedure: ;;; image-read-pixmap- ;;; Parameters: ;;; filename, a string ;;; Purpose: ;;; Read pixmap data from the specified file, returning a new ;;; image from that data. ;;; Produces: ;;; image, an image. ;;; Preconditions: ;;; filename names a file. ;;; That file was created by image-write-pixmap. ;;; Postconditions: ;;; image contains the same colors in the same positions as the image ;;; previously written with image-write-pixmap. (define image-read-pixmap (lambda (filename) (let* ((port (open-input-file filename)) (width (int-read port)) (height (int-read port)) (image (image-new width height))) ; Read the pixel data. (image-calculate-pixels! image (lambda (col row) (let ((next-color (rgb-read port))) (cond ((eof-object? next-color) (close-input-port port) (error "image-read-pixmap!: Premature end of file.")) (else next-color))))) ; When the image is full, clean up the file. (let ((next-color (rgb-read port))) (close-input-port port) (if (not (eof-object? next-color)) (error "image-read-pixmap!: Data remain in file after image was competely read"))) ; Return the new image. image)))
You'll note that image-write-pixmap
calls
rgb-write
rather than using instructions to
write the components directly. Similarly,
image-read-pixmap
uses rgb-read
rather than three calls to read
to get the
three components.
Why did we make that choice? Since we're still thinking about how we
store images, it makes it much easier to change our
minds. If we come up with another way to store colors (and we will),
rather than changing all the procedures that write and read colors
(such as image-read-pixmap
),
we need only change two function: rgb-write
and rgb-read
.
More generally, when you design a new data type or a new use of an old data type, it is helpful to encapsulate the algorithms you want into a library of procedures, and to only use those procedures to directly manipulate the data. We have used this technique before (e.g., for spots) and will use it again. While it may be tempting to just use the body of one of these library procedures in your code (as many of you did for spots), by using the library procedures you make it much easier to update your design.
Primary: [Front Door] [Syllabus] [Assignments] [Labs] [Readings]
References: [A-Z] [Primary] [Scheme Report (R5RS)] [Scheme Reference]
Related Courses: [CSC151 2007S (Rebelsky)] [CSC151 2008S (Davis)] | [CSC151 2008F (Davis)] [CSC151 2008F (Weinman)] | [CSC151 2009S (Davis)] [CSC151 2009S (Weinman)]
Copyright (c) 2007-2009 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.