png(Portable Network Graphics)はアルファチャネルによる透過機能持ち、圧縮効率と色の表現性のバランスのとれた便利な拡張子であり、Web開発や,ゲームで使用される画像やテクスチャファイル,として良く用いられる。しかし、pngのファイルの圧縮原理を理解し、自力で再生することはそれほど容易なことではない。
pngをメモリ上にビットマップ(拡張子のBMPではない)として展開して使用するには、「libpng」というオープンソースのライブラリが存在するが、これにはzlib Licenceというライセンスの影響を受けてしまい、ソースコード内にライセンス通知文章を含まなければならなくなる。私自身の個人的な主義だが、正直に言ってライセンスと言った様な契約が好きでなく、制作物はなるべくライセンスフリーで配布したいと考えている。
zlib Licenceは非常に緩いライセンスであるが、それでも自分の制作物に他者が宣言したライセンスが混入するのが嫌であったので、「BMP形式のファイルでなんとか透過画像を作る事が出来ないか」思案していたところ、BITMAPV4という規格ではビットマップにアルファチャネルを追加し透過することができることを知った。(ただし一般的な規格ではないので環境によっては表示出来なかったり崩れたりすることがある。)
早速png形式の画像を32bitのBMPに変換するソフトウェアを探してみたのであるが、Linuxに対応しているものは少なく、日本語情報も余り見つからなかったため、以前よりBMPのフォーマットに興味があったこともあり、勉強も兼ねて、pythonでpng画像を透過付きbmp画像に変換してみることにした。
1.アルファチャネル
透過付き画像を語る上では、まず、画像のアルファチャネルについて解説せねばならない、アルファチャネルとは、ビットマップイメージに対する透過度を示したマップであり、各ピクセルごとに、一つづつ透明度の情報が付加される。 画像の透明度はアルファチャネルをRGBそれぞれに乗算することで求められる。アルファチャネルは0〜1の間で変化すると定義されているが実際には8ビットint型であり、0〜255のレンジの百分率となる。例えばRGBそれぞれ8bitカラーの画像に対し、アルファチャネルを付加するとすると、1ピクセルあたり32bitの情報量になる。
アルファチャネルを持った画像同士の合成には、色々な種類があるが、今回は加増の重ね合わせ、一般的に言われるアルファブレンドだけついて解説する。まず、上に重ねる画像を、下側の被せられる方の画像をとし、重ね合わせによって出力されう画像をとし、それぞれのR,G,B,のそれぞれのチャネルをと呼び、アルファチャネルをと呼ぶことにする。
まず重ね合わせ後のアルファチャネルの計算は次のようになる。
この時計算結果が1を超えてしまった場合は1にする。計算結果を用いてR,G,Bそれぞれのチャネルをブレンドしてゆく、すると重ねあわせた後の画像は次のようになる。
2.python上でのpngの扱い
pythonでは、一般的に普及しているほとんどのラスタ画像を読み書きできるPILというライブラリが存在する。このライブラリは画像の読み込みや、表示だけを目的に作られたものでは無く、画像処理に関する様々な機能を持っているが、今回はPILを用いてpng画像のR(red),G(green)B(blue),A(Alpha)のそれぞれのチャネルにアクセスする。
PILはデフォルトでは入って居ない場合があるのでpipを用いてインストールする。今回はここのサイトを参考に次の様にコマンドを実行してインストールした。
$ sudo pip install pillow
次に実際にPILが動作するか確かめるために、pythonの対話モード次の様なプログラムを実行しPILの動作確認を行った。
from PIL import Image image = Image.open("任意のファイル名") image.show()
これで画像が表示されれば、正しくPILがインストールされたことが分かる。
3.透過付きBMPのヘッダの構造
透過つきBMPのファイル構造はMicrosoftが拡張したものであり、通常の32bitのBMPファイルとは異なった情報を持っている。これらのフォーマットの各要素はリトルエンディアン(最下位バイトから順に)で格納されるので注意せねばならない。
アドレス | 名前 | サイズ[Byte] | 値 |
---|---|---|---|
0x00 | bfType | 2 | "BM"の二文字を格納、BMPであると宣言 |
0x02 | bfSize | 4 | ファイルサイズ |
0x06 | bfReserved2 | 2 | 予約で0を格納 |
0x08 | bfReserved2 | 2 | 予約で0を格納 |
0x0A | bfOffset | 4 | 画像データまでのオフセットを格納(今回は134) |
0x0E | biSize | 2 | 以下に続くオプションの情報ヘッダのサイズ(今回は0x6C) |
0x12 | BiWidth | 4 | 横方向の解像度 |
0x16 | biHeight | 4 | 縦方向の解像度 |
0x1A | biPlane | 2 | プレーン数、常に1 |
0x1C | biBitCount | 2 | 画像のビット数(今回は32) |
0x1E | biComperession | 4 | RGB並びで非圧縮 |
0x22 | biSizeImage | 4 | 画像のサイズが格納されるが、不要なので0 |
0x26 | biXPerMeter | 4 | 横方向の一メートルあたりのドット数、今夏は0 |
0x2A | biYPerMeter | 4 | 縦方向の一メートルあたりのドット数、今回は0 |
0x2E | biCirUse | 4 | 色のパレット数、今回は0 |
0x32 | biCirImportant | 4 | パレットの重要色数、今回は0 |
0x36 | red_mask | 4 | 32bit中の赤色の桁を示すマスク(0x000000FF) |
0x3A | green_mask | 4 | 32bit中の赤色の桁を示すマスク(0x0000FF00) |
0x3E | blue_mask | 4 | 32bit中の赤色の桁を示すマスク(0x00FF0000) |
0x42 | alpha_mask | 4 | 32bit中の赤色の桁を示すマスク(0xFF000000) |
0x46 | CsType | 4 | 色の空間を示す("BGRs"の四文字) |
0x4A | Endpoints | 36={3*3*4} | RGB-XYZ変換テーブル今回はすべて0 |
0x6E | gamma_red | 4 | 赤色空間のガンマ値 |
0x72 | gammma_green | 4 | 緑色空間のガンマ値 |
0x76 | gamma_blue | 4 | 青色空間のガンマ値 |
0x7A | red | 4 | 不明、おそらく赤色空間のマスク |
0x7E | green | 4 | 不明、おそらく緑色空間のマスク |
0x82 | blue | 4 | 不明、おそらく緑色空間のマスク |
ヘッダーに関する解説は以上の通りである。実際のファイルではこの後に続いてRGBAのデータが格納されてゆく、格納されるビットマップのデータはRGBAの並びで格納される、通常のビットマップではBGRであるので注意が必要である。
4.実装
PILライブラリと透過付きBMPのヘッダフォーマットを元に、png画像を透過付きBMPに変換するプログラムを作成した。コードは次に示す通りである。
-
渡された関数は、引数から横幅やファイルサイズなどの情報を計算し、それを基にヘッダを作成する。
- ヘッダの情報にImage型の引数をRGBA形式に変換しヘッダに連結する。
- バイナリとしてファイルに書き込む
bytearrayにデータを追加していき、書き込むだけという単純なものである。基本的にPILで開くことのできるイメージファイルであれば、すべて対応していると考えられる。
from PIL import Image import sys #convert int value to 4byte bytearray def int_2_4byte( val): return bytearray([val & 0xff ,val >> 8 & 0xff, val >> 16 & 0xff ,val >> 24 & 0xff]) #convert int value to 2byte bytearray def int_2_2byte( val): return bytearray([val & 0xff ,val >> 8 & 0xff]) #save as 32bit transmission bitmap def con2bmp( _src ,FileName): rgba_im = _src.convert('RGBA') pix = im.size size = pix[0] * pix[1] * 4 + 134 bary = bytearray() #Bitmap File Header """0000""" bary.extend( bytearray(['B','M'])) """0002""" bary.extend( int_2_4byte(size) ) """0006""" bary.extend( bytearray([0,0]) ) """0008""" bary.extend( bytearray([0,0]) ) """000A""" bary.extend( int_2_4byte(134) ) #Bitmap Information Header """000E""" bary.extend( int_2_4byte(0x6C)) #stable """0012""" bary.extend( int_2_4byte(pix[0]) ) """0016""" bary.extend( int_2_4byte(pix[1]) ) """001A""" bary.extend( int_2_2byte(1) ) """001C""" bary.extend( int_2_2byte(32) ) #save as 32bit bit map """001E""" bary.extend( int_2_4byte(3) ) """0022""" bary.extend( int_2_4byte(0) )#dummy """0026""" bary.extend( bytearray([0xFF,0,0,0]) ) """002A""" bary.extend( bytearray([0xFF,0,0,0]) ) """002E""" bary.extend( int_2_4byte(0) ) """0032""" bary.extend( int_2_4byte(0) ) """0036""" bary.extend( bytearray([0xFF,0x00,0x00,0x00]) ) """003A""" bary.extend( bytearray([0x00,0xFF,0x00,0x00]) ) """003E""" bary.extend( bytearray([0x00,0x00,0xFF,0x00]) ) """0042""" bary.extend( bytearray([0x00,0x00,0x00,0xFF]) ) """0046""" bary.extend( bytearray(['B','G','R','s']) ) """0x004A""" for i in range(36): bary.extend([0x00]) """0x006E""" bary.extend( int_2_4byte(1) ) """0x0072""" bary.extend( int_2_4byte(1) ) """0x0076""" bary.extend( int_2_4byte(1) ) """0x007A""" bary.extend( bytearray([0xFF,0x00,0x00,0x00]) ) """0x007E""" bary.extend( bytearray([0x00,0xFF,0x00,0x00]) ) """0x0082""" bary.extend( bytearray([0x00,0x00,0xFF,0x00]) ) #ping Imgae to Bit map data for y in range(pix[1]): for x in range(pix[0]): r,g,b,a = rgba_im.getpixel((x, pix[1] - (y+1) )) bary.extend(bytearray([r,g,b,a])) #OpenFile with open(FileName, "wb") as fout: fout.write(bary)
args = sys.argv
im = Image.open(args[1])
con2bmp(im,args[2])
プログラムの実行は次の様に行う
python png2bmp.py [変換対象].png [変換後].bmp
残念ながらブログ上では、画像がすべてjpgに変換されてしまうため、この画面に写っているb画像はどちらもjpgになってしまっているが、左側の画像がpng画像、右側の画像がBMP画像である。余り画質に変化は見受けられない、また、ファイルサイズが2150[%]も増加してしまっている事からpngがBMPと比較しサイズ面で非常に優れている事が分かる。
変換前(PNG) | 変換後(BMP) |
---|---|
11,065 byte | 237,878 byte |
ライセンスを避けるために透過付きBMPを利用しようと考えたが、実際に使える事が分かった反面、ファイルサイズにおいてはPNGの方がはるかに優れていることを肌で感じた。やはり本当にライセンスを回避したいのであれば、PNGのファイルフォーマットを学んだほうが良いだろう、残念ながら僕にはまだそのレベルのプログラムを記述する実力が無い。