Python, OpenCV, NumPyで画像のアルファブレンドとマスク処理

Posted: | Tags: Python, OpenCV, NumPy, 画像処理

Python, OpenCVで画像のアルファブレンドとマスクによる合成処理を行う。OpenCVの関数を使わなくてもNumPyの機能で実現できるので合わせて説明する。NumPyの配列操作のほうが簡単かつ柔軟なのでオススメ。

ここでは以下の内容について説明する。

  • OpenCVでアルファブレンド: cv2.addWeighted()
  • OpenCVでマスク処理: cv2.bitwise_and()
  • NumPyでアルファブレンド
  • NumPyでマスク処理
  • NumPyで複雑なアルファブレンドとマスク処理
  • OpenCVの図形描画によるマスク画像の作成

画像処理ライブラリPillowを使ったアルファブレンド、マスク処理については以下の記事を参照。

Pillow, NumPy, OpenCVによる画像処理の比較については以下の記事を参照。

サンプルコードでは以下の画像を使う。

lena

rocket

mask horse

サンプルコードのOpenCVのバージョンは4.0.1。OpenCV3系と4系はあまり変わらないはずだが、OpenCV2系は異なっている可能性があるので注意。

OpenCVでアルファブレンド: cv2.addWeighted()

OpenCVでアルファブレンドを行うにはcv2.addWeighted()を使う。

dst = cv2.addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]])

引数の値に応じて以下のように計算される。

dst = src1 * alpha + src2 * beta + gamma

合成する2つの画像は同じサイズである必要があるのでリサイズしておく。

import cv2

src1 = cv2.imread('data/src/lena.jpg')
src2 = cv2.imread('data/src/rocket.jpg')

src2 = cv2.resize(src2, src1.shape[1::-1])

NumPy配列ndarrayとして読み込んだ画像のサイズの取得については以下の記事を参照。

第二引数alphaと第四引数betaの値に従って画像がアルファブレンドされる。なお、ここでは画像をファイルとして保存しているが、別ウィンドウで表示したい場合はcv2.imshow()を使えばよい(例: cv2.imshow('window_name', dst))。以降のサンプルコードでも同じ。

dst = cv2.addWeighted(src1, 0.5, src2, 0.5, 0)

cv2.imwrite('data/dst/opencv_add_weighted.jpg', dst)

OpenCV addWeighted()

第五引数gammaはすべての画素値に加えられる値。

dst = cv2.addWeighted(src1, 0.5, src2, 0.2, 128)

cv2.imwrite('data/dst/opencv_add_weighted_gamma.jpg', dst)

OpenCV addWeighted() with gamma

上の結果から分かるように最大値(uint8では255)を超えてもオーバーフローして異常な値になることはないが、データ型によっては適切に処理されない場合があるので注意。そのようなときはndarrayclip()メソッドを使う。後述のNumPyによるアルファブレンドの項を参照。

OpenCVでマスク処理: cv2.bitwise_and()

OpenCVでマスク処理を行うにはcv2.bitwise_and()を使う。

dst = cv2.bitwise_and(src1, src2[, dst[, mask]])

cv2.bitwise_and()は名前の通りビット単位のAND処理を行う関数。入力画像src1src2の各画素ごとの値のANDが出力画像の画素値となる。

ここではsrc2にマスク画像として白黒画像を読み込み処理する。

src2 = cv2.imread('data/src/horse_r.png')

src2 = cv2.resize(src2, src1.shape[1::-1])

print(src2.shape)
# (225, 400, 3)

print(src2.dtype)
# uint8

dst = cv2.bitwise_and(src1, src2)

cv2.imwrite('data/dst/opencv_bitwise_and.jpg', dst)

OpenCV bitwise_and()

画像ファイルを読み込む場合はデータ型がuint8(符号なし8ビット整数: 0 - 255)となり、黒が画素値0(2進数で表すと0b00000000)、白が画素値255(2進数で表すと0b11111111))となるのでビット演算の結果が分かりやすいが、浮動小数点数floatの場合は2進数表現にした上でビット演算が行われて予期せぬ結果となるので注意。

後述のNumPyによるマスク処理の方が理解しやすいかもしれない。

なお、OpenCVにはcv2.bitwise_and()の他にもOR演算を行うcv2.bitwise_or()、XOR(排他的論理和)演算を行うcv2.bitwise_xor()、NOT演算を行うcv2.bitwise_not()もある。

NumPyでアルファブレンド

NumPyでは配列の画素ごとの四則演算が簡単にできるので、アルファブレンドもシンプルな式で実現できる。

ここでは画像処理ライブラリPillowを用いて画像ファイルをNumPy配列ndarrayとして読み込んでいる。リサイズもPillowのメソッドで行っている。

OpenCVのcv2.imread()でもndarrayとして読み込まれるのでどちらを使っても構わないが、色の並びが異なるので注意。

ndarrayとスカラー値の演算は各要素の値とスカラー値との演算となるので、アルファブレンドは以下のように計算できる。データ型が自動的にキャストされるのでPillowで画像ファイルとして保存する場合は注意。

import numpy as np
from PIL import Image

src1 = np.array(Image.open('data/src/lena.jpg'))
src2 = np.array(Image.open('data/src/rocket.jpg').resize(src1.shape[1::-1], Image.BILINEAR))

print(src1.dtype)
# uint8

dst = src1 * 0.5 + src2 * 0.5

print(dst.dtype)
# float64

Image.fromarray(dst.astype(np.uint8)).save('data/dst/numpy_image_alpha_blend.jpg')

NumPy image alpha blend

なお、Pillowのsave()メソッドでjpgファイルとして保存する場合、引数qualityで品質を指定できる(例では省略しているのでデフォルトのまま)。

OpenCVのcv2.addWeighted()における引数gammaのように各画素に一律に値を加えたい場合も簡単。以下のように各色に異なる値を加えることができる。上述のように、画像ファイルの読み込み方法によって色の並びが異なるので注意。

clip()メソッドで最小値0, 最大値255に収めている。uint8の最大値255を超えた値があると画像ファイルとして保存するときに想定外の結果となるので注意。

dst = src1 * 0.5 + src2 * 0.2 + (96, 128, 160)

print(dst.max())
# 311.1

dst = dst.clip(0, 255)

print(dst.max())
# 255.0

Image.fromarray(dst.astype(np.uint8)).save('data/dst/numpy_image_alpha_blend_gamma.jpg')

NumPy image alpha blend with gamma

NumPyでマスク処理

NumPyの配列操作を利用するとマスク処理も簡単。

同じ形状shapendarray同士の四則演算は同じ位置の画素ごとの演算となる。

データ型uint8として読み込まれた白黒画像は黒が0、白が255となるが、これを255で割ることで、黒が0.0、白が1.0となり、さらに元の画像と乗算すると白1.0の部分だけが残りマスク処理が実現できる。

import numpy as np
from PIL import Image

src = np.array(Image.open('data/src/lena.jpg'))
mask = np.array(Image.open('data/src/horse_r.png').resize(src.shape[1::-1], Image.BILINEAR))

print(mask.dtype, mask.min(), mask.max())
# uint8 0 255

mask = mask / 255

print(mask.dtype, mask.min(), mask.max())
# float64 0.0 1.0

dst = src * mask

Image.fromarray(dst.astype(np.uint8)).save('data/dst/numpy_image_mask.jpg')

NumPy image mask

なお、この例でdst = src * mask / 255とすると、先にsrc * maskがデータ型uint8のまま計算されて値が丸められてから255で除算されるので想定の結果とならない。dst = src * (mask / 255)dst = mask / 255 * srcとすればOK。

順番を考えたくない場合は対象となる全てのndarrayを浮動小数点数float型にキャストしてから演算を行うという方法もある。そちらのほうがミスが少ないかもしれない。

マスク画像が単色画像で二次元(色の次元なし)のndarrayの場合は注意が必要。そのまま乗算を行うとエラーになる。

mask = np.array(Image.open('data/src/horse_r.png').convert('L').resize(src.shape[1::-1], Image.BILINEAR))

print(mask.shape)
# (225, 400)

mask = mask / 255

# dst = src * mask
# ValueError: operands could not be broadcast together with shapes (225,400,3) (225,400) 

NumPyには次元や形状が異なるndarray同士を自動的に適宜変換して演算を行うブロードキャストと呼ばれる仕組みがあるが、エラーメッセージの通り上の例の組み合わせではブロードキャストが適切に行われない。

二次元の単色画像にもう一次元加えるとうまくブロードキャストされる。

mask = mask.reshape(*mask.shape, 1)

print(mask.shape)
# (225, 400, 1)

dst = src * mask

Image.fromarray(dst.astype(np.uint8)).save('data/dst/numpy_image_mask_l.jpg')

ndarrayの形状を変換するreshape()メソッドに元の配列の形状shapeを展開して渡している。

reshape()メソッドではなくnp.newaxisnp.expand_dims()を使う方法もある。

# mask = mask[:, :, np.newaxis]

NumPyで複雑なアルファブレンドとマスク処理

上のアルファブレンドの例では画像全面に一律の割合で合成したが、NumPyの配列を演算する場合、別の画像(配列)をもとに合成することもできる。

以下のようなグラデーション画像を使う。グラデーション画像はNumPyを利用して生成可能。

gradation image

シンプルな演算で合成できる。グラデーション画像の画素値によってアルファ値(ブレンドする割合)が変化する画像が出力される。

import numpy as np
from PIL import Image

src1 = np.array(Image.open('data/src/lena.jpg'))
src2 = np.array(Image.open('data/src/rocket.jpg').resize(src1.shape[1::-1], Image.BILINEAR))

mask1 = np.array(Image.open('data/src/gradation_h.jpg').resize(src1.shape[1::-1], Image.BILINEAR))

mask1 = mask1 / 255

dst = src1 * mask1 + src2 * (1 - mask1)

Image.fromarray(dst.astype(np.uint8)).save('data/dst/numpy_image_ab_grad.jpg')

NumPy image alpha blend gradation

さらに別の画像でマスクしたい場合も簡単。

mask2 = np.array(Image.open('data/src/horse_r.png').resize(src1.shape[1::-1], Image.BILINEAR))

mask2 = mask2 / 255

dst = (src1 * mask1 + src2 * (1 - mask1)) * mask2

Image.fromarray(dst.astype(np.uint8)).save('data/dst/numpy_image_ab_mask_grad.jpg')

NumPy image alpha blend gradation mask

OpenCVの図形描画によるマスク画像の作成

幾何学的なマスク画像はOpenCVの図形描画関数を利用して作成できる。

処理したい画像が決まっていれば、np.zeros_like()でその画像と同じ形状shapeですべての要素が0ndarrayが生成できる。元画像と同じサイズの黒画像に相当する。

import cv2
import numpy as np

src = cv2.imread('data/src/lena.jpg')

mask = np.zeros_like(src)

print(mask.shape)
# (225, 400, 3)

print(mask.dtype)
# uint8

np.zeros()で任意のサイズを指定してもよい。詳細は以下の記事を参照。

ここに図形描画関数で任意の図形を描画する。矩形はcv2.rectangle()、円はcv2.circle()、多角形はcv2.fillConvexPoly()を使う。矩形と円はthickness=-1とすると塗りつぶしになる。

cv2.rectangle(mask, (50, 50), (100, 200), (255, 255, 255), thickness=-1)
cv2.circle(mask, (200, 100), 50, (255, 255, 255), thickness=-1)
cv2.fillConvexPoly(mask, np.array([[330, 50], [300, 200], [360, 150]]), (255, 255, 255))

cv2.imwrite('data/dst/opencv_draw_mask.jpg', mask)

OpenCV draw mask

図形描画の詳細は以下の記事を参照。

cv2.GaussianBlur()などで平滑化(ぼかし)処理を行うと境界がなめらかになるので、マスク処理でなめらかに合成できる。

cv2.GaussianBlur()の第二引数にはx方向・y方向のカーネルサイズをタプルで指定する。それぞれの値を大きくするとその方向のぼかし幅が大きくなる。値は奇数である必要がある。第三引数にはガウシアンの標準偏差値を指定するが、0とすれば自動的に計算される。省略はできないので注意。

そのほかの平滑化関数は以下の公式ドキュメントを参照。

mask_blur = cv2.GaussianBlur(mask, (51, 51), 0)

cv2.imwrite('data/dst/opencv_draw_mask_blur.jpg', mask_blur)

OpenCV draw mask blur

dst = src * (mask_blur / 255)

cv2.imwrite('data/dst/opencv_draw_mask_blur_result.jpg', dst)

NumPy image blend blur

なお、dst = src * (mask_blur / 255)の部分をdst = src * mask_blur / 255とすると想定の結果とならないので注意。NumPyのマスク処理の項を参照。

また、マスクとして使うndarrayが単色で二次元配列(色の次元なし)の場合はもう一次元加えないと演算できない。こちらもNumPyのマスク処理の項を参照。

関連カテゴリー

関連記事