My tribute to Steve Ballmer

2010-02-12 22:54:00

These days Microsoft is often being hammered in both the news and in Open Source communities across the globe, so on behalf of the Clojure community I would like to submit a small tribute to the man at the wheel, Steve Ballmer.




Preface

Microsoft makes good money, but they are going through tough times. So to make life a little happier at Camp Microsoft, I've decided to write a little Image to Ascii Art converter. Demonstrating, among other things, the power of macros. I don't think I'll get any arguments, that when you conjoin clojure and ascii-art, you get claskii art - So I've named this project appropriately


Processing an image

To convert an image to ascii we need to look at it as a bunch of colored pixels, converting them to characters one at a time. Disregarding the details of which image-holder we will use, we always find ourselves doing some tedious java-interop when working with Java Classes. Here's a quick example of how to get the brighest color from a pixel:

(defn get-pixel [image x y]
  (let [color (.getRGB image x y)
        red   (.getRed color)
        green (.getGreen color)
        blue  (.getBlue color)]
    (apply max [red green blue])))

Very simple right? Yes, and very boring. What I would like to be able to do, is just get at the fields more directly, like:

claskii> (def image (ImageIO/read (File. "steve-ballmer.jpg")))
#'image
claskii> (get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue)
[8 60]

Which of course isn't possible, because as soon as .getRed is evaluated I'll get a Symbol not defined error - Enter Macros! A recommended first step when writing macros is:

The second advice is

In our case we want get-properties to expand into something like

(get-properties object .getRed .getBlue)
>>> [(.getRed object) (.getBlue object)]

So to make this happen we need to walk through our method-list (ie. .getRed .getBlue) and return a sequence which results from applying the methods to the first argument, the object:

(defmacro get-properties [obj & properties]
  (for [property properties]
    (property obj)))

Because Lisp is homoiconic we treat code exactly like data in that (.getRed obj) is nothing more than a list whos first item is .getRed and the second is obj. To see what our macro expands to, call

claskii> (macroexpand-1 `(get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue))
(nil nil)

Not what we wanted! The reason is, that we haven't taken control of evaluation, using the macro-characters. I recommend you checkout them all before proceeding, as these puppies give you a lot of power, but also make macro-definitions a little hard on the eyes. The backquote stops evaluation, while allowing us to prefix items with a tilde ~ for evaluation, ~@ for splicing ~@(list 1 2 3) => 1 2 3.

(defmacro get-properties [obj & properties]
  `(vector
    ~@(for [property properties]
        (property obj))))

Unfortunately when you check that using macroexpand-1, you'll see the exact same result as above. The reason is, that when we're passing .getRed as a symbol and symbols are similar to :keywords in that they are a function of their arguments. When you're calling the symbol on the object you get nil in return. But why is the symbol being evaluated?

The splicing ~@ forces evaluation within its body, so we need to add an extra backquote:

(defmacro get-properties [obj & properties]
  `(vector
    ~@(for [property properties]
        `(property obj))))

Then check it

claskii> (macroexpand-1 `(get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue))
(clojure.core/vector (claskii/property claskii/obj) (claskii/property claskii/obj))

Now thats more like it! But calling it will throw an error of course, since property is not defined anywhere - Instead of taking property literally we want it evaluated to the 'property' var in the for-loop, and although direct evaluation would work in most-cases you always have to be on your toes when using macros, as one day a user will submit an argument, which clashes with one of yours. The solution is (gensym), which generates a unique name for your variables. Gensym can either be called directly or simple using the syntactic sugar #:

(defmacro get-properties [obj & properties]
  `(vector
    ~@(for [property# properties]
        `(~property# ~obj))))
claskii> (macroexpand-1 `(get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue))
(clojure.core/vector
    (.getRed (java.awt.Color. (.getRGB claskii/image 10 10)))
    (.getBlue (java.awt.Color. (.getRGB claskii/image 10 10))))
claskii> (get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue)
[8 60]

Nice! It expands like we want and it gets us the result we want! There are a couple of optimizations which are begging to be done. Firstly the object is being created once for each method argument. This is partly because the macro allows it, and partly because I'm not actually passing an object but rather the code which constructs an object. Secondly, by adding at object with the syntactic `(let [obj# ~obj]) gensym, will actually change the gensym with each run of the for-loop. So the ugly, stable and fully functioning version, using manual gensym comes out looking like so:

(defmacro get-properties [obj & properties]
  (let [target (gensym)]
    `(let [~target ~obj]
       (vector ~@(for [property properties]
                   `(~property ~target))))))


Scared yet?

I realize and admit that macro definitions look awful because we need all of our syntactic weapony rolled out in order to get the expansion we want. On the other hand, if you can look past the superficial, Lisps homoiconicity  lets us treat code exactly as data, which is just an incredible tool to be sitting with, as you can freely extend the very language itself by relatively simple means!

Well, to calm the nerves again, look at how easy java-interop has become. We have gone from:

(defn old-school [image]
  (let [w   (.getWidth image)
        h   (.getHeight image)
        r   (.getRed image)
        g   (.getGreen image)
        b   (.getBlue image)]))

To:

(def new-school [image]
     (let [[w h]   (get-properties image .getWidth .getHeight)
           [r g b] (get-properties (.getRGB image 10 10)l .getRed .getGreen .getBlue)]))


Ascii-Art


So making the ascii-art itself should be very simple. This is my strategy:

  1. Define a list of ascii characters of descending density
  2. Scale the image down to ascii-output-size
  3. Look at every pixel
  4. Examine the brighest color
  5. Divide that color by the amount of characters available and pick the appropriate one
  6. Output result

So begin by defining a list which you think looks good:

(def ascii-chars [\# \A \@ \% \$ \+ \= \* \: \, \. \space])

Then pick out the peak value and convert it to an index in the above data, by simple division

(defn ascii [img x y color?]
  (let [[red green blue] (get-properties ( Color. (.getRGB img x y))
                                         .getRed .getGreen .getBlue)
        peak    (apply max [red green blue])
        idx     (if (zero? peak)
                  (dec (count ascii-chars))
                  (dec (int (+ 1/2 (* (count ascii-chars) (/ peak 255))))))
        output  (nth ascii-chars (if (pos? idx) idx 0)) ]

...And depending on the output-type selected by the user, return either that character or an html-version:

    (if color?
      (html [:span {:style (format "color: rgb(%s,%s,%s);" red green blue)} output])
      output)))


Putting it together

Now that we have a way of processing each pixels, we just need to walk them all:

(defn convert-image [uri w color?]
  (let [raw-image   (scale-image uri w)
        ascii-image (->> (for [y (range (.getHeight raw-image))
                               x (range (.getWidth  raw-image))]
                           (ascii raw-image x y color?))
                         (partition w))

So that walks every X for every Y returning the ascii-representation of each pixel. When the for-loop completes you're sitting with a 1D stream of characters representing the image. In order to distinguish lines you need to partition the sequence, chopping it up every width-number-of-characters. Now we we're sitting with a sequence of lines, which we can properly format:

        output      (->> ascii-image
                         (interpose (if color? "<BR/>" \newline))
                         flatten)]

The only difference between the html version and ascii at this point, is how to seperate the lines. Once that done we can flatten the sequence of safe printing.

    (if color?
      (html [:pre {:style "font-size:5pt; letter-spacing:1px;
                           line-height:4pt; font-weight:bold;"}
             output])
      (println output))))

Adjust the html-settings to your liking - I'm no Ascii artist so consider this a raw prototype which more creative people can improve upon should they want to. Anyway, now's the time to test.


Getting Steve Ballmer

First I hit Google to get an image of the guy:


[caption id="" align="aligncenter" width="300" caption="Steve Ballmer"]Steve Ballmer[/caption]


Second, lets try and run it directly from the REPL:

claskii> (convert-image "/home/lau/Desktop/steve.jpg" 50 nil)

ASCII REPL

Since thats cooked down to only 50 characters it doesn't really to the guy justice, so lets try the HTML rendere:

claskii> (spit "h.html" (convert-image "steve.jpg" 120 true))

ASCII HTML

Thats more like it - although I'll admit that the A's look bad.


Conclusion

You've seen how macros are functions that control evaluation and outputs code. Macros are both fun and tricky so use them carefully. The entire program weighs in at 55 lines and I've put it on Github: here.

Haiseken
2010-02-13 02:08:10
That's awesome :D
Jake Voytko
2010-02-13 04:22:10
It looks like your program does a really good job! Clojure really lets you do some 'practical' things elegantly. You may have gotten me to start playing around with it again

If you want to experiment with the brightness, it pays to know that you can approximate the luminance by the formula L = .3 * red + .6 * green + .1 * blue
Nick Hodge
2010-02-13 16:07:56
++ Haiseken's comments on awesome

See, developers are cool.
John Rockefeller
2010-02-13 20:09:12
This is really neat :)
SM
2010-02-13 21:11:40
Great port. Thanks!
Willie
2010-02-15 03:49:05
This is the coolest post I've seen on Dzone in a week!

Got me interested in clojure too.
Laurent Petit
2010-02-15 14:18:18
Hi Lau, 

Great post!

I think you could have had an even shorter program (and post!) by just using a 'bean call on the Color, together with map destructuring:

(let [{:keys [red green blue]} (bean ( Color. (.getRGB img x y)))
   ...)

So I guess you could have resisted even more the temptation to jump from macro rule 1. to macro rule 2. ;-)
ArkRost
2010-02-18 06:38:09
Hi!
Thanks for the great post!
But I can't find scale-image function...
So in what library is it located?
Lau
2010-02-18 08:15:13
Hi ArkRost - Follow the link at the bottom of the post which says "Code on Github: here", there you will find the full source used for this blogpost, including scale-image.
Haiseken
2010-02-19 15:19:39
i'm trying to learn clojure
can you recommend me some guides or websites i should look? thx
Lau
2010-02-19 15:47:07
Hi Haiseken,

First stop: <a href="http://joyofclojure.com/buy" rel="nofollow">http://joyofclojure.com/buy</a>
When its complete, I'm sure it'll be the top resource 2nd only to this blog :) You can read the first chapter for free before deciding.

Second, there are a couple of recommended reads in the sidebar, especially Programming Clojure is a fantastic introduction to the language.

Thirdly, just get to it. Trying solving <a href="http://projecteuler.net/" rel="nofollow">Eulers</a> 1 - 50 with Clojure and if you run into trouble, come chat with us on #clojure on irc.freenode.net.

Hope it helps
Vadim
2010-02-20 17:19:17
nice post