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

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

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

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

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

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

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

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

OpenCV: Operations on arrays: 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()を使います。

OpenCV: Operations on arrays: 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()もあります。

OpenCV: Operations on arrays

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とすれば問題ありません。 順番を考えたくない場合は対象となる全ての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.newaxisを使う方法もあります。

# 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とすれば自動的に計算される。省略はできないので注意。 そのほかの平滑化関数は以下の公式ドキュメントを参照。

OpenCV: Image Filtering

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のマスク処理の項を参照。

シェア

関連カテゴリー

Python OpenCV NumPy 画像処理

Python, NumPy(OpenCV)で画像を二値化処理 Python, OpenCV, Pillow(PIL)で画像サイズ(幅、高さ)を取得 Pythonでの画像処理、Pillow, NumPy, OpenCVの違いと使い分け NumPy配列ndarrayをタイル状に繰り返し並べるnp.tile Python, OpenCVで顔検出と瞳検出(顔認識、瞳認識) Python, NumPyで画像処理(読み込み、演算、保存) NumPy配列ndarrayをシフト(スクロール)させるnp.roll Python, NumPyでグラデーション画像を生成 Python, OpenCVでBGRとRGBを変換するcvtColor NumPyでRGB画像の色チャンネルを分離して単色化、白黒化、色交換 Python, OpenCVで画像を縦・横に連結 (hconcat, vconcat, np.tile) Python, OpenCVで図形描画(線、長方形、円、矢印、文字など) Python, OpenCVで画像ファイルの読み込み、保存(imread, imwrite) Python, OpenCVで幾何変換(アフィン変換・射影変換など) Python, OpenCVで三角形・四角形領域を変形して別画像に貼り付け

Last Updated: 6/26/2019, 10:34:03 PM