Python, OpenCV, NumPyで画像のアルファブレンドとマスク処理
Python, OpenCVで画像のアルファブレンドとマスクによる合成処理を行う。OpenCVの関数を使わなくてもNumPyの機能で実現できるので合わせて説明する。NumPyの配列操作のほうが簡単かつ柔軟なのでオススメ。
ここでは以下の内容について説明する。
- OpenCVでアルファブレンド:
cv2.addWeighted()
- OpenCVでマスク処理:
cv2.bitwise_and()
- NumPyでアルファブレンド
- NumPyでマスク処理
- NumPyで複雑なアルファブレンドとマスク処理
- OpenCVの図形描画によるマスク画像の作成
画像処理ライブラリPillowを使ったアルファブレンド、マスク処理については以下の記事を参照。
Pillow, NumPy, OpenCVによる画像処理の比較については以下の記事を参照。
サンプルコードでは以下の画像を使う。
サンプルコードの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)
第五引数gamma
はすべての画素値に加えられる値。
dst = cv2.addWeighted(src1, 0.5, src2, 0.2, 128)
cv2.imwrite('data/dst/opencv_add_weighted_gamma.jpg', dst)
上の結果から分かるように最大値(uint8
では255)を超えてもオーバーフローして異常な値になることはないが、データ型によっては適切に処理されない場合があるので注意。そのようなときはndarray
のclip()
メソッドを使う。後述のNumPyによるアルファブレンドの項を参照。
OpenCVでマスク処理: cv2.bitwise_and()
OpenCVでマスク処理を行うにはcv2.bitwise_and()
を使う。
dst = cv2.bitwise_and(src1, src2[, dst[, mask]])
cv2.bitwise_and()
は名前の通りビット単位のAND処理を行う関数。入力画像src1
とsrc2
の各画素ごとの値の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)
画像ファイルを読み込む場合はデータ型が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')
なお、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でマスク処理
NumPyの配列操作を利用するとマスク処理も簡単。
同じ形状shape
のndarray
同士の四則演算は同じ位置の画素ごとの演算となる。
データ型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')
なお、この例で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
同士を自動的に適宜変換して演算を行うブロードキャストと呼ばれる仕組みがあるが、エラーメッセージの通り上の例の組み合わせではブロードキャストが適切に行われない。
- 関連記事: NumPyのブロードキャスト(形状の自動変換)
二次元の単色画像にもう一次元加えるとうまくブロードキャストされる。
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.newaxis
やnp.expand_dims()
を使う方法もある。
# mask = mask[:, :, np.newaxis]
NumPyで複雑なアルファブレンドとマスク処理
上のアルファブレンドの例では画像全面に一律の割合で合成したが、NumPyの配列を演算する場合、別の画像(配列)をもとに合成することもできる。
以下のようなグラデーション画像を使う。グラデーション画像はNumPyを利用して生成可能。
シンプルな演算で合成できる。グラデーション画像の画素値によってアルファ値(ブレンドする割合)が変化する画像が出力される。
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')
さらに別の画像でマスクしたい場合も簡単。
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')
OpenCVの図形描画によるマスク画像の作成
幾何学的なマスク画像はOpenCVの図形描画関数を利用して作成できる。
処理したい画像が決まっていれば、np.zeros_like()
でその画像と同じ形状shape
ですべての要素が0
のndarray
が生成できる。元画像と同じサイズの黒画像に相当する。
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)
図形描画の詳細は以下の記事を参照。
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)
dst = src * (mask_blur / 255)
cv2.imwrite('data/dst/opencv_draw_mask_blur_result.jpg', dst)
なお、dst = src * (mask_blur / 255)
の部分をdst = src * mask_blur / 255
とすると想定の結果とならないので注意。NumPyのマスク処理の項を参照。
また、マスクとして使うndarray
が単色で二次元配列(色の次元なし)の場合はもう一次元加えないと演算できない。こちらもNumPyのマスク処理の項を参照。