|
This is a pre-print extract from the forthcoming O'Reilly book Lisp Outside the Box. Contents are subject to change as the book's production progresses. Feedback is most welcome, either in private by or in public by responding to the blog entry which announced this chapter. Table of Contents In this chapter we’re going to download and install an open-source library and then put it to work. The package I chose for this demonstration was Cyrus Harmon’s “ch-image”: a native Lisp library for representing, processing and manipulating images in a variety of formats. Unlike the libraries of Part III, this one is neither ubiquitous nor high on people’s lists for standardization. What motivates this chapter-long example is:
Ch-image is by no means the only graphics library for Lisp. It has the benefit of being small enough that we can summarise it in under 4000 words. People do cutting-edge image processing in Lisp—see for example the FREEDIUS geospatial and image compression toolkit from http://www.ai.sri.com/software/freedius—and many alternative libraries are available, up to and including a number of OpenGL bindings (for example), which we don’t have space for here. This chapter covers the websites you should visit for locating Lisp libraries. The first part of the chapter will show you how to set about
capturing a library and integrating it into your code: very much
part of the work of a professional programmer. The material which
follows will be useful in setting a context for the rest of Part
Four but it is not essential for understanding later chapters. The
examples here employ a few standard Unix utilities such as
This chapter is peppered with suggestions to “look at the source”, along with some exercises which involve using the inspector or debugger (whether explicitly, or implicitly when the code you’ve written goes wrong). If you’re at all familiar with Emacs you might want to skip ahead and peruse the next chapter, on SLIME (the Superior Lisp Interaction Mode for Emacs), ahead of the end of this one. If you’re doing any form of Lisp development work outside of one of the windowing GUIs then SLIME will make your life a lot easier. I’m going to walk in detail through the tasks which I performed in order to install ch-image on a FreeBSD machine:
Downloadable software is a volatile beast and the specific issues I encounter here may not apply to ch-image even a year from now. Download instructions might have changed; platform dependencies might no longer require the use of an “unofficial” release; for all I know the whole library might have been totally refactored. On a more general level, what follows won’t apply to all Lisp libraries and in some cases it’s altogether more straightforward than this; it all depends very much on how the library was packaged up in the first place. We’ll come back to downloads and installations in Chapter 19, Systems. Having said all that, the general principles described here will remain valid even if the details do not, and as they will apply to many other Lisp libraries too the lesson won’t be wasted. TipIf you try to take the exact steps that I did but find that links printed below have rotted, you’ll find copies of everything I downloaded here:
I’ve posted libraries there in two forms: the original download bundles, and as browsable files in case you’d rather peek at source code without downloading anything. There is no single central repository for Lisp libraries. There are however two good directory sites each listing hundreds of packages for a wide range of applications. When you’re looking for a library you should normally consult both of these.
I found ch-image on the CLiki by selecting the front page link for “Graphics Library interfaces” and then scanning a list of about 40 graphics-related entries until I found one which met my requirements. Two clicks from there brought me through to the project’s home page:
Sometimes a direct search will be all you need. For example, when I needed to add compression to an application I’d been working on I simply googled for “lisp zip library”. The top result in the search was David Lichteblau’s “Common Lisp ZIP library” and I never needed to look any further. I’ve also found that it’s worth keeping half an eye on the metablog “Planet Lisp”
and making notes when people announce releases of libraries which look like they might turn out to be useful to me one day. There is no centralized validation of Lisp libraries, no official stamp of approval. Once you’ve found a candidate library, the next step is to determine whether it meets your requirements. There is no vetting procedure for work posted to any of the sites listed above and it’s up to you to verify that the code you’re proposing to use is fit for purpose and works the way you need it to, both
In the end the only way you’re going to be able to do that is to try it out. In more complex cases—not this one—even basic testing might involve significant porting effort and so any preliminary investigations you can conduct become all the more valuable. (We’ll give an example of such a port in Chapter 29, Talking to C.) Start by checking the library’s dependencies. According to the ch-image website, we need to download a total of six libraries:
The first three are located by following links from the ch-image site. The others can be found instantly by googling on the library names. CautionRead the next section before downloading anything. The next check is whether your platform is supported. While writing this section of the book I was working with version 1.3 of Clozure Common Lisp (CCL) on FreeBSD. The ch-image documentation does not mention hardware or operating systems and so we might assume that the code’s only platform dependencies are on Lisp implementation. We could write to the author and ask for clarification; in any case we’re going to test it all very shortly. The documentation does state that the library was developed and tested on a different Lisp (SBCL) but that it “should run” on other implementations. A note on the main ch-image website labeled “clem and ch-image on non-SBCL lisps”
supplies details and in particular download links which support CCL. So if you’re working through this chapter with SBCL then the links listed above will work, and if like me you’ve got CCL in front of you they’ll need some modification. What follows isn’t rocket science. The two links
and
take us to revision histories hosted under the git version control system (http://git-scm.com/). At the end of each entry is a link labeled “snapshot” whose URL is a tarball in .tar.gz format. The only question is whether to try the most recent revisions or the older but possibly more stable releases (clem 0.4.5 and ch-image 0.4.2). Both of these are timestamped with a somewhat more recent date than the note which pointed us at them. Let’s go for the releases; instead of the links printed above for ch-image and clem we’ll use the release snapshots from the git repositories. The URLs are long and unilluminating so I’ve shortened them:
and
We need to download and unpack our six libraries. A very
effective Unix tool for the download is [ndl@vanity ~/chapter-17/tarballs]$ TipThe TinyURLs above result in files with names as revolting as
the snapshot URLs in the git repositories. Work round this with the
curl -o ch-image_0.4.2.tar.gz http://tinyurl.com/ch-image-0-4-2-tgz Once everything is downloaded, we have six tar files to decompress and unpack. [ndl@vanity ~/chapter-17/tarballs]$ There’s one last check to make before moving on. If you couldn’t locate and hence examine licensing terms before you downloaded the libraries, do so now. (In this case all six libraries use permissive two- or three-clause BSD licenses.) Congratulations! The nasty part is behind us and we can get on with the Lisp. The final task is to compile the source we downloaded and load the result into a running Lisp. Working with system definitions—Lisp’s answer to makefiles—and in particular with ASDF (Another System Definition Facility) is a major subject all on its own and we devote the whole of Chapter 19, Systems to it. So for now let’s just show how to build ch-image and load it into Clozure CL; we’ll come back for explanations later. Note that each of the six library directories contains a file
named after that library and with extension [ndl@vanity ~/chapter-17]$ (You’ll spot that we ended up with about twice as many links as we were expecting here. Some of the download directories contain system definitions for documentation and test suites. These won’t get in our way and so needn’t concern us.) TipThis chapter is unashamedly Unix-flavored. Chapter 19, Systems will discuss the use of ASDF on other platforms. Now we fire up Lisp, tell it we need to use ASDF and where the .asd links are, and set it compiling: [ndl@vanity ~/chapter-17]$ A few seconds and a couple of hundred lines of output later, we’re back at the Lisp prompt. Most of this output consists of compiler warnings. We’ll come back to these in Chapter 28, World Building when we look at World Building. The important thing for now is to note that there aren’t any errors in the report. If you have to restart your Lisp session just repeat the three
steps above ( ExerciseRepeat the idiot mistake I made while researching this project and overlook the advice about how to work with CCL. Download release 0.4.1 of clem (i.e. the wrong version for Clozure CL) from its main website page (or from http://lisp-book.org/3rd-party-libraries/) and try to load it into CCL. Fix the major problem which arises. There’s a hint at the end of the chapter. Don’t spend too long on this if you’re not making progress. Figure 17.1. Random sets of lines: you might try this one for yourself. You’ll need to make calls of the form (ch-image:draw-line image from-j from-i to-j to-i). ![]() This section is the software equivalent of kicking the tires and taking it out for a spin. There is some documentation on the project’s website which is fine for getting started but incomplete. If we’re not sure what a function is planning to do with its arguments then a quick peek in the source might help. And there’s absolutely no harm in trying things out.
(defun simple-circle (outfile)
(let (;; Images are represented by CLOS instances. Make an instance of a
;; class which supports 8-bit RGB images.
(image (make-instance 'ch-image:rgb-888-image
:height 200 :width 200)))
;; FILL-IMAGE sets the background color. The second argument represents
;; an RGB value. Turning red and green fully on and blue off gives us
;; yellow.
(ch-image:fill-image image '(255 255 0))
;; The first two arguments to DRAW-CIRCLE specify its center, then
;; comes its radius and finally a foreground color.
;; WARNING: the ch-image package always specifies co-ordinates from the
;; top-left corner of the image and gives the row before the column.
(ch-image:draw-circle image 100 100 80 '(0 0 255))
;; Write the image to our chosen destination. Note that we've built the
;; image without reference to an output format (.png in the invocation
;; below); the image's internal representation is totally generic.
(values image
(ch-image:write-image-file outfile image))))
?
ExerciseRun this; find some way of viewing the result; adapt the example to make it more fun, or change it altogether (Figure 17.1, “Random sets of lines: you might try this one for yourself. You’ll need to make calls of the form (ch-image:draw-line image from-j from-i to-j to-i).”). TipThe Common Lisp function ExerciseLocate the source for The external image formats supported by ch-image are:
Two I/O functions are provided:
?
Both functions use the file’s Internally there are several different image formats to choose between including 8 and 16 bits-per-channel RGB and ARGB (that’s RGB plus an “alpha” channel which specifies opacity) and 8, 16 and 32 bits-per-channel greyscale. The internal format of an image corresponds to its class, in particular:
Figure 17.2. Kings College Chapel, Cambridge. An RGB image split by ch-image into its constituent channels. Clockwise from top left: red, green, blue. Lower left panel is greyscale generated from the RGB using the utility ch-image:argb-image-to-gray-image. ![]() An image’s channels are objects which you can manipulate directly, for example (see also Figure 17.2, “Kings College Chapel, Cambridge. An RGB image split by ch-image into its constituent channels. Clockwise from top left: red, green, blue. Lower left panel is greyscale generated from the RGB using the utility ch-image:argb-image-to-gray-image.”):
(defun read-call-write (function infile)
(let* ((in-image (ch-image:read-image-file infile))
(out-image (funcall function in-image))
(outfile (make-pathname :name (format nil "~a-blue"
(pathname-name infile))
:defaults infile)))
(values out-image
(ch-image:write-image-file outfile out-image))))
(defun extract-blue-channel (original)
(let* ((width (ch-image:image-width original))
(height (ch-image:image-height original))
(blue-only (make-instance 'ch-image:ub8-matrix-image
:width width :height height)))
;; GET-CHANNELS and SET-CHANNELS work with a list of an image's
;; channels. IMAGE-R, IMAGE-G, IMAGE-B are individual accessors for the
;; three color channels.
(ch-image:set-channels blue-only (list (ch-image:image-b original)))
blue-only))
ExerciseWrite an extension to this function which extracts all three color channels as separate outputs. ExerciseUse either the SLIME inspector or The pixel values of each channel are numbers; for 8-bit images they’re integers in the range 0-255 inclusive. You can also access pixel values without reference to channels. For a multi-channel image you’ll get a list of numbers, for greyscale you get the number itself. Read and set pixel values thus: ? ExerciseThe first call to We’ve already met the function for drawing circles. Ch-image supplies utilities for:
ExerciseVisit ch-image’s API documentation by following the link from the project webpage. Pick a drawing utility from first half of the above list and flick through the documentation to locate the function which supports it. If you find the results insufficiently informative, locate the source for this function and check what it does with its arguments. Get a call to this utility to work. Now repeat the experience with one of the higher-level tasks from the second half of the list. At a higher level still, ch-image supports gamma correction, Gaussian blur, and image sharpening. Let’s conclude our brief tour of ch-image with the full source of a small application for taking a perfectly respectable image and mangling it beyond recognition by applying a number of spiral deformations to it. We define five functions. The devil is in the details and we
comment on these inline. Note the inevitably repetitive nature of
image processing code: it’s hard to avoid saying everything
twice (or, in ExerciseLook ahead to the listing for CautionRemember that, somewhat unusually, the ch-image package always specifies the row before the column. For consistency with the library we’re using, we choose to follow that convention here and pass “j” values (verticals) before “i” values (horizontals).
;; Top-level utility. Applies a (small, random) number of spiral
;; deformations to the source image, each with randomly chosen
;; "strength" and centered about a random location. Sets each pixel in
;; the destination image by determining a source location for that
;; pixel and copying values from there. Returns deformed image.
(defun spiralize (source)
(let* ((destination (ch-image:copy-image source))
(width (ch-image:image-width source))
(height (ch-image:image-height source))
(max-i (- width 2))
(max-j (- height 2)))
;; Loop with "random" parameters.
(loop repeat (+ 3 (random 3)) do
(let ((center-i (+ (round width 4) (random (round width 2))))
(center-j (+ (round height 4) (random (round height 2))))
(strength (* (+ 50.0 (random 100.0))
(if (zerop (random 2)) +1 -1)))
;; Make temporary copy of image.
(temp (ch-image:copy-image destination)))
;; Crawl across destination image...
(dotimes (destination-j height)
(dotimes (destination-i width)
;; ... choosing a source pixel to copy into each
;; destination pixel ...
(multiple-value-bind (source-j source-i)
(source-coordinates destination-j destination-i
center-j center-i strength)
;; ... and copy that pixel from the temporary source.
(copy-pixel temp
(max 0 (min source-j max-j))
(max 0 (min source-i max-i))
destination destination-j destination-i))))))
destination))
;; Returns result of rotating co-ordinates (i, j) about given center
;; through angle proportional to strength and inversely proportional
;; to the distance between (i, j) and the center. New co-ordinates
;; need not be integers.
(defun source-coordinates (j i center-j center-i strength)
(let* ((diff-i (- i center-i))
(diff-j (- j center-j))
(distance-squared (+ (* diff-i diff-i)
(* diff-j diff-j))))
(if (zerop distance-squared)
;; Avoid dividing by zero.
(values j i)
(let* ((distance (sqrt distance-squared))
(angle-to-rotate-by (/ strength distance)))
(rotate j i angle-to-rotate-by center-j center-i)))))
;; Utility which rotates the point (i, j) about (center-i, center-j) by
;; theta radians.
(defun rotate (j i theta center-j center-i)
(let* ((sin (sin theta))
(cos (cos theta))
(old-diff-i (- i center-i))
(old-diff-j (- j center-j))
(new-diff-i (- (* old-diff-i cos)
(* old-diff-j sin)))
(new-diff-j (+ (* old-diff-j cos)
(* old-diff-i sin))))
(values (+ center-j new-diff-j)
(+ center-i new-diff-i))))
;; Utility for copying ch-image pixels. It iterates over channels and is
;; written to work equally with multi-channel images and greyscale.
(defun copy-pixel (source source-j source-i
destination destination-j destination-i)
(loop for source-channel in (ch-image:get-channels source)
as destination-channel in (ch-image:get-channels destination)
do
(let ((new-value (interpolate-pixel-values source-channel
source-j source-i)))
(ch-image:set-pixel destination-channel
destination-j destination-i
new-value))))
;; Workhorse for averaging pixel values. We use bilinear interpolation,
;; exactly as in the clem package (see macro bilinear-interpolate in
;; "clem/src/interpolation.lisp"), to average values from four surrounding
;; source pixels.
(defun interpolate-pixel-values (source source-j source-i)
(multiple-value-bind (base-i fraction-i)
(floor source-i)
(multiple-value-bind (base-j fraction-j)
(floor source-j)
(let ((pixel-00 (ch-image:get-pixel source base-j base-i))
(pixel-01 (ch-image:get-pixel source base-j (1+ base-i)))
(pixel-10 (ch-image:get-pixel source (1+ base-j) base-i))
(pixel-11 (ch-image:get-pixel source (1+ base-j) (1+ base-i))))
(round ;; Apply "bilinear interpolation": the average of pixel
;; values is weighted according to how close we are to to
;; the location of each pixel.
(+ pixel-00
(* fraction-i (- pixel-01 pixel-00))
(* fraction-j (- pixel-10 pixel-00))
(* fraction-i fraction-j (- (+ pixel-00 pixel-11)
(+ pixel-01 pixel-10)))))))))
ExerciseWhy do we make a fresh copy of the image ExerciseThe values ExerciseWhat caused the stripy effect round the edges of Figure 17.3, “The “Old Schools” building, Cambridge, distorted by spiralize.”. ExerciseEstimate how much memory is occupied by the internal
representation of an image. Then see if this can be tallied to what
ExerciseRewrite this example to perform some totally different mangling
operation on the source image. You’ll have to modify
Hint for earlier exercise: the problem is in a package definition; once you’ve fixed that you need to make one other minor change to compensate; in total two one-line changes. |