PIL/Pillow チートシート

この記事は最終更新日から3年以上が経過しています。

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

参考

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=(src255)1/γ×g×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 コンボリューション行列

参考

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」というモジュールを作成しています。

インストールは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

参考

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×log10MAX2MSE

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'です、修正いたします。 

pashango2
e-zero1
映像制作の企画・開発、モバイルアプリの制作を行っています。
http://www.e-zero1.co.jp/
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
  • 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
  • 線画抽出
  • コメント

    画像処理は素人の私には、大変ありがたい解説です。アルファ値を考慮したグレイスケール、参考になります。

    @python_ufo さん
    コメントありがとうございます、この記事はうっかり下書き中に公開してしまったため、中途半端な状態で公開してしまいました。
    随時、記事を更新していきます。

    Pillowは機能は少ないですが奥の深いライブラリです、お役に立てたのであれば嬉しいです。

    @pashango2 さん

    さくっとしか見ていないのでこちらが間違っている?かもですが、2値化の部分で、

    gray.point(lambda x: 0 if x < 230 else x) 
    

    が、2値の場合はたとえば白黒の0か255にする場合、

    gray.point(lambda x: 0 if x < 230 else 255) 
    

    が正しいかも?と思いました!:sunny:

    あなたもコメントしてみませんか :)
    すでにアカウントを持っている方は
    ユーザーは見つかりませんでした