Categories: Python

画像のピラミッドを1枚の画像として出力するサンプル

3.2k{icon} {views}


同一画像で繰り返し半分に縮小しながら積み重ねていく操作(ピラミッド)が必要になったので、ピラミッドを1枚の画像として出力するサンプルを作ってみました。

ピラミッド

同一画像の解像度をある一定比率(よくある例では半分)で繰り返し縮小しながら積み重ねていくことを、ピラミッドと言います。OpenCVのドキュメントでは、ガウシアンピラミッドやラプラシアンピラミッドが紹介されています(ラプラシアンピラミッドはGANの評価指標・SWDに使われます)。

例えば、128×128の解像度の画像があったとしましょう。これを高レベル(低解像度)方向にピラミッドを作っていくと、

  • 128×128の画像(オリジナル)
  • 64×64の画像(オリジナルを半分に縮小)
  • 32×32の画像(さらに半分に縮小)
  • 16×16の画像(さらに半分に縮小)

という具合に積み重なっていきます。

この記事でやりたいことは、出来上がったピラミッドを1枚の画像として結合し、出力するということです。

考え方

出力画像の解像度をなるべく抑えようとすると次のように考えられます。

まず、オリジナル画像の解像度に対して、縦か横を1.5倍した画像を「キャンバス」として考えます。そのキャンバスに対してピラミッドの各画像をペーストしていくようにします。最後にそのキャンバスをファイルとして保存すれば完成です。

ここで問題になるのは、ピラミッドの各画像をペーストする際、キャンバスでのどの座標に配置するか?ということです。これは書き出して行くと規則性が見えて、

解像度 左上x 左上y 右下x 右下y
128 0 0 128 128
64 0 128 64 192
32 64 128 96 160
16 64 160 80 176

ピラミッドの1枚目(オリジナル画像)が、キャンバスの(0,0)の座標に配置します。解像度は縦横128pxなので、右下・左下は(128,128)となります。

ピラミッドの2枚目は、1枚目の下に配置します。つまり、キャンバスでの座標(0, 128)にペーストします。この画像の解像度は64pxなので、始点に+64したのが右下の座標になります。

ピラミッドの3枚目は、1枚目の下・2枚目の右に配置します。ピラミッドは高レベルになるほど解像度が下がるのでこういう配置が可能です。

以下同様です。つまり、下→右→下→右→…のように配置していきます。

ここで、各画像のペースト先のキャンバス上での座標に対して、「2枚目-1枚目」、「3枚目-2枚目」の差分を取ります。そうすると次のようになります。

idx 差分x 差分y
0 0 128
1 64 0
2 0 32
3 16 0

規則性が見えてきました。「idxが偶数ならyに、idxが奇数ならx」に足せばよいのです。あとはこれを実装するだけです。

実装

オリジナル画像を「train.jpg」とします。

from PIL import Image

def tile_pyramid(n_repeat):
    with Image.open("train.jpg") as img:
        width, height = img.size
        with Image.new(img.mode, (width, height * 3 // 2)) as out: # 出力サイズは縦だけ1.5倍
            x, y = 0, 0
            out.paste(img, (x, y))
            for i in range(n_repeat):
                if i % 2 == 0:
                    y += height // (2 ** i) # 0, 2, 4..でy方向にシフト
                else:
                    x += width // (2 ** i) # 1, 3, 5..でx方向にシフト
                paste_img = img.resize((width // (2 ** (i + 1)), height // (2 ** (i + 1))), Image.LANCZOS)
                out.paste(paste_img, (x, y))
            out.save("out.png")

if __name__ == "__main__":
    tile_pyramid(6)

「n_repeat」はピラミッドを繰り返す回数を表します。この例では6回としました(最終的な解像度は1/64になります)。

結果

もうちょっと良い配置あるかも。

別バージョン

互い違いに足すことは変わりないですが、奇数偶数で縦横足さない場合でも微小な差分を足すことをしてみます。

def tile_pyramid(n_repeat):
    with Image.open("train.jpg") as img:
        width, height = img.size
        with Image.new(img.mode, (width, height * 3 // 2)) as out: # 出力サイズは縦だけ1.5倍
            x, y = 0, 0
            out.paste(img, (x, y))
            for i in range(n_repeat):
                if i % 2 == 0:
                    x += width // (2 ** (i + 3))                    
                    y += height // (2 ** i) # 0, 2, 4..でy方向にシフト
                else:
                    x += width // (2 ** i)  # 1, 3, 5..でx方向にシフト
                    y += height // (2 ** (i + 3))                    
                paste_img = img.resize((width // (2 ** (i + 1)), height // (2 ** (i + 1))), Image.LANCZOS)
                out.paste(paste_img, (x, y))
            out.save("out.jpg")

どっちが良いかはお好みで。

こしあん