Functional Problem Solving (CSC 151 2014F) : Readings

Basic Image Operations


Summary: We explore the basic operations for creating and saving images.

Introduction

The focus of this course is image making and manipulation. That means that something like “images” will be one of the basic data types we explore. In the course, when we say “image”, we essentially mean “an image that Gimp knows how to work with”. Right now, we know only two procedures for dealing with images. image-show shows an existing image and drawing->image takes a drawing and creates a new image. But what if we want to do more? In this reading, we explore some of the other basic image operations.

Some Basic Questions About Images

Each time you encounter a new type in Scheme (or any programming langauge), you should familiarize yourself with a variety of characteristics of the type. Here are some questions we tend to ask ourselves when we encounter a new type, along with some answers for images?

  • What purpose does the type serve? The image type provides a way to refer to the images that Gimp can manipulate. It allows us to talk to Gimp, and to work with the world beyond Racket.
  • How does Scheme represent the type? Scheme represents images as integers. (Behind the scenes, these integers are indices into a table of more detailed information about the image.)
  • How do we create new values in the type? We've already seen one way to create images, using drawing->image. We'll soon see that you can create a new, “blank” image and that you can load an image from a file. We'll discover other ways to create images throughout the semester.
  • What can we do with values in the type? Right now, we know only one thing: We can show them with image-show. But we can also save them to files, transform them in a variety of ways, and even render new drawings on top of them.

As we explore new types, we also often write programs to help us better understand the type.

Primary Image Operations

Showing images with image-show

As you saw in the early explorations of the drawing type, drawings are conceptual. They need to be rendered. Some of us can think well in the conceptual space, but most of us need to be able to see the images we create. The image-show procedure lets us see images.

Recently, you may have learned about procedures with “side effects” - procedures that we call not to compute a value, but to change something. Note that image-show is called primarily for its side effect - it adds a window onto the screen. The image is there already, and we don't change the image, but we do change the state of our computer system.

Note that image-show introduces a potentially problematic relationship with Gimp. If you show an image (or, more precisely, if Racket tells Gimp to show an image), then Gimp has some power over the image. In particular if you close an image in Gimp, you may not be able to access it any more in Scheme. For example,

> (define canvas (drawing->image my-masterpiece 400 300))
> canvas
1
> (image-show canvas)
1
; A window showing our masterpiece appears.
> (image-show canvas)
1
; Another window showing our masterpiece appears.
: We close the second window.
> (image-show canvas)
1
; Yet another window showing our masterpiece appears.
; We close both windows.
> (image-show canvas)
image-show: expects type <image> for 1st parameter, given 1 in (image-show 1)

Creating new images with image-new

We've seen one way to create images - we can use drawing->image. But what if we want a blank image? (Why would we want a blank image? We'll soon learn some ways to manipulate blank images.) One option is to use drawing->image, but with a really small white drawing. But that seems a but of a kludge. Fortunately, there is a image-new procedure. image-new takes two parameters, a width and a height, and creates a blank image of the specified size. That image is not yet shown. You will still have to show it with image-show.

Are there other reasons to use image-new rather than just just drawing->image. There's a subtle one. drawing->image always creates the drawing on a white canvas. In contrast, image-new creates an image the same color as the current Gimp background color. We can simulate this effect by using a large colored rectangle, but sometimes it's easier to just have a procedure that does what we want.

Rendering drawings with drawing-render!

Right now, we know that if we have a drawing, we can easily convert it to an image with drawing->image. But what if we want to add a drawing to an existing image - whether an image created by image-new (which can be a different color than white), an image created by an earlier to call to drawing->image, or an image created in another way? The procedure (drawing-render! drawing image) renders a drawing on an existing image. This procedure does not create a new drawing. It does not modify the drawing. But it modifies the image by adding to it.

Scheme programmers have some conventions to remind ourselves about certain kinds of side effects. In particular, when we write a procedure that modifies one of its parameters, we usually end its name with an exclamation point. In this case, to reminder ourselves that drawing-render! modifies the image, we include an exclamation point in its name. (Yes, it's not always obvious what parameter is being modified. That's one reason we also document procedures.)

Since you are likely to want to use the image (e.g., to render something else, to show it, or to save it), drawing-render! returns the image.

Loading existing images with image-load

Up to now, we've created all of our drawings “from scratch” (or at least from unit squares and circles). But we may want to work with existing images. You can make some images in Gimp, and you can load images in Gimp. But we want to be able to work programmatically. Hence, Mediascheme includes an image-load function. image-load takes one parameter, a string that gives the full path to an image file, loads that file, and returns an integer that we can use to identify the image.

Does image-load have side effects? That's debatable. It doesn't change the underlying file. But it does add an image to our environment. In that sense, it's much like drawing->image. Hence, we do not end its name with an exclamation point.

Saving created images with image-save

It's useful to be able to load images so that we can manipulate them. It's even more useful to be able to save images that we've created. The procedure (image-save image filename) saves an image to a specified file. You should provide the full path name to the file, surrounding it by quotes. For example, you might use something like "/home/student/images/masterpiece.png". The suffix you give to the file name determines the type of file that is saved - jpg, gif, png, and so forth.

Note that you can use image-save with any image we make, whether it's created with drawing->image, image-new, image-load, or anything else we learn.

Getting information on an image with image-width and image-height

We're almost done with the image procedures. All that's left is to be able to get some basic information on an image. The most basic information we might want is the width and height of the image, which we can get with (image-width image) and (image-height image).

Are there others? You may recall that for drawings, we were able to get their left edge, their top edge, their width, their height, their type (ellipse, rectangle, or group), and, sometimes, their color. It doesn't make sense to ask for the top and left edge of an image. Those are always 0. It's not immediately clear what the “type” of an image is. And typical images have many colors, so we won't ask about the overall color of the image. (However, we'll soon be able to ask about the color at individual points in the image.) It looks like width and height will suffice, at least for now.

An Example: Rescaling a Drawing

How might we use all of these procedures? Here's a simple example: We'll take a drawing and rescale it to fit on the image. (In the lab, you'll have an opportunity to think about shifting the drawing, too.)

So, suppose we have a drawing that's w units wide and h units high, and we want to make it fit onto an image that's W units wide and H units high. What should we do? A bit of simple reflection and some math suggests that we should scale the width by W/w, and the height by H/h.

How do we get those four values? With drawing-width, drawing-height, image-width, and image-height.

;;; Procedure:
;;;   scare! (SCale And REnder)
;;; Parameters:
;;;   drawing, a drawing
;;;   image, an image
;;; Purpose:
;;;   scale the drawing to the size of the image and then render it
;;;   on the image
;;; Produces:
;;;   image, the updated image
;;; Preconditions:
;;;   drawing and image both have a positive width and height
;;; Postconditions:
;;;   An appropriately scaled version of drawing appears on image
(define scare!
  (lambda (drawing image)
    (drawing-render!
     (hscale-drawing (/ (image-width image) (drawing-width drawing))
      (vscale-drawing (/ (image-height image) (drawing-height drawing))
       drawing))
     image)))

Another Example: Modifying a Stored Image

The scare! procedure let us explore three of the new image functions: drawing-render!, drawing-width, and drawing-height. What about the file operations?

Here are a collection of procedures that, when taken together, add a small icon to the bottom-right corner of an existing image and save it in a new file.

;;; Procedure:
;;;   claim
;;; Parameters:
;;;   source, a string that names an existing image file
;;;   target, a string that names an image file 
;;; Purpose:
;;;   Add a little "ownership" symbol to the image in source, 
;;;   saving the result in target.
;;; Produces:
;;;   The name of the target file.
;;; Preconditions:
;;;   You have permission to access the source file (in the computer sense,
;;;     the legal sense, and the ethical sense).
;;;   You have permission to write the target file.
;;;   The target file does not yet exist.
;;; Postconditions:
;;;   target now exists.
;;;   target contains an image much like source.
;;;   target contains a small icon to distinguish itself from source.
(define claim
  (lambda (source target)
    (image-save (image-claim! (image-load source)) target)))

;;; Procedure:
;;;   image-claim!
;;; Parameters:
;;;   source, an image
;;; Purpose:
;;;   Add a little "ownership" symbol to source.
;;; Produces:
;;;   source, the same image, now modified
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   source now contains a small icon to distinguish itself from 
;;;   the previous version.
(define image-claim!
  (lambda (source)
    (drawing-render!
     (hshift-drawing 
      (- (image-width source) 5)
      (vshift-drawing
       (- (image-height source) 3)
       claim-icon))
     source)))

;;; Name:
;;;   claim-icon
;;; Type:
;;;   drawing
;;; Contents:
;;;   A little icon we use to "claim" images.
(define claim-icon
  (drawing-group
   (scale-drawing 50 (recolor-drawing "red" drawing-unit-circle))
   (scale-drawing 40 drawing-unit-circle)
   (scale-drawing 30 (recolor-drawing "red" drawing-unit-circle))
   (scale-drawing 25 drawing-unit-circle)))

Note that claim! takes advantage of the way Scheme evaluates expressions. Since the image-load call is innermost, it evaluates that first. Hence, the

    (image-save (image-claim! (image-load source)) target)))

gets interpreted as “First load the source image, then claim the image using image-claim!, then save it in the target file.

Self Checks

Check 1: Image vs. Drawing

The reading gives you four questions you should ask when learning a new type, but they are also useful for comparing and reviewing types.

Compare and contrast the answers to the four questions for an image type with the answers you would give for the drawing type.