[nolan@nprescott.com] $  cat weblog archive feed

Pictures in Racket

2019-05-25

I've been reading about generative art and image processing and thought it would be fun to try defining a few fundamental image filters from scratch. Racket has excellent support for images, especially in Dr Racket, so why not combine them both?

Why Racket

Racket is capable of dealing with the kind of image basics that I'm interested in at a high-level, out of the box. I am cheating just a bit here by using 2htdp (that's 2nd edition How to Design Programs), which is a "teach pack" for racket to introduce programming basics. If I ever progress to full lisp weenie I should probably use racket/draw instead, which is what is happening under the covers.

#lang racket
(require 2htdp/image)
(define sample-image (bitmap/file "stairs.jpg"))

This provides an array of functionality like lists of pixels in the image, built-in support for dealing in RGB, and a REPL that can display images.

Greyscale

I think an interesting effect to recreate as a very basic test of the whole system will be a "black and white" filter. Here I'm using a freely available image from Matt Seymour (resized down to make image processing more manageable):

a series of stairs

One thing I learned previously, when I was exploring colors on the web, is that one measure of "lightness" for a given pixel (as a gross simplification) is termed relative luminance and has a straight-forward definition:

(define (luminance rgb)
  (exact-round (+ (* 0.21 (color-red rgb))
                  (* 0.72 (color-green rgb))
                  (* 0.07 (color-blue rgb)))))

With this "lightness" value for a given pixel, a specific grey is then possible by making the RGB values triples of the luminance value. I've made a helper function to do the reverse as well by picking out a single value from one of these triples.

(define (greyscale image)
  (define (grey n)
    (make-color n n n))
  (for/list ([value (image->color-list image)])
    (grey (luminance value))))

(define (grey-value grey-color)
  ; readability helper function
  (color-red grey-color))

As a test of the above, we can output the result of this greyscale function directly in the REPL:

(define (create-bitmap img)
  (color-list->bitmap
   (greyscale img)
   (image-width img)
   (image-height img)))
   
(create-bitmap sample-image)
greyscale series of stairs

A Black and White Filter

With a means of defining "grey" it is possible to define a threshold function to clamp between black and white. I've defined two different functions with which to set the black and white threshold values, median and average.

(define (median color-lst)
  (let ([lst (sort color-lst #:key grey-value >)])
    (list-ref lst (/ (length lst) 2))))
    
(define (average color-lst)
  (let* ([lst (sort color-lst #:key grey-value >)]
         [avg-grey (/ (+ (grey-value (first lst)) (grey-value (last lst))) 2)])
    (make-color avg-grey avg-grey avg-grey)))

At this point, I've got a list of grey values based on luminance for the entire image and a function to capture the median grey color. All that is left is a kind of binary function to translate this threshold test into pure black and white pixels.

(define (black-and-white clamp img)
  ; filter based on "clamp" function
  (define grey-lst (greyscale img))
  (define color-midpoint (clamp grey-lst))
  (for/list ([color-value grey-lst])
    (let ([value (grey-value color-value)]
          [midpoint (grey-value color-midpoint)]
          [black (make-color "black")]
          [white (make-color 255 255 255)])
      (if (< value midpoint) black white))))

From there, it's a matter of writing out this transformed list of pixels to a bitmap image file, which we've already seen, Racket does without any real ceremony. First is the "average" function, which I have found to be too uneven in the distribution of detail:

(define (create-bitmap img)
  (color-list->bitmap
   (black-and-white average img)
   (image-width img)
   (image-height img)))

(create-bitmap sample-image)

a greyscale filtered
image of stairs

Finally is my preferred clamping function, median:

(define (create-bitmap img)
  (color-list->bitmap
   (black-and-white median img)
   (image-width img)
   (image-height img)))

(create-bitmap sample-image)

a
greyscale filtered image of stairs

Thoughts

I found all of this very fun, Racket provides enough batteries in the standard library to make problems tractable without taking all of the fun out of re-inventing these things myself. I've already started to continue the work by re-implementing a contrast adjustment but it is so far proving to be a veritable rabbit hole of further things to investigate and try.