四畳半テクノポリス

コロナのストレスで気が狂い、D進した院生

pythonによるpng画像の32bit透過付きBMPへの変換

 png(Portable Network Graphics)はアルファチャネルによる透過機能持ち、圧縮効率と色の表現性のバランスのとれた便利な拡張子であり、Web開発や,ゲームで使用される画像やテクスチャファイル,として良く用いられる。しかし、pngのファイルの圧縮原理を理解し、自力で再生することはそれほど容易なことではない。

 pngをメモリ上にビットマップ(拡張子のBMPではない)として展開して使用するには、「libpng」というオープンソースのライブラリが存在するが、これにはzlib Licenceというライセンスの影響を受けてしまい、ソースコード内にライセンス通知文章を含まなければならなくなる。私自身の個人的な主義だが、正直に言ってライセンスと言った様な契約が好きでなく、制作物はなるべくライセンスフリーで配布したいと考えている。

 zlib Licenceは非常に緩いライセンスであるが、それでも自分の制作物に他者が宣言したライセンスが混入するのが嫌であったので、「BMP形式のファイルでなんとか透過画像を作る事が出来ないか」思案していたところ、BITMAPV4という規格ではビットマップにアルファチャネルを追加し透過することができることを知った。(ただし一般的な規格ではないので環境によっては表示出来なかったり崩れたりすることがある。)

 早速png形式の画像を32bitのBMPに変換するソフトウェアを探してみたのであるが、Linuxに対応しているものは少なく、日本語情報も余り見つからなかったため、以前よりBMPのフォーマットに興味があったこともあり、勉強も兼ねて、pythonpng画像を透過付きbmp画像に変換してみることにした。

1.アルファチャネル

 透過付き画像を語る上では、まず、画像のアルファチャネルについて解説せねばならない、アルファチャネルとは、ビットマップイメージに対する透過度を示したマップであり、各ピクセルごとに、一つづつ透明度の情報が付加される。 画像の透明度はアルファチャネルをRGBそれぞれに乗算することで求められる。アルファチャネルは0〜1の間で変化すると定義されているが実際には8ビットint型であり、0〜255のレンジの百分率となる。例えばRGBそれぞれ8bitカラーの画像に対し、アルファチャネルを付加するとすると、1ピクセルあたり32bitの情報量になる。

 アルファチャネルを持った画像同士の合成には、色々な種類があるが、今回は加増の重ね合わせ、一般的に言われるアルファブレンドだけついて解説する。まず、上に重ねる画像をX、下側の被せられる方の画像をYとし、重ね合わせによって出力されう画像をOutとし、それぞれのR,G,B,のそれぞれのチャネルをX_{RGB}と呼び、アルファチャネルをX_{A}と呼ぶことにする。

 まず重ね合わせ後のアルファチャネルの計算は次のようになる。

Out_{A } = X_{A} + Y_{A}(1 - X_{A})

この時計算結果が1を超えてしまった場合は1にする。計算結果を用いてR,G,Bそれぞれのチャネルをブレンドしてゆく、すると重ねあわせた後の画像は次のようになる。

Out_{RGB} = \frac{ ( X_{RGB}\, X_{A} + Y_{RGB} \,Y_{A}(1 - X_{A}) )} {Out_{A}}

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ファイルとは異なった情報を持っている。これらのフォーマットの各要素はリトルエンディアン(最下位バイトから順に)で格納されるので注意せねばならない。

BMPのファイルフォーマット

アドレス名前サイズ[Byte]
0x00 bfType 2 "BM"の二文字を格納、BMPであると宣言
0x02 bfSize 4 ファイルサイズ(width + height)\times4+header
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に変換するプログラムを作成した。コードは次に示す通りである。

  1. 流れとしてはまずPILのImageでpng画像を読み込み、Image型の画像データをBMPで保存する関数へ渡す。

  2. 渡された関数は、引数から横幅やファイルサイズなどの情報を計算し、それを基にヘッダを作成する。

  3. ヘッダの情報にImage型の引数をRGBA形式に変換しヘッダに連結する。
  4. バイナリとしてファイルに書き込む

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[%]も増加してしまっている事からpngBMPと比較しサイズ面で非常に優れている事が分かる。

 

png画像の透過付きビットマップへの変換

変換前(PNG変換後(BMP
f:id:toriten1024:20170213181605p:plain f:id:toriten1024:20170213181610j:plain
11,065 byte 237,878 byte

 

 ライセンスを避けるために透過付きBMPを利用しようと考えたが、実際に使える事が分かった反面、ファイルサイズにおいてはPNGの方がはるかに優れていることを肌で感じた。やはり本当にライセンスを回避したいのであれば、PNGのファイルフォーマットを学んだほうが良いだろう、残念ながら僕にはまだそのレベルのプログラムを記述する実力が無い。