Pillow 利用ノート

Python で画像処理といえば、長い間 PIL が活躍していた。だがここ最近は Pillow というフォークパッケージが主流になっているらしい。とある別の作図パッケージのアップグレードの際に、それが Pillow に依存していることに気付いて世代交代を知ったのだった。

そこで、本稿では Pillow の導入方法と、既存の PIL 利用コードの移行手順について記す。なお、PyOpenGL や Pygame との連携で PIL を利用していた既存コードのそれについては、各ノートにて言及していく。

Note

  • OS
    • Windows 7 Home Premium x64 SP1
    • Windows 10 Home x64
  • 本稿において、利用した各パッケージのバージョンは次のとおり。
    • Python: 3.4.1, 3.5.0, 3.5.2
    • Pillow: 2.5.1, 2.8.1, 3.0.0, 3.2.0

関連リンク

Pillow
公式パッケージ・インストーラー配布元。
Python Extension Packages for Windows - Christoph Gohlke
非公式インストーラー配布元。

関連ノート

PyOpenGL 利用ノート
Pillow はテクスチャーデータを生成するのに大いに有用だ。

インストール

一般のサードパーティー製 Python パッケージのインストール手順 の方針に従ってインストールする。

ちなみに PIL が既に環境に居座っている場合は、Pillow をインストールする前に PIL をアンインストールする必要があるようだ。公式サイトの Installation ページに PIL と Pillow が共存できない旨が明記されている。

アップグレード

Anaconda または Miniconda で Python 環境を管理しているのであれば、コマンドラインで Pillow をアップグレードすることは容易い。単に conda update Pillow と入力するだけでよい。

それ以外の環境では pip を用いることになる。

バージョン確認

Anaconda または Miniconda で Python 環境を管理していて、コマンドラインで作業をしているならば、次のように conda ツールを用いる。

$ conda list Pillow
# packages in environment at D:\Miniconda3:
#
pillow                    3.2.0                    py35_1

Python インタラクティブシェル(長いので、以下単にインタープリターと書く)でインストール直後の Pillow のバージョンを確認するならば次のようにする。

>>> import PIL
>>> PIL.VERSION
'1.1.7'
>>> PIL.PILLOW_VERSION
'3.0.0'

ドキュメント

まずは Pillow 公式サイトの Guides を一読するとよい。それほどしっかりと読み込む必要はなく、使えそうな機能だけをつまみ読みでよい。

画像処理基本

ここでは基本モジュール Image のクラスと関数を利用した、初歩的な画像処理の実例をいくつか挙げる。

以下の説明で、単にメソッドという用語が現れる場合は、クラス Image のメソッドの意である。また、次のイメージを各操作デモの原像として多用している。

イルベロ

画像ファイルフォーマット変換(単発)

GIF 画像ファイル image.gif を PNG 形式に変換して、ファイル image.png として保存するコードを示す。

>>> from PIL import Image
>>> im = Image.open("image.gif")
>>> im.save("image.png")
  • 関数 open に画像ファイルパスを渡すと、画像オブジェクトが生成する。
  • メソッド save で画像ファイルをファイルシステム上に作成する。
    • 上のコードのようにファイルパスだけを指定する場合、ファイルの画像フォーマットは拡張子に基づいて Pillow が決定する。
    • 拡張子に頼らずに画像フォーマットを指定する場合は、キーワード引数 format から文字列により指定できる。

画像ファイルフォーマット変換(バッチ処理)

カレントディレクトリー内の GIF ファイルを PNG ファイルに変換、保存する処理のコードはこうなる。

#!/usr/bin/env python
"""batch.py: Demonstrate converting GIF files to PNG files.
"""
import os.path
import glob
from PIL import Image

for infile in glob.glob("*.gif"):
    file, ext = os.path.splitext(infile)
    im = Image.open(infile)
    im.save(file + ".png")
  • ちなみに、自分でわざわざ上のようなコードを用意する必要はほぼないだろう。後述するように、 Python の Scripts ディレクトリーにインストールされている pilconvert.py を使う手がある。
  • 公式サイトのドキュメントでは、同様の方式によるサムネイル画像ファイルを一括作成する例が紹介されている。メソッド opensave の呼び出しの間にメソッド thumbnail 呼び出しをはさむとそうなる。

Pillow がサポートする画像フォーマットを知る

Pillow のインストールディレクトリーで grep Image.register_ *.py するとよい。

画像データをバイト配列として扱う

画像データをバイト配列として扱うには、メソッド tobytes を用いる。 PyOpenGL のプログラムで画像ファイルからテクスチャーを生成する際に必須の方法だ。

  • 戻り値として bytes オブジェクトが得られる。
  • メモリレイアウトはキーワード引数 encoder_name で指定できるが、生のままでよい。例えば透過 PNG ファイルから Image オブジェクトを生成したときは、 tobytes は RGBARGBA... のようなデータ構造になる。
  • 旧メソッド名は tostring だったが、現在 deprecated となっている。昔のコードを再利用するときに置換しておこう。

簡易ビューワー

インタープリターで Pillow を試しているときに、クラス Image のオブジェクトをビューワーで確認したくなることがよくある。メソッド show はこういう場合に利用するのに相応しい。

>>> im = Image.open(...)
>>> ...
>>> im.show()

筆者の環境では Windows のフォトビューワーが起動して、画像の内容が表示された。

  • オブジェクトが既存のファイルから得られたものでない場合にも通用する。
  • ドキュメントによると、このメソッドは効率が悪いらしい。場合によってはオブジェクトをファイルに保存しつつ、再読み込み機能がある画像ビューワーを開いたままにして、デバッグ作業するのもありか。

ジャギー解消

メソッド resizethumbnail を利用するのならば、キーワード引数 resample の実引数をデフォルトの Image.NEAREST 以外の値にすることを検討するとよい。

  • Image.ANTIALIAS はドキュメントからは消えているが、単に Image.LANCZOS の別名である。

イメージモノクロ化

イルベロ

メソッド convert を引数 "L" で呼び出す。各ピクセルの RGB 値を次の式でグレースケール化してモノクロ化するようだ。

\begin{eqnarray*} L = \cfrac{299}{1000} R + \cfrac{587}{1000} G + \cfrac{114}{1000} B \end{eqnarray*}
>>> ...
>>> im = Image.open("illvelo.png")
>>> im.convert("L")

画像生成

ゼロから画像オブジェクトを生成するのに、関数 Image.new を利用することができる。

>>> from PIL import Image
>>> img = Image.new('RGB', (1024, 768))
  • 引数 mode でよく指定する値は 'RGB''RGBA' のどちらかになる。
  • 引数 size は画素単位の画像サイズ。幅、高さの順に要素が並ぶ tuple オブジェクトで指定する。
  • キーワード引数 color でいわゆる背景色を指定できる。デフォルトは黒。

色の表現

Pillow はコードでの色の表現手段を多数サポートしている。詳しくはモジュール ImageColor の内容を見るとよい。

色名による色指定

Pillow のメソッドで色を引数に取るものについては、色成分を列挙した listtuple だけでなく、モジュール ImageColor で決められている色名で指定することもできる。色名の型はプログラマーにやさしい形式、つまり文字列である。

>>> from PIL import Image
>>> img = Image.new('RGB', (1024, 768), 'deeppink')

色名は colormap という dict インスタンスに保持されている。興味があれば列挙してみるとよい。

>>> from PIL import ImageColor
>>> for i in sorted(ImageColor.colormap.items()):
>>>     print(i)
...
('aliceblue', '#f0f8ff')
('antiquewhite', '#faebd7')
('aqua', '#00ffff')

後半略。全部で 147 項目あるのでかなり長い。

色名から RGB 値に変換する

関数 ImageColor.getrgb を利用するほうが早い。

>>> from PIL.ImageColor import getrgb
>>> getrgb('deeppink')
(255, 20, 147)

PyOpenGL プログラムなどで A 値も欲しい場合は関数 getcolor を用いるという手もある。このとき引数 mode の明示的な指定を必要とする。

>>> from PIL.ImageColor import getcolor
>>> getcolor('deeppink', 'RGBA')
(255, 20, 147, 255)

透過

透過画像生成

関数 new で生成する画像オブジェクトを透過対応する方法は、引数 mode'RGBA' にすることだ。

さらに、引数 color を、例えば 4 成分の array-like オブジェクトを渡すとしよう。この場合 color[0:3] はそれぞれ今までどおり R, G, B 値を指定する。さらなる成分 color[3] はアルファー値といい、アルファベットの A で示す。この A 値は下限値の 0 が完全透明を意味し、上限値の 0xFF は完全不透明を意味する(ところで英語には opaque という単語があるが、日本語だと否定形で表現するしかなさそうだ)。その中間の値は、まあまあ透明という意味だ。実際はアルファブレンディングという手法で最終的な RGB 色を決定するための値である。

>>> img = Image.new('RGBA', (1024, 768), (0, 0, 0, 0))

このようにすると、真っ黒だが透明という画像オブジェクト img が生成する。

アルファチャンネルを持つ PNG 画像を青地背景上に重ねたい

要するに、透過ピクセルを含む画像をブルーバックの画像の上に乗せると思って欲しい。

#!/usr/bin/env python
"""alphapaste.py: Demonstrate how to composite two images with alpha values.
"""
import os.path
from PIL import Image

SOURCE_PATH = os.path.join(
    os.path.dirname(__file__), '../../_static/illvelo.png')

# Layer 1 in Photoshop.
img = Image.open(SOURCE_PATH)

# Background layer in Photoshop.
bkgnd = Image.new('RGBA', img.size, 'blue')

# Blend images with the transparency mask from img.split()[3].
result = Image.alpha_composite(bkgnd, img)
result.show()

いちばん楽なのは関数 alpha_composite を用いることだ。自分でメソッド split を呼んでバンドオブジェクトを用意したり、マスクを指定して関数 paste を呼ばずに済む。

元画像と処理後の画像はこうなる。ここで、元画像のキャラクターの輪郭の外側は完全透明色、内側領域は完全不透明色である。

イルベロ

グラデーション

Pillow は直接的にはグラデーションをサポートしていなそうなので、描きたいときは自作する。ここではグラデーションパターンの生成について、いくつかコツを記す。

線形グラデーション(透過なし)

線形グラデーション(透過なし)

\(1 \times 256\) ピクセルのイメージを作成し、ピクセルカラーをその位置に応じてセットしていく方針で絵を描く。まずは putpixel メソッドを利用してこれを行い、それから目的のサイズにイメージを拡縮する。

次に示すコードは、サイズが \(320 \times 240\) で、上部が赤で下部が青の線形グラデーションとなるイメージを作成する。

#!/usr/bin/env python
"""gradient1.py: Demonstrate drawing linear gradient.
"""
from PIL import (Image, ImageColor)
import numpy as np

COLOR_START = ImageColor.getrgb('antiquewhite')
COLOR_STOP = ImageColor.getrgb('deeppink')
IMAGE_WIDTH, IMAGE_HEIGHT = 320, 240
WORK_SIZE = 0x100

img = Image.new('RGB', (1, WORK_SIZE))
gradient = np.array(
    [np.linspace(i, j, WORK_SIZE) for i, j in zip(COLOR_START, COLOR_STOP)],
    dtype=int)

for i, rgb in enumerate(gradient.T):
    img.putpixel((0, i), tuple(rgb))

img = img.resize((IMAGE_WIDTH, IMAGE_HEIGHT))
img.show()
  • 線形補間の計算コード記述の手間を少々省くため、NumPy の関数 linspace を利用した。

線形グラデーション(単色にグレースケール透過)

線形グラデーション(単色にグレースケール透過)

メソッド putalpha 利用版グラデーション。

#!/usr/bin/env python
"""gradient2.py: Demonstrate drawing linear gradient.
"""
from PIL import Image

BASE_COLOR = 'red'
IMAGE_WIDTH, IMAGE_HEIGHT = 320, 240
WORK_SIZE = 0x100

img = Image.new('RGBA', (IMAGE_WIDTH, IMAGE_HEIGHT), BASE_COLOR)
gradient = Image.new('L', (1, WORK_SIZE))

for i in range(WORK_SIZE):
    gradient.putpixel((0, i), WORK_SIZE - i)

img.putalpha(gradient.resize(img.size))
img.show()

出力は上部が赤で、下部に至るにつれて透過していく線形グラデーションイメージとなる。

線形グラデーション(さらなる応用)

イルベロ

イメージ 3 枚重ね。

#!/usr/bin/env python
"""gradient3.py: Demonstrate drawing linear gradient on an image.
"""
import os.path
from PIL import Image

WORK_SIZE = 0x100
SOURCE_PATH = os.path.join(
    os.path.dirname(__file__), '../../_static/illvelo.png')

img = Image.open(SOURCE_PATH)
assert img.mode == 'RGBA'

gradient = Image.new('L', (1, WORK_SIZE))
for i in range(WORK_SIZE):
    gradient.putpixel((0, i), i)

alpha = gradient.resize(img.size, Image.ANTIALIAS)

final = Image.new('RGBA', img.size, (0, 0, 0, 0))
final.paste(img, None, mask=alpha)
final.show()

放射状グラデーション

放射状グラデーション

放射状のグラデーションを実現する例を示す。

#!/usr/bin/env python
"""gradient4.py: Demonstrate drawing radial gradient.
"""
from PIL import (Image, ImageColor, ImageDraw)
import numpy as np

COLOR_START = ImageColor.getrgb('antiquewhite')
COLOR_STOP = ImageColor.getrgb('deeppink')
WORK_SIZE = 0x100

img = Image.new('RGB', (WORK_SIZE, WORK_SIZE), color='white')
draw = ImageDraw.Draw(img)

gradient = np.array(
    [np.linspace(i, j, WORK_SIZE) for i, j in zip(COLOR_START, COLOR_STOP)],
    dtype=int)
for i, rgb in enumerate(gradient.T):
    draw.ellipse([i, i, WORK_SIZE - i, WORK_SIZE - i], fill=tuple(rgb))

img.show()

クラス ImageDraw.Draw のメソッド ellipse をバウムクーヘン方式に描画する。バウムクーヘンを外側から内側に向かって、少しずつ色を変えて塗れば、遠目からはグラデーションのように見えるだろう。

  • RGB 値の算出に np.linspace を用いるのは、前述の方法論による。
  • 楕円の形状指定方法は、そのバウンディングボックスを与えることによる。そして、ボックスの形状指定方法は、その左上頂点座標と右下頂点座標を与えることによる。

テキスト

テキストを画像として動的に生成する方法を見ていこう。ここでインポートする Pillow のモジュールは Image のほかにも ImageDrawImageFont がある。

Hello, world

Hello, world
#!/usr/bin/env python
"""text1.py: Demonstrate how to draw a text with PIL.
"""
from PIL import (Image, ImageDraw)

IMAGE_SIZE = (96, 24)
BKGND_COLOR = 'black'
TEXT_COLOR = 'white'

img = Image.new('RGBA', IMAGE_SIZE, BKGND_COLOR)
draw = ImageDraw.Draw(img)
draw.text((0, 0), 'Hello, world', fill=TEXT_COLOR)
img.show()

上のコードを動作させると、黒地に白い字で Hello, world と書かれた画像が生成する。これだけだとかなり不満がある。

  • テキストのサイズ等の情報が事前に予測不可能。
  • フォントを指定したい。

日本語テキスト

カラス出会い系メールの文面

コツは 3 つある。

  • 関数 ImageFont.truetype で日本語対応のフォントオブジェクトを作成する。
  • その際に encoding 引数に適切なエンコーディングを指示する。
  • text メソッドの引数にそのフォントを与える。
#!/usr/bin/env python
"""text2.py: Demonstrate how to draw multi-line text using ImageFont.
"""
from PIL import (Image, ImageDraw, ImageFont)

TEXT = '''どうしても会ってもらえませんか?
私はこんなにあなたに会いたいのに…。
お金には余裕があるので心配しないで
ください。
コード780の1102番で、
あなたを待っています。
'''

TEXT_COLOR = 'white'
BKGND_COLOR = 'black'
FONT = ImageFont.truetype('HGRME.TTC', 24, encoding='utf-8')

img = Image.new('RGB', (1024, 256), BKGND_COLOR)
draw = ImageDraw.Draw(img)

width = 0
height = 0
for line in TEXT.splitlines():
    draw.text((0, height), line, font=FONT, fill=TEXT_COLOR)

    ext = draw.textsize(line, FONT)
    width = max(ext[0], width)
    height += ext[1]

# Trim extra margin of right and bottom.
final = img.crop((0, 0, width, height))
final.show()

トリム処理を加えることで、出力画像の大きさはテキストにフィットする最小の矩形になるはずだ。

応用

少し手間をかけて、画像を加工することを試そう。

スクリーンショット

スクリーンキャプチャー

モジュール ImageGrab を用いると、画面イメージキャプチャーが得られる。このスクリーンショット取得機能は Pillow の Windows 版にだけある。

#!/usr/bin/env python
"""grabdemo.py: Demonstrate how to use function ImageGrab.grab.
"""
from PIL import (Image, ImageGrab)

# Take a snapshot of the whole screen.
img = ImageGrab.grab()

# For the purpose of display, make it to a thumbnail.
img.thumbnail((256, 256), Image.ANTIALIAS)

上下左右ループ壁紙パターン生成

イルベロ

気がついたら Pillow のドキュメントにこの技法が載っていたが、ここでは少々凝ったやり方で壁紙の繰り返し単位部分を生成する。

  1. 元画像を \(2 \times 2\) 分割して対角線上の区域を入れ替える。
  2. そこへ元画像をブレンドなりオーバーレイなりして重ね合わせる。

スクリプトのリポジトリーを移転したので、次のリンク先を参照。 pattern.py

モジュール ImagePath

これはユーザーが直接利用するのではなさそうなので使うのをやめた。

モジュール ImagePath は、ドキュメントがないモジュール ImageDraw2 の描画機能(点列のアフィン変換)のために、補助データ構造を提供するもののようだ。

コマンドラインユーティリティー

Pillow をインストールすると、パッケージの他に一連のスクリプトファイル群が Python ディレクトリーの Scripts フォルダーに格納される。これらのスクリプトは Pillow の機能を応用した、画像操作のためのささやかなコマンドラインユーティリティーだ。

スクリプト 何をするのか
pilconvert.py 画像ファイルの画像フォーマットや色モードを変換する。
pildriver.py 画像ファイルの画像の操作をする。対話モードあり。
pilfile.py 画像ファイルの鑑定をする。
pilfont.py フォントファイルを PIL のラスターフォントフォーマットに変換する。
pilprint.py 画像ファイルを PostScript プリンターに出力する。うまく動かない?

フォーマット変換

コマンドラインで pilconvert.py を利用する。例えば sample.gif から PNG 形式のファイル sample.png を作成するには次のように入力する。

$ pilconvert.py sample.gif sample.png

カレントディレクトリーのすべての GIF ファイルから PNG ファイルに変換したいならばこうなる。ちなみにシェルは bash である。

$ for name in *.gif ; do \
>   pilconvert.py $name ${name%.*}.png ; \
> done

リサイズ

コマンドラインで pildriver.py を利用する。以前にも記したが、バッチモードとインタラクティブモードがある。

$ pildriver.py
PILDriver says hello.
pildriver> open illvelo.png
[<PIL.PngImagePlugin.PngImageFile image mode=RGBA size=256x252 at 0xBEF800>]
pildriver> thumbnail 64 64
[<PIL.PngImagePlugin.PngImageFile image mode=RGBA size=64x63 at 0xBEF800>]
pildriver> show
[]
pildriver>