Python, NumPyで画像処理(読み込み、演算、保存)
画像ファイルをNumPy配列ndarray
として読み込むと、NumPyの機能を使って様々な画像処理を行うことができる。
要素(画素)の値の取得や書き換え、スライスでのトリミング、結合などndarray
の操作がそのまま使えるので、NumPyに慣れている人はOpenCVなどのライブラリを使わなくても様々な処理ができる。
OpenCVを使う場合も、PythonのOpenCVでは画像データをndarray
として扱うので、NumPy(ndarray
)での処理を覚えておくと何かと便利。ほかにも、scikit-imageなど、画像をndarray
として扱うライブラリは多い。
ここでは、基本的な画像ファイルの読み書きの方法として、
- 画像ファイルをNumPy配列
ndarray
として読み込む方法 - NumPy配列
ndarray
を画像ファイルとして保存する方法
と、NumPy(ndarray
)での画像処理の例、
- 画素値の取得と変更
- 単色化と画像の結合(連結)
- ネガポジ反転(画素値の逆転)
- 減色処理
- 二値化処理
- ガンマ補正(四則演算)
- スライスでトリミング
- スライスや関数で分割
- スライスで貼り付け
- アルファブレンドとマスク処理
- 回転と上下左右反転
- 2枚の画像の比較
について説明する。
ここではPillowを使って画像ファイルをndarray
として読み込み、保存する方法について述べる。OpenCVでの画像ファイルの読み込みと保存については以下の記事参照。
Pillowについては以下の記事も参照。画像の読み込みや保存、リサイズや回転などの簡単な処理はPillowのみで実行可能。
Pillow, NumPy, OpenCVの使い分けについては以下の記事を参照。
Web上の画像をファイルとして保存したい場合は以下の記事を参照。
画像ファイルをNumPy配列ndarrayとして読み込む方法
以下の画像を例とする。
np.array()
にPIL.Image.open()
で読み込んだ画像データを渡すと形状shape
が(行(高さ), 列(幅), 色(チャンネル))
の三次元の配列ndarray
が得られる。
from PIL import Image
import numpy as np
im = np.array(Image.open('data/src/lena.jpg'))
print(type(im))
# <class 'numpy.ndarray'>
print(im.dtype)
# uint8
print(im.shape)
# (225, 400, 3)
色(チャンネル)の並びはRGB(赤、緑、青)の順。OpenCVのcv2.imread()
で読み込んだ場合(BGR)と異なるので注意。
convert('L')
で白黒(グレースケール)画像にしてから変換すると二次元のndarray
として読み込まれる。
im_gray = np.array(Image.open('data/src/lena.jpg').convert('L'))
print(im_gray.shape)
# (225, 400)
なお、np.asarray()
でも同様にndarray
が得られるが、np.array()
は書き換え可能なndarray
を返すのに対し、np.asarray()
は書き換え禁止のndarray
を返す。
np.array()
の場合。要素の値を変更可能。
print(im.flags.writeable)
# True
print(im[0, 0, 0])
# 109
im[0, 0, 0] = 0
print(im[0, 0, 0])
# 0
np.asarray()
の場合。書き換え禁止なので、読み込んだndarray
を元に新たなndarray
を生成したりするのは問題ないが、値を直接変更することはできない。
im_as = np.asarray(Image.open('data/src/lena.jpg'))
print(type(im_as))
# <class 'numpy.ndarray'>
print(im_as.flags.writeable)
# False
# im_as[0, 0, 0] = 0
# ValueError: assignment destination is read-only
データ型dtype
はuint8
(8ビット符号なし整数)で読み込まれる。浮動小数点数float
として処理したい場合は、astype()
で変換するほか、np.array()
やnp.asarray()
の第二引数にデータ型を指定することもできる。
im_f = im.astype(np.float64)
print(im_f.dtype)
# float64
im_f = np.array(Image.open('data/src/lena.jpg'), np.float64)
print(im_f.dtype)
# float64
ndarray
のデータ型についての詳細は以下の記事を参照。
NumPy配列ndarrayを画像ファイルとして保存する方法
Image.fromarray()
にndarray
を渡すとPIL.Image
が得られ、そのsave()
メソッドで画像ファイルとして保存できる。保存されるファイルのフォーマットはsave()
の引数に指定したパスの拡張子から自動的に判定される。
pil_img = Image.fromarray(im)
print(pil_img.mode)
# RGB
pil_img.save('data/temp/lena_save_pillow.jpg')
白黒画像(二次元配列)も指定可能。自動的にmode
が'L'
(白黒=グレースケール)となる。そのままsave()
で保存可能。
pil_img_gray = Image.fromarray(im_gray)
print(pil_img_gray.mode)
# L
pil_img_gray.save('data/temp/lena_save_pillow_gray.jpg')
保存するだけなら一行で書いてもいい。
Image.fromarray(im).save('data/temp/lena_save_pillow.jpg')
Image.fromarray(im_gray).save('data/temp/lena_save_pillow_gray.jpg')
ndarray
のデータ型dtype
がfloat
などの場合はエラーとなるため、uint8
に変換する必要がある。
# pil_img = Image.fromarray(im_f)
# TypeError: Cannot handle this data type
pil_img = Image.fromarray(im_f.astype(np.uint8))
pil_img.save('data/temp/lena_save_pillow.jpg')
astype()
による型変換では特にスケーリングなどは行われないので、画素値が0.0 - 1.0
で表されている場合は255
を乗算してからuint8
に変換して保存する必要がある。要注意。
なお、save()
ではフォーマットに応じたパラメータを引数で指定できる。詳細はImage file formatを参照。例えばjpg
の場合は引数quality
で品質を指定できる。1
(最低)から95
(最高)まででデフォルトは75
。実際は100
まで設定できるが95
より大きい値は推奨されていない。
画素値の取得と変更
インデックス[]
で座標を指定することで、画素値を取得できる。
行, 列
の順番で指定する。xy座標で考えるとy, x
の順番なので注意。原点は左上となる。
from PIL import Image
import numpy as np
im = np.array(Image.open('data/src/lena.jpg'))
print(im.shape)
# (225, 400, 3)
print(im[100, 150])
# [111 81 109]
print(type(im[100, 150]))
# <class 'numpy.ndarray'>
上の例は(y, x) = (100, 150)
つまり100行150列目の画素の値を示している。上述の通り、Pillowを利用してndarray
を取得した場合の色の並びはRGBの順なので、(R, G, B) = (111, 81, 109)
という結果。
アンパックで別々の変数に代入することもできる。
R, G, B = im[100, 150]
print(R)
# 111
print(G)
# 81
print(B)
# 109
インデックス[]
で色まで指定して値を取得することも可能。
print(im[100, 150, 0])
# 111
print(im[100, 150, 1])
# 81
print(im[100, 150, 2])
# 109
新たな値への変更もできる。RGBまとめてでも、単色だけでもOK。
im[100, 150] = (0, 50, 100)
print(im[100, 150])
# [ 0 50 100]
im[100, 150, 0] = 150
print(im[100, 150])
# [150 50 100]
なお、実際には後述の例のように画像全体あるいはスライスで範囲を指定して処理する場合が多く、個別の画素の値を変更することはあまりないかもしれない。
NumPy配列ndarray
の値の取得や変更についての詳細は以下の記事を参照。
単色化と画像の結合(連結)
他の色の値を0にして単色画像を生成する。さらに横に並べて結合する。
結合についての詳細は以下の記事参照。
from PIL import Image
import numpy as np
im = np.array(Image.open('data/src/lena_square.png'))
im_R = im.copy()
im_R[:, :, (1, 2)] = 0
im_G = im.copy()
im_G[:, :, (0, 2)] = 0
im_B = im.copy()
im_B[:, :, (0, 1)] = 0
# 横に並べて結合(どれでもよい)
im_RGB = np.concatenate((im_R, im_G, im_B), axis=1)
# im_RGB = np.hstack((im_R, im_G, im_B))
# im_RGB = np.c_['1', im_R, im_G, im_B]
pil_img = Image.fromarray(im_RGB)
pil_img.save('data/dst/lena_numpy_split_color.jpg')
元画像。以降の例でも使用。
処理結果。

一つの色を選ぶのではなくRGB画像を白黒画像に変換することもできる。
ネガポジ反転(画素値を逆転)
画素値を計算して処理するのも簡単。
Max値(uint8
の場合は255)から画素値を引くとネガポジ反転した画像が取得できる。
import numpy as np
from PIL import Image
im = np.array(Image.open('data/src/lena_square.png').resize((256, 256)))
im_i = 255 - im
Image.fromarray(im_i).save('data/dst/lena_numpy_inverse.jpg')
なお、resize()
で画像を縮小しているが、元のサイズが大きすぎるので、便宜上、小さくしているだけ。特に意味はない。以降の例でも同様。
減色処理
//
で割り算の余りを切り捨てて再度掛け算すると画素値が飛び飛びの値になり、色数を減らせる。
import numpy as np
from PIL import Image
im = np.array(Image.open('data/src/lena_square.png').resize((256, 256)))
im_32 = im // 32 * 32
im_128 = im // 128 * 128
im_dec = np.concatenate((im, im_32, im_128), axis=1)
Image.fromarray(im_dec).save('data/dst/lena_numpy_dec_color.png')
二値化処理
しきい値に応じて白黒に振り分けることも可能。
詳細は以下の記事を参照。
ガンマ補正(四則演算)
掛け算、割り算、累乗、なんでもできる。
画像全体をそのまま計算できるので、forループなどを用いる必要はない。
from PIL import Image
import numpy as np
im = np.array(Image.open('data/src/lena_square.png'))
im_1_22 = 255.0 * (im / 255.0)**(1 / 2.2)
im_22 = 255.0 * (im / 255.0)**2.2
im_gamma = np.concatenate((im_1_22, im, im_22), axis=1)
pil_img = Image.fromarray(np.uint8(im_gamma))
pil_img.save('data/dst/lena_numpy_gamma.jpg')

計算の結果、numpy.ndarray
のデータ型dtype
が浮動小数点数float
に変換される。最後に保存する際にuint8
に変換する必要があるので注意。
スライスでトリミング
スライスで領域を指定すると、矩形にトリミングすることもできる。
from PIL import Image
import numpy as np
im = np.array(Image.open('data/src/lena_square.png'))
print(im.shape)
# (512, 512, 3)
im_trim1 = im[128:384, 128:384]
print(im_trim1.shape)
# (256, 256, 3)
Image.fromarray(im_trim1).save('data/dst/lena_numpy_trim.jpg')
numpy.ndarray
におけるスライスについての詳細は以下の記事を参照。
左上の座標とトリミングする領域の幅・高さで指定する関数を用意しておくと便利かもしれない。
def trim(array, x, y, width, height):
return array[y:y + height, x:x+width]
im_trim2 = trim(im, 128, 192, 256, 128)
print(im_trim2.shape)
# (128, 256, 3)
Image.fromarray(im_trim2).save('data/dst/lena_numpy_trim2.jpg')
画像のサイズ外を指定すると無視される。
im_trim3 = trim(im, 128, 192, 512, 128)
print(im_trim3.shape)
# (128, 384, 3)
Image.fromarray(im_trim3).save('data/dst/lena_numpy_trim3.jpg')
スライスや関数で分割
スライスを用いると画像を分割できる。画像の端からトリミングしているのと同じ。
from PIL import Image
import numpy as np
im = np.array(Image.open('data/src/lena_square.png').resize((256, 256)))
print(im.shape)
# (256, 256, 3)
im_0 = im[:, :100]
im_1 = im[:, 100:]
print(im_0.shape)
# (256, 100, 3)
print(im_1.shape)
# (256, 156, 3)
Image.fromarray(im_0).save('data/dst/lena_numpy_split_0.jpg')
Image.fromarray(im_1).save('data/dst/lena_numpy_split_1.jpg')
NumPyの関数で分割することも可能。
横に分割するのはnp.hsplit()
。第二引数に整数値を指定するとその個数に等分割される。結果の画像は省略。
im_0, im_1 = np.hsplit(im, 2)
print(im_0.shape)
# (256, 128, 3)
print(im_1.shape)
# (256, 128, 3)
第二引数にリストを指定すると、その値の位置で分割される。
im_0, im_1, im_2 = np.hsplit(im, [100, 150])
print(im_0.shape)
# (256, 100, 3)
print(im_1.shape)
# (256, 50, 3)
print(im_2.shape)
# (256, 106, 3)
縦に分割するのはnp.vsplit()
。引数の考え方はnp.hsplit()
と同じ。
np.hsplit()
やnp.vsplit()
で第二引数に整数値を指定する場合、等分割できない(割り切れない)とエラーになるが、np.array_split()
を使うと適当にサイズを調整して分割してくれる。
# im_0, im_1, im_2 = np.hsplit(im, 3)
# ValueError: array split does not result in an equal division
im_0, im_1, im_2 = np.array_split(im, 3, axis=1)
print(im_0.shape)
# (256, 86, 3)
print(im_1.shape)
# (256, 85, 3)
print(im_2.shape)
# (256, 85, 3)
np.hsplit()
やnp.vsplit()
, np.array_split()
についての詳細は以下の記事を参照。
また、縦横に等間隔で分割(同じサイズのブロックに分割)したい場合はscikit-imageのskimage.util.view_as_blocks()
が便利。
スライスで貼り付け
スライスを使うと配列の矩形領域を別の配列の矩形領域で置き換えることができる。
これを利用すると画像の一部または全体を別の画像に貼り付けられる。
import numpy as np
from PIL import Image
src = np.array(Image.open('data/src/lena_square.png').resize((128, 128)))
dst = np.array(Image.open('data/src/lena_square.png').resize((256, 256))) // 4
dst_copy = dst.copy()
dst_copy[64:128, 128:192] = src[32:96, 32:96]
Image.fromarray(dst_copy).save('data/dst/lena_numpy_paste.jpg')
dst_copy = dst.copy()
dst_copy[64:192, 64:192] = src
Image.fromarray(dst_copy).save('data/dst/lena_numpy_paste_all.jpg')
左辺と右辺のスライスで同じサイズの領域を指定しないとエラーになるので注意。
アルファブレンドとマスク処理
配列の要素ごとの演算も簡単にできるので、2つの画像をアルファブレンドしたり、別途用意したマスク画像をもとに合成したりできる。詳細は以下の記事を参照。
回転と上下左右反転
配列を回転したり上下左右に反転したりする関数もある。
元画像。
90度回転。
上下反転。
2枚の画像の比較
2枚の画像を比較して、完全一致しているか判定したり差分画像を生成したりすることができる。
元画像。
劣化画像。
差分。