Python/PILによる画像のグレイスケール化とアスキーアート化

これを見ていたら手がうずうずしてきたので描いてみた。

Cool Ascii Animation using an Image Sprite, Canvas, and Javascript

リンク先はカラー画像(JavaマスコットキャラのDukeくん)をCanvas

  1. グレイスケール化し、
  2. 輝度にあわせてアスキーアートに変換している。

本記事ではPython/PILによって見よう見まねでグレイスケールとアスキーアート画像を作成する。

画像のグレイスケール化

PILを使えば簡単に行うことができる。

from PIL import Image
from PIL import ImageOps

if __name__ == "__main__":
    input_image = Image.open("lena.png")
    output_image = ImageOps.grayscale(input_image)
    output_image.save("gray_by_pil.png")

単にグレイスケール画像を作りたいならこれで事足りるが、アスキーアートを作成する際はピクセル単位で処理を行う必要がある。
よって練習のためにgrayscale関数を実装する。

from itertools import product
def grayscale(input_image):
    w, h = input_image.size
    input_pix = input_image.load()
    output_image = Image.new("L", (w, h))
    output_pix = output_image.load()

    for x, y in product(range(w), range(h)):
        r, g, b = input_pix[x, y]
        output_pix[x, y] = (r + g + b)/3
    return output_image

if __name__ == "__main__":
    input_image = Image.open("lena.png")
    output_image = grayscale(input_image)
    output_image.save("gray.png")

元の画像の大きさは512x512である。

Original Grayscale

ここでは単に平均をとっているが、グレイスケール変換には輝度を抽出するように係数を調整するやり方が存在する。

Three algorithms for converting color to grayscale ― The Endeavour
More on colors and grayscale ― The Endeavour

係数で重み付けした計算式は次のようなものだ。

r*0.2126 + g*0.7152 + b*0.0722

よって計算式を適用した関数は次のようになる。

def luminosity_grayscale(input_image):
    w, h = input_image.size
    input_pix = input_image.load()
    output_image = Image.new("L", (w, h))
    output_pix = output_image.load()

    for x, y in product(range(w), range(h)):
        r, g, b = input_pix[x, y]
        output_pix[x, y] = r*0.2126 + g*0.7152 + b*0.0722
    return output_image

grayscaleとluminosity_grayscale関数を見比べてみると、色の計算部以外はすべて同じ処理であることがわかる。
そこで共通の処理をまとめてprocessImageというデコレータにしてしまおう(いい名前が思いつかなかった)。

def processImage(func):
    def wrapper(input_image):
        w, h = input_image.size
        input_pix = input_image.load()
        output_image = Image.new("L", (w, h))
        output_pix = output_image.load()
        for x, y in product(range(w), range(h)):
            output_pix[x, y] = func(input_pix[x, y])
        return output_image
    return wrapper

@processImage
def grayscale(rgb):
    r, g, b = rgb
    return (r + g + b)/3

@processImage
def luminosity_grayscale(rgb):
    r, g, b = rgb
    return r*0.2126 + g*0.7152 + b*0.0722

画像のアスキーアート

グレイスケールができたら次はいよいよ画像のアスキーアート化に取り掛かる。
画像のグレイスケールに応じて明るいところではまばらな文字、暗いところでは詰まった文字を割り当てる。
いまいち勝手が掴めずまったく汎用性のないコードになってしまった。

def image2ascii(input_image):
    from PIL import ImageDraw, ImageFont
    w, h = input_image.size
    character, line = "", []
    DIV = 64
    fontsize = w/DIV
    font = ImageFont.truetype("C:/Windows/Fonts/msgothic.ttc", fontsize, encoding="utf-8")
    input_pix = input_image.load()
    output_image = Image.new("RGBA", (w, h))
    draw = ImageDraw.Draw(output_image)

    for y in range(0, h, fontsize):
        line = []
        for x in range(0, w, fontsize):
            r, g, b = input_pix[x, y]
            gray = r*0.2126 + g*0.7152 + b*0.0722
            if gray > 250:
                character = " "
            elif gray > 230:
                character = "`"
            elif gray > 200:
                character = ":"
            elif gray > 175:
                character = "*"
            elif gray > 150:
                character = "+"
            elif gray > 125:
                character = "#"
            elif gray > 50:
                character = "W"
            line.append(character)
        draw.text((0, y), "".join(line), font = font, fill="#000000")
    return output_image

if __name__ == "__main__":
    input_image = Image.open("lena.png")
    output_image = image2ascii(input_image)
    output_image.save("ascii.png")


余談

今回のコードはWindows/Python2.6/PIL1.1.7の環境で動かした。
フォントを扱う段階でPILがコケた際は、下記記事のStep5が参考になった。
blockdiag を WindowsXP で動かす « Stop Making Sense

あとLenaさんの画像は
http://optipng.sourceforge.net/pngtech/img/lena.html
から拾ってきたのだが公式に配布しているところとかあるのだろうか?