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")
アルファ値を考慮したグレイスケール
alpha.convert("LA")
ちなみにconvert('L')
はアルファ値を考慮してくれません。
alpha.convert("L")
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")
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になる
画像を明るくする / 暗くする
img.point(lambda x: x * 1.5) # 1.5倍明るくする
img.point(lambda x: x * 0.5) # 1 / 2に暗くする
セピア
いったん画像をグレイスケールに変換してからセピア化します。
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)
)
)
ガンマ補正
ガンマ補正もルックアップテーブルを使えば非常に高速に画像変換できます。
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))
ループ内で使用する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)
同一画素チェック
2つの画像の同一の場合ImageChops.difference
はすべて0の画像を返すため、getbbox
がNone
がならば同一と判断できます。
ImageChops.difference(img1, img2).getbbox() is None
リサイズ
img.resize((128, 128), Image.LANCZOS)
サムネイル
サムネイルはリサイズと違い、縦横比を維持します。
なぜかthumbnail
は破壊的メソッドなので注意してください、Image.copy
なので複製を作っておくとよいでしょう。
img.thumbnail((128, 128), Image.LANCZOS)
img.size
# (128, 79)
回転
引数expand
にTrue
を指定すると、回転時に画像が大きくなってしまう場合に画像を拡張します。
img.rotate(90, expand=True)
モザイク処理
モザイク処理は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)
アルファブレンド
Image.blend(img,effect_img, 0.5)
減色
img.quantize(4) # 4色に減色
アルファ画像を貼り付け
アルファ付き画像を貼り付ける場合はImage.paste
の引数'mask'にアルファ付き画像を指定する。
img.paste(alpha, mask=alpha)
使用されている色をカウント
使用されている色のカウントを取る、Image.getcolors
では引数なしの場合は255色以上はカウントできません。
255色以上の色を使った画像の場合、ピクセル数を引数で渡しておけば安心です。1
img.getcolors(img.size[0] * img.size[1])
ヒストグラム
イメージの色ヒストグラムをリストで返す。
各バンドを連続して返すため、RGBモードの場合は256 x 3=768個の要素を返します。
img.histogram()
色の置換
色を置換するメソッドはありません、色を置換した場合は下記の記事を参照してください。
ImageOpsモジュール
ネガポジ反転
ImageOps.invert(img)
左右反転 / 上下反転
ImageOps.mirror(img) # 左右反転
ImageOps.flip(img) # 上下反転
カラー化
グレイスケール画像をピクセル値0をblack
に、ピクセル値255をwhite
の色を着色します。
gray = ImageOps.grayscale(img)
ImageOps.colorize(gray, black=(0, 0, 0), white=(255, 255, 0))
ポスタライズ
画像のビット深度を引数の値に縮小して色を単純化します。
ImageOps.posterize(img, 2)
ソーラライズ
しきい値を超えるすべてのピクセル値を反転させます。
使いどころは不明。
ImageOps.solarize(img, 128)
平均化(イコライズ)
イメージのヒストグラムを均等化します。
入力画像に非線形マッピングを適用して、出力画像にグレースケール値の均一な分布を作成します。
ImageOps.equalize(img)
ImageChopsモジュール
ImageChops
モジュールはチャンネルを操作するモジュールです。
左が被エフェクト画像、右がエフェクト用画像です、この章ではこの2つの画像をサンプルとして使用していきます。
覆い焼き(リニア)/ 減算
ImageChops.add(img, effect_img) # img + effect_img
ImageChops.subtract(img, effect_img) # img - effect_img
mod演算
ImageChops.add_modulo(img, effect_img) # img + effect_img % MAX
ImageChops.subtract_modulo(img, effect_img) # img - effect_img % MAX
乗算 / スクリーン
ImageChops.multiply(img, effect_img)
ImageChops.screen(img, effect_img)
比較(明)/比較(暗)
ImageChops.lighter(img, effect_img)
ImageChops.darker(img, effect_img)
差の絶対値
ImageChops.difference(img, effect_img)
オフセット
ImageChops.offset(img, 100, 100)
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
# )
ImageFilter.DETAIL
img.filter(ImageFilter.DETAIL)
# size: (3, 3),
# scale: 6,
# offset: 0,
# kernel: (
# 0, -1, 0,
# -1, 10, -1,
# 0, -1, 0
# )
ImageFilter.SHAPEN
img.filter(ImageFilter.SHARPEN)
# size: (3, 3),
# scale: 16,
# offset: 0,
# kernel: (
# -2, -2, -2,
# -2, 32, -2,
# -2, -2, -2
# )
ImageFilter.CONTOUR
img.filter(ImageFilter.CONTOUR)
# size: (3, 3),
# scale: 1,
# offset: 255,
# kernel: (
# -1, -1, -1,
# -1, 8, -1,
# -1, -1, -1
# )
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
# )
ImageFilter.EMBOSS
img.filter(ImageFilter.EMBOSS)
# size: (3, 3),
# scale: 1,
# offset: 128,
# kernel: (
# -1, 0, 0,
# 0, 1, 0,
# 0, 0, 0
# )
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
# )
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
# )
ガウシアンブラー
ガウシアンぼかしにより画面の平滑化します。
img.filter(ImageFilter.GaussianBlur(1.0))
img.filter(ImageFilter.GaussianBlur(1.5))
img.filter(ImageFilter.GaussianBlur(3.0))
膨張 / 収縮
MaxFilter
はDilation(膨張)、MinFilter
はErosion(収縮)と呼ばれるものになります。
img.filter(ImageFilter.MinFilter())
img.filter(ImageFilter.MaxFilter())
参考
メディアンフィルタ
MedianFilter
はノイズ除去によく使われます、ガウシアンフィルタに比べ輪郭があまりボケません。
img.filter(ImageFilter.MedianFilter())
参考
モードフィルタ
指定されたサイズのボックスで最も頻繁に使用されるピクセル値を選択します。1回または2回だけ発生するピクセル値は無視されます。
使いどころ不明。PillowのModeFilterを使って写真を絵画調にするを参照してください。
img.filter(ImageFilter.ModeFilter(5))
ImageEnhanceモジュール
カラーバランス調整
enhancer = ImageEnhance.Color(img)
enhancer.enhance(0.0) # 白黒
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) # 元画像
コントラスト調整
enhancer = ImageEnhance.Contrast(img)
enhancer.enhance(0.0) # 灰色画像
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) # 元画像
明るさ調整
enhancer = ImageEnhance.Brightness(img)
enhancer.enhance(0.0) # 黒画像
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) # 元画像
シャープネス調整
enhancer = ImageEnhance.Sharpness(img)
enhancer.enhance(0.0) # ボケ画像
enhancer.enhance(0.5) # ↕
enhancer.enhance(1.0) # 元画像
enhancer.enhance(1.5) # ↕
enhancer.enhance(2.0) # シャープ画像
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)
ソフトライト
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)
ハードライト
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)
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)
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)
参考
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を使ったほうがよいでしょう。
参考
線画抽出
gray = img.convert("L")
gray2 = gray.filter(ImageFilter.MaxFilter(5))
senga_inv = ImageChops.difference(gray, gray2)
senga = ImageOps.invert(senga_inv)
こちらの方法を参考にしました、非常に素晴らしいです。
-
2017-02-07修正 'Image.getcount'となっていましたが、正しくは'Image.getcolors'です、修正いたします。 ↩