note.nkmk.me

Python, NumPyで画像処理(読み込み、演算、保存)

Posted: 2015-08-22 / Modified: 2019-12-16 / Tags: 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として読み込む方法

以下の画像を例とする。

lena

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

データ型dtypeuint8(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のデータ型dtypefloatなどの場合はエラーとなるため、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')

元画像。以降の例でも使用。

lena square

処理結果。

NumPy image processing split color

ネガポジ反転(画素値を逆転)

画素値を計算して処理するのも簡単。

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')

Python NumPy inverse

なお、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')

Python NumPy decrease color

二値化処理

しきい値に応じて白黒に振り分けることも可能。

詳細は以下の記事を参照。

Python NumPy OpenCV binarization

ガンマ補正(四則演算)

掛け算、割り算、累乗、なんでもできる。

画像全体をそのまま計算できるので、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 image processing split gamma

計算の結果、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 image trimming 1

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')

numpy image trimming 2

画像のサイズ外を指定すると無視される。

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')

numpy image trimming 3

スライスや関数で分割

スライスを用いると画像を分割できる。画像の端からトリミングしているのと同じ。

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 image split 0

numpy image split 1

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.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')

numpy image paste

dst_copy = dst.copy()
dst_copy[64:192, 64:192] = src

Image.fromarray(dst_copy).save('data/dst/lena_numpy_paste_all.jpg')

numpy image paste all

左辺と右辺のスライスで同じサイズの領域を指定しないとエラーになるので注意。

アルファブレンドとマスク処理

配列の要素ごとの演算も簡単にできるので、2つの画像をアルファブレンドしたり、別途用意したマスク画像をもとに合成したりできる。詳細は以下の記事を参照。

NumPy image alpha blend gradation

NumPy image blend blur

回転と上下左右反転

配列を回転したり上下左右に反転したりする関数もある。

元画像。

lena

90度回転。

numpy rot90 image

上下反転。

nupmy flipud image

2枚の画像の比較

2枚の画像を比較して、完全一致しているか判定したり差分画像を生成したりすることができる。

元画像。

lena

劣化画像。

lena low quality

差分。

lena low quality difference abs normalize

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事