Google App Engineで画像処理するためにpypngを使ってみた

Google App Engineで画像処理をしようと思ったのですが、公式で提供されているAPIは機能が少なすぎます。
http://code.google.com/intl/ja/appengine/docs/python/images/

Pythonの画像処理で有名なライブラリにPIL(Python Imaging Library)というのがあるらしいのですがGoogle App Engineでは使えないそうです。
http://www.pythonware.com/products/pil/

そこでPure Pythonのライブラリであるpypngを使うことにしました。

GIFが対象の場合はpygifで頑張ってる方がいらっしゃったのでリンクを張っておきます。

やりたいこと

PNGの左上(x,y)=(0,0)のドットと同じ色を透過色にする

PHPでGDを使えばこれだけで済むんだけど…
https://gist.github.com/824781

今までは外部サーバにこれを設置して処理を丸投げしていたのですが、諸事情で使えなくなりそうなので、頑張ってpypngでやります。

pypng

Google Code
http://code.google.com/p/pypng/
Document
http://packages.python.org/pypng/

説明がシンプルすぎます。
詳細な使い方を知るにはソース読むしかないです…。

とりあえず読み込む

read()でもいいのですがasRGB8()っていうメソッドを見つけたのでこれを使います。
8bitPNGとして読み込んで統一しておいたほうが書き込む時にRGBAの範囲を0-255に決め打ちできて後々楽だと考えました。

GAEのことは忘れてファイルの読み書きをしますよ。

import png

pr = png.Reader(file=open('before.png', 'rb'))
x,y,pixels,meta = pr.asRGB8()

print x,y,pixels,meta
16 16  {'bitdepth': 8, 'colormap': False, 'interlace': 0, 'planes': 3, 'greyscale': False, 'alpha': False, 'size': (16, 16)}

x,y,metaの型はわかった。generator objectってなんぞ?(・ω・`)

http://www.panopticon.jp/blog/2007/02/152332.html

なるほど。for文で回せるオブジェクトを関数みたいに定義して作成したもの、という理解であってるかな?
ピクセル全部を配列に入れて保持してるとメモリが大変でしょ、っていう配慮だろうか?

"イテレータ"っていうのを知るともっと賢くなれそうなので後で調べてみる。
http://jutememo.blogspot.com/2008/06/python.html

とりあえずforで回してみた

for p in pixels:
	print p
[125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55]
[125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55]
[125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 56, 125, 82, 58, 125, 82, 58, 125, 82, 57, 125, 81, 55, 125, 81, 55]
[125, 81, 55, 125, 81, 55, 125, 81, 56, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 82, 56, 125, 80, 52, 125, 73, 41, 125, 74, 42, 125, 76, 45, 125, 82, 57, 125, 81, 55]
[125, 81, 55, 125, 82, 57, 125, 79, 51, 125, 80, 53, 125, 82, 57, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 82, 56, 125, 79, 50, 125, 86, 65, 127, 126, 142, 128, 134, 153, 126, 107, 105, 125, 77, 46, 125, 82, 56]
[125, 81, 56, 125, 76, 46, 125, 88, 69, 125, 85, 63, 125, 77, 47, 125, 82, 56, 125, 81, 55, 125, 81, 55, 125, 81, 54, 125, 82, 59, 129, 154, 195, 127, 124, 138, 126, 101, 90, 129, 155, 197, 127, 117, 124, 125, 74, 43]
[125, 76, 46, 127, 119, 128, 128, 142, 173, 129, 144, 177, 126, 106, 104, 125, 77, 46, 125, 82, 57, 125, 82, 58, 125, 74, 41, 127, 118, 125, 129, 149, 186, 124, 68, 31, 125, 71, 36, 126, 99, 90, 129, 160, 206, 125, 78, 49]
[126, 90, 73, 129, 158, 202, 125, 80, 53, 126, 98, 87, 129, 155, 197, 125, 79, 51, 125, 81, 55, 125, 83, 58, 125, 72, 38, 128, 130, 150, 128, 136, 160, 125, 74, 42, 125, 83, 59, 125, 90, 72, 129, 161, 207, 125, 84, 62]
[126, 98, 87, 129, 153, 194, 125, 69, 32, 125, 84, 62, 129, 157, 199, 125, 83, 59, 125, 80, 54, 125, 83, 58, 125, 74, 42, 127, 115, 119, 129, 152, 191, 125, 69, 32, 124, 69, 32, 126, 103, 98, 129, 158, 204, 125, 77, 47]
[125, 80, 52, 128, 146, 180, 127, 120, 132, 128, 134, 156, 128, 133, 155, 125, 75, 43, 125, 81, 55, 125, 80, 53, 125, 80, 53, 125, 80, 54, 129, 150, 187, 128, 131, 152, 127, 108, 106, 129, 158, 202, 127, 111, 113, 125, 75, 44]
[125, 79, 51, 125, 84, 62, 127, 119, 129, 127, 114, 118, 125, 76, 46, 126, 102, 97, 128, 139, 162, 128, 137, 160, 127, 127, 142, 125, 80, 53, 125, 82, 58, 127, 120, 131, 128, 128, 145, 126, 99, 91, 125, 76, 46, 125, 82, 56]
[125, 81, 56, 125, 80, 53, 125, 74, 41, 125, 75, 43, 125, 81, 54, 125, 84, 60, 125, 87, 67, 125, 87, 67, 125, 86, 64, 125, 81, 56, 125, 80, 54, 125, 74, 41, 125, 73, 40, 125, 77, 47, 125, 82, 57, 125, 81, 55]
[125, 81, 55, 125, 81, 56, 125, 82, 58, 125, 82, 57, 125, 81, 55, 125, 80, 54, 125, 79, 52, 125, 79, 52, 125, 80, 53, 125, 81, 55, 125, 81, 56, 125, 82, 58, 125, 83, 58, 125, 82, 57, 125, 81, 55, 125, 81, 55]
[125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55]
[125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55]
[125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55, 125, 81, 55]

3つずつのペアになっていて、左から順にRGB値になってるっぽいですね。
用意した画像が16x16なので16x16個、ちゃんとあります。

とりあえず書き込む

何も加工せずそのまま書き込めば画像のコピーができるはず。

pw = png.Writer(
	x,
	y,
	interlace=False,
	bitdepth=meta['bitdepth'],
	planes=meta['planes'],
	alpha=False
)
pw.write(open('after.png', 'wb'), pixels)

よしよし、before.pngがafter.pngにコピーされました。

本題

透過するには書き込み時にalpha=Trueとすれば良いわけですが、3つずつのペアになっていたピクセルのデータを4つのペアにする必要があります。
4つ目がアルファ値ですね。0が透過、255が透過無し、のようです。
左上のドットのRGB[r,g,b]を覚えておいてそれに一致するドットは[0,0,0,0]とかにしたいわけです。
それ以外は[r,g,b,255]にすれば元のままです。

import png

pr = png.Reader(file=open('before.png', 'rb'))
x,y,pixels,meta = pr.asRGB8()
new_pixels = []
i = -1
for ps in pixels:
	if i == -1:
		r = ps[0]
		g = ps[1]
		b = ps[2]
	i = 0
	new_p = []
	new_p_set = []
	for p in ps:
		new_p_set.append(p)
		i += 1
		if i >= 3:
			new_p_set.append(255)
			if new_p_set == [r, g, b, 255]:
				new_p_set = [0, 0, 0, 0]
			new_p.extend(new_p_set)
			i = 0
			new_p_set = []
	new_pixels.append(new_p)
pw = png.Writer(
	x,
	y,
	interlace=False,
	bitdepth=meta['bitdepth'],
	planes=meta['planes'],
	alpha=True
)
pw.write(open('after.png', 'wb'), new_pixels)

な、なんかPythonなのに汚いな!でも動いたからよし!
ところでwriteするときにリスト渡しちゃってるけどせっかくreadでgenerator返ってきてるのに意味なくね?
generatorにして引数に渡しましょう。勉強にもなるし。

import png

pr = png.Reader(file=open('before.png', 'rb'))
x,y,pixels,meta = pr.asRGB8()
def new_pixels(pixels):
	i = -1
	for ps in pixels:
		if i == -1:
			r = ps[0]
			g = ps[1]
			b = ps[2]
		i = 0
		new_p = []
		new_p_set = []
		for p in ps:
			new_p_set.append(p)
			i += 1
			if i >= 3:
				new_p_set.append(255)
				if new_p_set == [r, g, b, 255]:
					new_p_set = [0, 0, 0, 0]
				new_p.extend(new_p_set)
				i = 0
				new_p_set = []
		yield new_p
pw = png.Writer(
	x,
	y,
	interlace=False,
	bitdepth=meta['bitdepth'],
	planes=meta['planes'],
	alpha=True
)
pw.write(open('after.png', 'wb'), new_pixels(pixels))

できました。
ここでGAEのことを思い出します。

GAEはファイルの読み書きができないのでStringIOを使うのが定石らしいです。

from StringIO import StringIO
import png

#前略

pr = png.Reader(file=StringIO(image_binary_data))

#中略

o = StringIO()
pw.write(o, new_pixels(pixels))
new_image_binary_data = o.getvalue()

#後略

こんな感じで変換後のバイナリデータを取り出せます。

画像処理って難しいですね!