13.02.13

ASCII Image of the day

Combining latest rocket science and 30 years old Internet protocol together for fun.

This small project is inspired by Star Wars Episode 4 via DNS of which you probably heard. Unfortunately this nice trick is no longer available, but it will be remembered in our hearts.

So when I saw this traceroute output I thought "Wow", that's cool. Can I do something at least somewhat cool too? Obvious idea was to use something to do something, what this first something was not intended to do. Like reverse DNS lookup was not intended to show intro of Star Wars, Episode IV.

So I came up with the idea to combine ancient service from the times when Internet was young and NASA's Image of the day.

So, meet our heroes:
  • QOTD service (if you want more details, see Wikipedia article) basically returns some text string to anybody who has connected to port 17. 
  • NASA Image of the day is a web page which displays new space-related image every day. 
  • ASCII art is a technique used to present images in form of printable characters.
I guess, you're getting my idea already. If you're not, don't worry. It is pretty stupid anyway. Idea is to get NASA's image of the day, convert it to text and show it for anybody who asked for quote of the day using old QOTD protocol.

There are existing image to ASCII converters in the Internets, but I really like to invent the wheel, that's where the real fun is.

In the remaining of this article I will show how to convert image into ASCII-art and how to serve it via TCP connection. All the code is written in Python. I have to apologize for the quality of the code, I was quite anxious to complete the project asap, so bulk of all the code was written in 3 or 4 hours.
Some of the code was added later, but I didn't review all the code and didn't tried to optimize or clean it up.

I had an idea how to convert image into ASCII-art and decided to implement it. I didn't bothered to look what other people do, so my algorithm is simple, easy, obvious and does not produce the best results. There are ways to improve it and, probably, I will work on it further.

There are some simplifications I decided to take.
First of all, no color images. This is obviously easier as I don't need to think of dithering, quantization and ASCII-commands to set colors.
Second, I decided to go with a simple one pixel - one symbol approach.
Third, I will not care about different fonts in different terminals. I learned to stop worrying and love FreeMono font.

Basic idea of converting grayscale image into ascii is simple. Get each pixel brightness and find a character with a similar brighness.
For example character space ' ' will be great substitute for black pixels, while '`' (backtick) is ok for dark gray pixels and '@' is almost the brightest character we can get, so it will be used for white pixels.

I wrote short script that printed each character into a small image and calculated histogram for that image to see how many white pixels would be there. Then I got a table where each charater was mapped to its relative brightness. If there were several characters with the same brighness, I manually selected more pleasant looking (in my case it was symbols of more or less round shape or at least the ones with horizontal symmetry).

Here is approximate table that shows character brightness for some selected characters

SymbolBrightness
W41
Q38
@36
931
O30
$27
V24
H20
*17
z16
r14
=12
:10
-5
`3
0

Having this table it is easy to go through all the pixels of the image and convert pixels into characters.But first we need to do some image manipulations. Python Imaging Library comes in handy here.
We don't want to display 1600px wide image as text. Most of us still stuck with consoles as narrow as hundred and a half characters. So I decided to scale image down to 112 pixels by longes side. With PIL it is as simple as image.thumbnail((112,112)). I also converted image to grayscale with image = image.convert('L').
After this preparations converting image to ASCII is easy enough. We only need to make sure that pixel values in range 0..255 are mapped into brightness range 0..41, supported by our characters. I actually took maximal brightness as 45, so all pixels with scaled value greater than 41 will be displayed as white.

COLOR_MAP = [
    (41, 'W'),
    (38, 'Q'),
    (36, '@'),
    (31, '9'),
    (30, 'O'),
    (27, '$'),
    (24, 'V'),
    (20, 'H'),
    (17, '*'),
    (16, 'z'),
    (14, 'r'),
    (12, '='),
    (10, ':'),
    (5, '-'),
    (3, '`'),
    (0, ' ')
]

MAX_COLOR = 45


def pixel_to_char(value):
    clapped_value = value * MAX_COLOR / 255
    for index, c in enumerate(COLOR_MAP):
        if c[0] <= clapped_value:
            return c[1]
    return COLOR_MAP[-1][1]


def convert_image_to_strings(image):
    sz = image.size
    strings = []
    for y in xrange(0, sz[1]):
        s = ''
        for x in xrange(0, sz[0]):
            pix_char = pixel_to_char(image.getpixel((x, y)))
            s += pix_char
        strings.append(s)
    return strings
}
Double loop over all pixels and retrieving pixels with getpixel() function is certainly not efficient at all, but our image is quite small and I didn't want to complicate things.

Here is result of the algorithm's work:


------------:-:==--=z- ``:*rrrHHHHHHHHHz*H*HHVHHHHHHH*****HHHHHHHHHHVVVVVVVVVVVVVVVVVVH*z=:--```---*HHz=--------
----------`-::=-:::rz-``-r*rrrHHHHHHHHH**H*HVVHHHH*H*****HHHHHHHHVVHVVVVVVVVVVVVVVVVVVH**=:--```--:zHHr:---`----
-------:-------:-```r=-z*z=*H*HHHHHHHHH*rHHHHHH**H**H***HHHHHHHHHVHHVVVVVVVVVVVVVVVVVVVH*r::----`-:-rHH:--------
-------:-:--`--=-```-=:*zrrHHH*HHHHHHHH*rHHHHH**H***HH*HHHHHHHHHHHHHVVVVVVVVVVVVVVVVVVVH*z::----``--=HH=--------
``---`=-`-:--`=-```--zr:r*H*H****HHHHHHHHHHHH***HHHHHHHHHHHHHHHHHHHHVVVVHVVVVVVVVVVVVVVVHz=::--```---HH*:-------
``---`r-`-=---r`--` rz=:*H*HHHH**HHHHHHHHHHH****HHHHHHHHHHHHHHHHHHHHHVVVVVVVVVVVVVVVVVVVH*=:---`-----HH*:-------
-------``=:--:--``r*r==VHHHHHH***HVVVVHH*:-::==rrrz**HHHHHHHHHHHHHHHVHHHVHVVVVVVVVVVVVHH*z:----```-=`*H*=-------
---------:---:`` -**==*VHHHHHH***HHVVHH*::::=:=====rz**HHHHHHHHHHHHHHVHVHHHHVVVVVVVVVHHH*r-----``--=-*H*r---:---
--````::-:`:--``:H==*HHHHHHVH**HHHVVHrr********zr=:-----=r*HHHHHHHHHHHHHHHVVHHVHHH*r=:--:::::--------rHHz-------
-````--:::---``:**=zHHHHHHVH**HHHVH*=zzz***rrH**rzr=::---:=*HHHHHHHHHHHHHHHHHHHH*r::::==rr==:---```--=HHz:------
--`-----`---``:*r=HHHHHHVVVHH*HVVr:===:====--=zr-=:=r=r=r==z****HHHHHHVVHHHHHH*rrr=r:-::::------```--:HHz=:`----
-`-`-----``-``*r:*HHHHHHVVHHHHVH=-::-::----``-----`:=:zrrrrzz*****HHHVHHVHHHH*zz=:---------------``---HH==:-----
=r:-`---````:*rr*H*HHHHVVVHHHV:---------------:--`----=rrzrzz*****HHHHVVVVHHz-------------------------HHz=:-----
-===--`-```-**:HVHHHHHHVVHHVH:---------------:**=--`-`-rzzrrz******HHVVVVV*=-------z=:-----------`----HH*::-----
---`----```zr:HHHHHHHVVVHVV=::-----------``:--VVHH*--=:=rrrrzz****HHVVVVV*--------z***:----------`-:--HH*::-----
---`--``--:*:rVH*HVHHVVVVV*:=:---`--:=-::--=-:VVVVVr-:r:rr=rrzz***HHVVVVH:---:--:--*H*=---`---`----:--HH*-::-:--
----`-```r*-zH**HHVVVVVV*==rrr===---:=r:----=VVVVVH*=:rr==rrrzzz**HVV$VHz=zr::=::-=H**=---`-----``-:--HH*:-:-:--
`-`--`--:*==VH*HHHHVVVV*==rrrzzzz=--=rrz==rHHVVVVHHr**=zrrrrrrzz**HV$$VHr=r**=::=rHHH*=--------`---:--HH*=-:::--
--``` `-*:=VH*HHHVVVV*==rrrrrz***zr===r=rz***HHHHHH****rzrrrrrrz**HH$$VH*zrrr***H*H*r=-`--------------*HHr::=---
--````:z==*HHHHVVVVV*:=rrzr*zz*****zzzr=z=rz*=rrzHHHHH*rrrrzzzrr**HHV$VHHH***zzrrrr------:------------**Hr:-=---
-````:*r=HH*HHHVVVH::==zzz********z***z*zrzzzr*********rrrzzrzrrz*HHV$$HH***zzrrrrr===-==:------------**H*r-=---
-```-zz=zHHHHHVVVH-:==rr*z************z***********H**H*rzzzzzzrrz*HHVV$VHHH****zzrzr======:---`-------**H*z-=---
``-=*==rHHHHVVVr--===rrzzz*****HHHHHHHHHHHHHHHHHHHHHH*****z*zzzzr*HHVV$VHHHH*****zzzrrrr==:---`-------r*H*z-::--
---z*==*HHHVVVV:-::=rrrr********HHH*HHHHHHHHHHHHHHHH***z**z*zzrzr**HVV$VHHHH******zzrrrr==:---`-------r*HH*---`-
`-*r==HHHVHVV*---:=rrrrzzz*****H*HHHHHHHHHHHHHHHHHHH*******zzzrrr**HVV$VVHHHH*****zzzrrrr=:----------`=HHH*---`-
`-*==rHHHVHV*----:=rrrrrzz*****H*HHHHHHHHHHHHHHHHHHH********z*rzz**HVV$VVHHHH***zzzzzzzrr=:---------`-=*H**:--`-
r*=rrVHHVVV-`---:=rrrrrrzz*******HHHHHHHHHHHHHHHHHHH********zzrrz**HVV$VVHHHH*****zzzrzrr=:-------:--`:*Hz*z`-``
*H==zVVHHVV``---:=rrrrrrzz*******HHHHHHHHHHHHHHHHHH**********zrzzz*HHV$$VHHHH*****zzzzrrr=:-------:----zHr**`---
r==HVHVVV--`----:=rrrrrzzzz********HHHHHHHHHHHHHH******z**zzzrzrz***HV$$VH*HH******zzzzrr=--------:--`-rH***--``
==rVHHVV=```----:==rrzrrzz**********HHHHHHHHHHHHHH*****z***zrrrrzz**HHV$VHHH*******zzzrrr:--`-----:-``-rH***:-`-
=rHVVVV*-```:---::==rrrzzzz**********HHHHHHHHHHH*H****zzzzzrrrrrrzz*HHV$$V*H*******zrzrrr---`-----:-``-:H*r*r-`-
=*VVVV:`````:---:===rzzrzzzzz*******H*HHHHHHHHHH*H****zzzzzrrrrrrrz*HHV$$VH********zzzr==---------:-``--**z*r:`-
zHVVV-``-```=----:=rrrrrzrz***************HHHH*HH*****zzzzzrrrrrrrz**HH$$$H*******zzzrr=:-`---------``--*H*Hz:`-
HVVVr`--`--`=:--::==rrrrzzzz**************HHH**H*****zrrrrzzrrrrrzz**HH$$$H*******zzrrz::--`---------`--*H**z=--
HHzr:``````-=:---:==rrrrrrzzz****************H*H*****z==rz*zz*zrrz*z*HH$$$V*******zzrr=:--`----------`--zH****--
*HH=-``--``-==--:===rrrzzrz*z************************r=rzz*z****rrr**HVV$$H*******zzrr=---``---------`-`rHH**z--
H*---``--``-r:--::=rrrrrrrzz*************************z=rzzzrrrrzrrrr**HVVV********zzrr---`-----------`-`:HH**z:`
=-`````-----=:--::==rrrrrzrzzzz***********************rzzz=---:::rrrz*HHVH*******zzrr=-----------------`-*H*r*:-
```---------==::==rrrrrrrzrz*z******************H*****rrrrrrrrrr===rrr*HHHH*******zr:-----------------``-*H*r*=-
``----------rr:-:==rrrrrrrzzzzz************************zrrrzzzzzrrr**HHHHH*****zzzzr-----------:------``-*H*r*=-
---------`--rr=::=r=rrrrrrzzzz*zz*************************zzz**H***VVHVHHH*****zzzz:-----------:---------r*Hr*:-
------------rr=::==rrrrrrzrz*z****************H***************HHH*HHVVHHH******zzzr------------:--------`=*H=z--

It looks pretty good, ain't it?

Little bit details on how I get images from NASA web site. Nothing special, actually, they do have a RSS feed for Image of the day. I use feedparser library to get and parse it to retrieve image URL and description. Images are preprocessed (converted to grayscale and scaled down) and stored in sqlite database. Server process reads latest image from database, converts it into ASCII and sends to client.

You can try the service by telneting to ninja-cat.dlinkddns.com:80 (yes, I put service on port 80, so I can use it from work, where port 17 is closed). It is hosted on Raspberry Pi and uplink channel does not have much bandwidth, so it is not going to win any speed contests.

Right now ninja-cat.dlinkddns.com hosts newer version of code, which generates 16 color dithered images with ANSI escape sequences. Those with Windows should use decent terminal, like mintty from cygwin package.

Future plans include more sophisticated color image conversion, code cleanup and optimization and minimizing references to PIL (I would like to do my own image scaling, color conversion, and Floyd-Steinberg dithering).

Code is available under Apache License 2.0 from github.

Немає коментарів:

Дописати коментар