Edited at

PIL/Pillow チートシート

More than 1 year has passed since last update.

PIL/Pillowはコンパクトで高速なPython用の画像ライブラリです。

よく使う処理をまとめました(随時更新)


PILとPillowの違い

基本的にPILを使う理由はありません、Pillowの方がリサイズフィルタのバグフィックスがされており高品質です。


Pillowの速度について

Pillowは非常に高速にチューニングされており、同様なライブラリであるImageMagickよりも常に高速に動作します。

ただし、getpixel/putpixelは非常に低速です、画像生成以外には使わないようにしましょう。

更に高速なpillow-simdもあります。概ねオリジナルのPillowの4〜5倍の速度が出るようです。

pillow-simd

https://github.com/uploadcare/pillow-simd


参考

https://python-pillow.org/pillow-perf/


Imageモード一覧

モード
説明

1
1bit マスクに使用、論理演算が可能

L
8bit グレイスケール

P
パレットモード

RGB
8bit x 3

RGBA
8bit x 4 透明度(アルファ)付き

CMYK
8bit x 4 印刷関連でよく使われる

YCbCr
8bit x 3 ビデオ関連でよく使われる

HSV
8bit x 3 pillowのみ

RGBa
アルファチャンネルでRGB値を乗算

LA
アルファチャンネルでL値を乗算

I
32bit 整数

F
32bit 浮動少数


リサイズフィルタ

フィルタ
ダウンスケーリング品質
アップスケーリング品質
パフォーマンス

Image.NEAREST

⭐⭐⭐⭐⭐

Image.BOX

⭐⭐⭐⭐

Image.BILINEAR


⭐⭐⭐

Image.HAMMING
⭐⭐

⭐⭐⭐

Image.BICUBIC
⭐⭐⭐
⭐⭐⭐
⭐⭐

Image.LANCZOS
⭐⭐⭐⭐
⭐⭐⭐⭐

※Image.ANTIALIASはImage.LANCZOSの別名で、互換性のために残されています。

参考

http://pillow.readthedocs.io/en/3.4.x/handbook/concepts.html#concept-filters


Imageモジュール


グレイスケール

img.convert("L")

mono.png


アルファ値を考慮したグレイスケール

alpha.convert("LA")

alpha.pngla.png

ちなみにconvert('L')はアルファ値を考慮してくれません。

alpha.convert("L")

alpha.pngl.png


HSV変換

img.convert("HSV")

pillowのみHSV色空間にコンバートできます。色相(Hue)、彩度(Saturation)、明度(Value)の3つの成分で構成されています。

以下は、色相環をシフトする例です。

h, s, v = img.convert("HSV").split()

_h = ImageMath.eval("(h + 128) % 255", h=h).convert("L")
Image.merge("HSV", (_h, s, v)).convert("RGB")

hsv.png


CIE XYZ変換

CIE XYZは色同士のユークリッド距離が、人間の知覚が感じる差異と同一に成るように調整された色空間。

rgb2xyz = (

0.412453, 0.357580, 0.180423, 0,
0.212671, 0.715160, 0.072169, 0,
0.019334, 0.119193, 0.950227, 0
)
img.convert("RGB", rgb2xyz)


2値化

gray = img.convert("L")                     # グレイスケールに変換

gray.point(lambda x: 0 if x < 230 else x) # 値が230以下は0になる

bin.png


画像を明るくする / 暗くする

img.point(lambda x: x * 1.5)    # 1.5倍明るくする

img.point(lambda x: x * 0.5) # 1 / 2に暗くする

ligter.png darker.png


セピア

いったん画像をグレイスケールに変換してからセピア化します。

gray = img.convert("L")

Image.merge(
"RGB",
(
gray.point(lambda x: x * 240 / 255),
gray.point(lambda x: x * 200 / 255),
gray.point(lambda x: x * 145 / 255)
)
)

cepia.png


ガンマ補正

ガンマ補正もルックアップテーブルを使えば非常に高速に画像変換できます。

src=入力色、γ=ガンマ値、g=ゲイン値とすると、ガンマ補正の式は以下のとおりです。

dst = \biggl(\frac{src}{255}\biggr)^{1/γ} \times g \times 255

def gamma_table(gamma_r, gamma_g, gamma_b, gain_r=1.0, gain_g=1.0, gain_b=1.0):

r_tbl = [min(255, int((x / 255.) ** (1. / gamma_r) * gain_r * 255.)) for x in range(256)]
g_tbl = [min(255, int((x / 255.) ** (1. / gamma_g) * gain_g * 255.)) for x in range(256)]
b_tbl = [min(255, int((x / 255.) ** (1. / gamma_b) * gain_b * 255.)) for x in range(256)]
return r_tbl + g_tbl + b_tbl

img.point(gamma_table(1.2, 0.5, 0.5))

gamma.png


ループ内で使用するpointの高速化

ループ内でのImage.pointにはlambdaを渡さない方がよいでしょう、pointに渡す引数はあくまで変換テーブルであるため、事前に展開しておいた方が高速です。

for ...:

img.point(lambda x: x * 100)

# 下の処理は上の処理と等しいが高速です
table = [x * 100 for x in range(256)] * len(img.getbands())
for ...:
img.point(table)


getbbox

Image.getbboxは画像内で値が0でない最小領域を返します、値がすべて0の画像はNoneを返します。


アルファ成分の余白カット

alpha = Image.open("alpha.png")

crop = alpha.split()[-1].getbbox()
alpha.crop(crop)

範囲を選択_003.png範囲を選択_004.png


同一画素チェック

2つの画像の同一の場合ImageChops.differenceはすべて0の画像を返すため、getbboxNoneがならば同一と判断できます。

ImageChops.difference(img1, img2).getbbox() is None


リサイズ

img.resize((128, 128), Image.LANCZOS)

resize.png


サムネイル

サムネイルはリサイズと違い、縦横比を維持します。

なぜかthumbnailは破壊的メソッドなので注意してください、Image.copyなので複製を作っておくとよいでしょう。

img.thumbnail((128, 128), Image.LANCZOS)

img.size
# (128, 79)

thumb.png


回転

引数expandTrueを指定すると、回転時に画像が大きくなってしまう場合に画像を拡張します。

img.rotate(90, expand=True)

rotate90.png


モザイク処理

モザイク処理はImage.LINEARで縮小と拡大をすれば良いが、ガウシアンブラーをかけてから縮小するとやわらかいモザイクになります。

# ギザギザモザイク

img.resize([x // 8 for x in img.size]).resize(img.size)

# ガウシアンブラーをかけるとやわらかいモザイクに
gimg = img.filter(ImageFilter.GaussianBlur(4))
gimg.resize([x // 8 for x in img.size]).resize(img.size)

mozaic2.png mozaic.png


アルファブレンド

Image.blend(img,effect_img, 0.5)

B9BSxGZmEQpmAAAAAElFTkSuQmCC.png kL+ySM465ToAlAAAAAElFTkSuQmCC.png blend.png


減色

img.quantize(4)    # 4色に減色

quantize.png


アルファ画像を貼り付け

アルファ付き画像を貼り付ける場合はImage.pasteの引数'mask'にアルファ付き画像を指定する。

img.paste(alpha, mask=alpha)

paste.png


使用されている色をカウント

使用されている色のカウントを取る、Image.getcolorsでは引数なしの場合は255色以上はカウントできません。

255色以上の色を使った画像の場合、ピクセル数を引数で渡しておけば安心です。1

img.getcolors(img.size[0] * img.size[1])


ヒストグラム

イメージの色ヒストグラムをリストで返す。

各バンドを連続して返すため、RGBモードの場合は256 x 3=768個の要素を返します。

img.histogram()


色の置換

色を置換するメソッドはありません、色を置換した場合は下記の記事を参照してください。

PIL/Pillowで画像の色を高速に置換する


ImageOpsモジュール


ネガポジ反転

ImageOps.invert(img)

nega.png


左右反転 / 上下反転

ImageOps.mirror(img)    # 左右反転

ImageOps.flip(img) # 上下反転

mirror.png flip.png


カラー化

グレイスケール画像をピクセル値0をblackに、ピクセル値255をwhiteの色を着色します。

gray = ImageOps.grayscale(img)

ImageOps.colorize(gray, black=(0, 0, 0), white=(255, 255, 0))

mono.png → colorize.png


ポスタライズ

画像のビット深度を引数の値に縮小して色を単純化します。

ImageOps.posterize(img, 2)

posterize.png


ソーラライズ

しきい値を超えるすべてのピクセル値を反転させます。

使いどころは不明。

ImageOps.solarize(img, 128)

solarize.png


平均化(イコライズ)

イメージのヒストグラムを均等化します。

入力画像に非線形マッピングを適用して、出力画像にグレースケール値の均一な分布を作成します。

ImageOps.equalize(img)

equalize.png


ImageChopsモジュール

ImageChopsモジュールはチャンネルを操作するモジュールです。

B9BSxGZmEQpmAAAAAElFTkSuQmCC.png kL+ySM465ToAlAAAAAElFTkSuQmCC.png

左が被エフェクト画像、右がエフェクト用画像です、この章ではこの2つの画像をサンプルとして使用していきます。


覆い焼き(リニア)/ 減算

ImageChops.add(img, effect_img)         # img + effect_img

ImageChops.subtract(img, effect_img) # img - effect_img

wE7Grw7M8iQMgAAAABJRU5ErkJggg==.png X+81Pf98mz2EwAAAABJRU5ErkJggg==.png


mod演算

ImageChops.add_modulo(img, effect_img)         # img + effect_img % MAX

ImageChops.subtract_modulo(img, effect_img) # img - effect_img % MAX

addm.png subm.png


乗算 / スクリーン

ImageChops.multiply(img, effect_img)

ImageChops.screen(img, effect_img)

8yo3kAAAAASUVORK5CYII=.png P8BtLqhlqZTLrgAAAAASUVORK5CYII=.png


比較(明)/比較(暗)

ImageChops.lighter(img, effect_img)

ImageChops.darker(img, effect_img)

X+81Pf98mz2EwAAAABJRU5ErkJggg==.png wDMatqtY+79MgAAAABJRU5ErkJggg==.png


差の絶対値

ImageChops.difference(img, effect_img)

diff.png


オフセット

ImageChops.offset(img, 100, 100)

offset.png


ImageFilterモジュール

コンボリューション(畳み込み演算)を行います。

カーネルという行列を組み替えることにより、さまざまな画像変換を行います。

パラメータ
説明

size
カーネルのサイズ

scale
行列演算後にこの値で除算

offset
行列演算後にこの値で加算

kernel
コンボリューション行列


参考

https://github.com/python-pillow/Pillow/blob/6e7553fb0f12025306b2819b9b842adf6b598b2e/PIL/ImageFilter.py


ImageFilter.BLUR

img.filter(ImageFilter.BLUR)

# size: (5, 5),
# scale: 16,
# offset: 0,
# kernel:(
# 1, 1, 1, 1, 1,
# 1, 0, 0, 0, 1,
# 1, 0, 0, 0, 1,
# 1, 0, 0, 0, 1,
# 1, 1, 1, 1, 1
# )

bluer.png


ImageFilter.DETAIL

img.filter(ImageFilter.DETAIL)

# size: (3, 3),
# scale: 6,
# offset: 0,
# kernel: (
# 0, -1, 0,
# -1, 10, -1,
# 0, -1, 0
# )

detail.png


ImageFilter.SHAPEN

img.filter(ImageFilter.SHARPEN)

# size: (3, 3),
# scale: 16,
# offset: 0,
# kernel: (
# -2, -2, -2,
# -2, 32, -2,
# -2, -2, -2
# )

shapen.png


ImageFilter.CONTOUR

img.filter(ImageFilter.CONTOUR)

# size: (3, 3),
# scale: 1,
# offset: 255,
# kernel: (
# -1, -1, -1,
# -1, 8, -1,
# -1, -1, -1
# )

contour.png


ImageFilter.EDGE_ENHANCE / ImageFilter.EDGE_ENHANCE_MORE

img.filter(ImageFilter.EDGE_ENHANCE)

# size: (3, 3),
# scale: 2,
# offset: 0,
# kernel: (
# -1, -1, -1,
# -1, 10, -1,
# -1, -1, -1
# )

img.filter(ImageFilter.EDGE_ENHANCE_MORE)

# size: (3, 3),
# scale: 1,
# offset: 0,
# kernel: (
# -1, -1, -1,
# -1, 9, -1,
# -1, -1, -1
# )

edge.png edge_more.png


ImageFilter.EMBOSS

img.filter(ImageFilter.EMBOSS)

# size: (3, 3),
# scale: 1,
# offset: 128,
# kernel: (
# -1, 0, 0,
# 0, 1, 0,
# 0, 0, 0
# )

emboss.png


ImageFilter.FIND_EDGES

img.filter(ImageFilter.FIND_EDGES)

# size: (3, 3),
# scale: 1,
# offset: 0,
# kernel: (
# -1, -1, -1,
# -1, 8, -1,
# -1, -1, -1
# )

edge.png


ImageFilter.SMOOTH / ImageFilter.SMOOTH_MORE

img.filter(ImageFilter.SMOOTH)

# size: (3, 3),
# scale: 13,
# offset: 0,
# kernel: (
# 1, 1, 1,
# 1, 5, 1,
# 1, 1, 1
# )
#

img.filter(ImageFilter.SMOOTH_MORE)
# size: (5, 5),
# scale: 100,
# offset: 0,
# kernel: (
# 1, 1, 1, 1, 1,
# 1, 5, 5, 5, 1,
# 1, 5, 44, 5, 1,
# 1, 5, 5, 5, 1,
# 1, 1, 1, 1, 1
# )

smooth.png smooth_more.png


ガウシアンブラー

ガウシアンぼかしにより画面の平滑化します。

img.filter(ImageFilter.GaussianBlur(1.0))

img.filter(ImageFilter.GaussianBlur(1.5))
img.filter(ImageFilter.GaussianBlur(3.0))

gb10.png gb15.png gb30.png


膨張 / 収縮

MaxFilterはDilation(膨張)、MinFilterはErosion(収縮)と呼ばれるものになります。

img.filter(ImageFilter.MinFilter())

img.filter(ImageFilter.MaxFilter())

min.png max.png


参考

膨張・収縮・オープニング・クロージング


メディアンフィルタ

MedianFilterはノイズ除去によく使われます、ガウシアンフィルタに比べ輪郭があまりボケません。

img.filter(ImageFilter.MedianFilter())

noise.pngmedian.png


参考

ノイズ除去


モードフィルタ

指定されたサイズのボックスで最も頻繁に使用されるピクセル値を選択します。1回または2回だけ発生するピクセル値は無視されます。

使いどころ不明。PillowのModeFilterを使って写真を絵画調にするを参照してください。

img.filter(ImageFilter.ModeFilter(5))

rank.png


ImageEnhanceモジュール


カラーバランス調整

enhancer = ImageEnhance.Color(img)

enhancer.enhance(0.0) # 白黒
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) # 元画像

col0.png col05.png sample.png


コントラスト調整

enhancer = ImageEnhance.Contrast(img)

enhancer.enhance(0.0) # 灰色画像
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) # 元画像

con0.png con5.png sample.png


明るさ調整

enhancer = ImageEnhance.Brightness(img)

enhancer.enhance(0.0) # 黒画像
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) # 元画像

br0.png br5.png sample.png


シャープネス調整

enhancer = ImageEnhance.Sharpness(img)

enhancer.enhance(0.0) # ボケ画像
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) # 元画像
enhancer.enhance(1.5) # ↕
enhancer.enhance(2.0) # シャープ画像

sha0.png sample.png sample.png


ImageMathモジュール

ImageMathモジュールはピクセル同士の演算をあたかも数値演算のように書けるモジュールです。

使いこなせば複雑な画像処理も簡単に書けるようになります。


  • シングルバンドのみ演算可能、マルチバンドはImage.splitで分割してから処理

  • floatで演算する時の値の範囲は0.0~1.0ではなく0.0~255.0

  • 演算中のImageはmodeが"I"(int)、または"F"(float)になる、最後にmodeを"L"にコンバートする

  • 各種演算(+,-,*,/,**,%)はピクセル処理ではなくイメージまるごとの演算

  • 演算はピクセルごとではなくImageごとに行われ、演算ごとにImageが生成される

  • 演算のかわりにlambdaなどのコール可能オブジェクトも渡せる

シングルバンドしか処理できないため、RGBなどのイメージを変換するときに面倒なので、以下のようなヘルパー関数を用意しておくとよいでしょう。

def _blend_f(img1, img2, func):

blend_eval = "convert(func(float(a), float(b)), 'L')"
bands = [
ImageMath.eval(
blend_eval,
a=a,
b=b,
func=func
)
for a, b in zip(img1.split(), img2.split())
]
return Image.merge(img1.mode, bands)


参考

PIL/Pillowで高速にPhotoShopなどの描画モードを実装する


オーバーレイ

def _over_lay(a, b):

_cl = 2 * a * b / 255
_ch = 2 * (a + b - a * b / 255) - 255
return _cl * (a < 128) + _ch * (a >= 128)

_blend_f(img, effect_img, _over_lay)

overlay.png


ソフトライト

def _soft_light(a, b):

_cl = (a / 255) ** ((255 - b) / 128) * 255
_ch = (a / 255) ** (128 / b) * 255
return _cl * (b < 128) + _ch * (b >= 128)

_blend_f(img, effect_img, _soft_light)

softlight.png


ハードライト

def _hard_light(a, b):

_cl = 2 * a * b / 255
_ch = 2.0 * (a + b - a * b / 255.0) - 255.0
return _cl * (b < 128) + _ch * (b >= 128)

_blend_f(img, effect_img, _hard_light)

hardlight.png


Photoshopの描画モードを使う

Photoshopの描画モードを実装した「Image4Layer」というモジュールを作成しています。

https://github.com/pashango2/Image4Layer

インストールはpipで簡単に行なえます、実行にはpillow(PIL)があらかじめインストールされている必要があります。

$pip install image4layer

使い方は簡単です、color-dodgeモードで合成する例です。

from PIL import Image

from image4layer import Image4Layer

source = Image.open("ducky.png")
backdrop = Image.open("backdrop.png")

Image4Layer.color_dodge(backdrop, source)

color_dodge.png


GIFの書き込み

複数のGIF(GIFアニメーション)を書き込むことができます。

im.save(out, save_all=True, append_images=[im1, im2, ...])

簡単なアニメーションGIFの作成例です。

imgs = []

for i in range(100):
imgs.append(img.point(lambda x: x * (1.0 - (i/100))))

img.save("anime.gif", save_all=True, append_images=imgs, loop=True)

animation.gif


参考

http://pillow.readthedocs.io/en/4.0.x/handbook/image-file-formats.html?highlight=seek#saving


QImagaeへの変換

PyQtのQImageへの変換はImageQtモジュールを使用します。

ImageQt.ImageQt(img)

なお、PySideを使っている場合は、下記の方法が良いでしょう。

from PySide.QtGui import *

import io

img_buffer = io.BytesIO()
base.save(img_buffer, "BMP")
qimage = QImage()
qimage.loadFromData(img_buffer.getvalue(), "BMP")

無駄な処理なように見えますが、RGB→BGR変換やY軸の反転問題など面倒な部分をPillow/PySideがやってくれます。


参考

PIL.Image と PyQt4.QtGui.QImageの相互変換


PSNR

PSNRと2つの画像を比較する指標値です。

現在ではPSNRよりもSSIMが良いとされていますがPSNRもよく使用されます。

値が高いほうが画質が良く、圧縮の劣化度を測る際にPSNRが30~50の間が標準的な品質とされています。

計算式は以下の通り、MSEは平均二乗誤差で、MAXは255です。

PSNR = 10 \times \log 10\frac{MAX^2}{MSE}

PSNRを求める関数は以下のようになります、ImageStatモジュールを使う事により高速にPSNRを求める事ができます。

def psnr(img1, img2):

diff_img = ImageChops.difference(img1, img2)
stat = ImageStat.Stat(diff_img)
mse = sum(stat.sum2) / len(stat.count) / stat.count[0]
return 10 * math.log10(255 ** 2 / mse)

なお、SSIMはPIL/Pillowで高速に求めるのは現状では難しいです、pyssimモジュールを使うか、OpenCVを使ったほうがよいでしょう。


参考

ピーク信号対雑音比 - Wikipedia


線画抽出

gray = img.convert("L")

gray2 = gray.filter(ImageFilter.MaxFilter(5))
senga_inv = ImageChops.difference(gray, gray2)
senga = ImageOps.invert(senga_inv)

senga.png

こちらの方法を参考にしました、非常に素晴らしいです。





  1. 2017-02-07修正 'Image.getcount'となっていましたが、正しくは'Image.getcolors'です、修正いたします。 



  • Imageモード一覧
  • リサイズフィルタ
  • Imageモジュール
  • ImageOpsモジュール
  • ImageChopsモジュール
  • ImageFilterモジュール
  • ImageFilter.BLUR
  • ImageFilter.DETAIL
  • ImageFilter.SHAPEN
  • ImageFilter.CONTOUR
  • ImageFilter.EDGE_ENHANCE / ImageFilter.EDGE_ENHANCE_MORE
  • ImageFilter.EMBOSS
  • ImageFilter.FIND_EDGES
  • ImageFilter.SMOOTH / ImageFilter.SMOOTH_MORE
  • ガウシアンブラー
  • 膨張 / 収縮
  • メディアンフィルタ
  • モードフィルタ
  • ImageEnhanceモジュール
  • ImageMathモジュール
  • オーバーレイ
  • ソフトライト
  • ハードライト
  • Photoshopの描画モードを使う
  • GIFの書き込み
  • QImagaeへの変換
  • PSNR
  • 線画抽出