1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenCVでスプライトを半透明にする #2 ~グラデーションな半透明を作る~

Posted at

はじめに

連休を楽しむために、宿題を片付けよう。

目次

OpenCVでスプライトを半透明にする #1 ~おばけを出現させる~
OpenCVでスプライトを半透明にする #2 ~グラデーションな半透明を作る~ ←今ここ

行列の計算をnumpyでおこなう

まず、前回の関数を再掲する。関数だけなのでこれだけでは動かないことに注意。

sprite_alpha( )
import cv2

def sprite_alpha(back, front4, alpha):
    front = front4[:, :, :3]                        # RGB(3ch)
    mask = front4[:, :, 3]                          # A要素(1ch)
    mask = cv2.merge((mask, mask, mask))            # 3chにする

    # 全体を合成
    compo = cv2.addWeighted(front, alpha, back, 1-alpha, 0)
    # マスク処理
    compo_with_mask = cv2.bitwise_and(compo, mask)
    back_with_mask = cv2.bitwise_and(back, 255-mask)

    # 必要に応じて有効化してください
    # cv2.imshow("compo", compo)
    # cv2.imshow("mask", mask)
    # cv2.imshow("255-mask", 255-mask)
    # cv2.imshow("compo_with_mask", compo_with_mask)
    # cv2.imshow("back_with_mask", back_with_mask)

    # 完成形
    result = compo_with_mask + back_with_mask
    return result

さて、これまでの記事ではマスク処理でcv2.bitwise_and()を使った。
だが、自分でも気づかぬうちにより汎用的な方法を思いついていた。この記事で。

同サイズの二つの行列の要素ごとの積を計算するアダマール積。これがマスクとして使える(使っている)ことに当時は気づいていなかったわけだ。

さらに、前回、cv2.AddWeighted(src1, alpha, src2, beta, gamma)alpha * src1 + beta * src2 + gamma という行列の計算をしていると述べた。

つまり、cv2.bitwise_and()cv2.AddWeighted()を使わず、行列の計算式で上の関数を書き直すことができるというわけだ。
ついでにcv2.merge()もnumpy関数に変えちゃえ。

OpenCVの関数を使わない方法 sprite_alpha_np( )
import cv2
import numpy as np

def sprite_alpha_np(back, front4, alpha):
    front = front4[:, :, :3]                        # RGB(3ch)
    mask = front4[:, :, 3]                          # A要素(1ch)
    mask = np.dstack((mask, mask, mask))            # 3chにする

    # 全体を合成
    compo = (alpha * front + (1-alpha) * back).astype(np.uint8)
    # マスク処理
    compo_with_mask = (1/255 * compo * mask).astype(np.uint8)
    back_with_mask = (1/255 * back * (255-mask)).astype(np.uint8)

    # 必要に応じて有効化してください
    # cv2.imshow("compo", compo)
    # cv2.imshow("mask", mask)
    # cv2.imshow("255-mask", 255-mask)
    # cv2.imshow("compo_with_mask", compo_with_mask)
    # cv2.imshow("back_with_mask", back_with_mask)

    # 完成形
    result = compo_with_mask + back_with_mask
    return result

0~255の値を持つ行列要素同士の積なので、計算結果は0~65535…じゃないや0~65025の値を取りうる。アダマール積の行列のデータ型は元の行列と同じくuint8で0~255の値が格納される。これでは正しい画像として表示することができない。
そこで最初に255で割って0.0~255.0の小数行列にして、それをuint8に変換している。

マスクを0.0~1.0の小数の行列として扱う

マスクの行列を255で割って0.0~1.0の小数の行列にすれば都度255で割る必要はなくなる。
これでコードがかなりシンプルになることだろう。
ただしこの状態だと先程と同様、画像が0.0~255.0の小数値になってしまう。

だからマスクだけでなく画像の行列も255で割ってしまおう。そうすれば計算結果も0.0~1.0の小数値の行列になる。
こういうも正規化というのかな。

cv2.imshow()は0~255の整数行列だけでなく0.0~1.0の小数行列でも正しく表示できる。だから途中の確認は問題ない。ただし0.0~1.0の小数値の行列ではcv2.imwrite()で画像を保存できないので注意。

後でどう使われるかわからないので、関数の戻り値だけはこれまで通り255をかけてuint8にしておこう。

0~1の小数の行列で計算する方法 sprite_alpha_norm( )
import cv2
import numpy as np

def sprite_alpha_norm(back, front4, alpha):
    front = 1 /255 * front4[:, :, :3]               # RGB(3ch)0~1の小数値
    mask = 1 / 255 * front4[:, :, 3]                # A要素(1ch)0~1の小数値
    mask = np.dstack((mask, mask, mask))            # 3chにする
    back = 1 / 255 * back                           # 背景も0~1の小数値にする

    # 全体を合成
    compo = alpha * front + (1-alpha) * back
    # マスク処理
    compo_with_mask = compo * mask
    back_with_mask = back * (1-mask)

    # 必要に応じて有効化してください
    # cv2.imshow("compo", compo)
    # cv2.imshow("mask", mask)
    # cv2.imshow("1-mask", 1-mask)
    # cv2.imshow("compo_with_mask", compo_with_mask)
    # cv2.imshow("back_with_mask", back_with_mask)

    # 完成形
    result = (255*(compo_with_mask + back_with_mask)).astype(np.uint8)
    return result

アルファ値とマスクを合成する

必須ではないが、アルファ値とマスクを合成しておきたい。つまり、これを作りたい。
計算は alpha * mask、スカラーと行列の乗算をするだけ。

前景部分は半透明
背景を消すマスク
mask_with_alpha.png

このマスクを使うと、これまでとは違う中間画像が得られる。
もちろん完成形は同じ。ただの私のこだわりだ。

前回

前景部分は合成
背景部分は黒
前景部分は黒
背景部分はそのまま
完成形
compo_p.png compo_n.png compo_np.png

今回

前景部分は半透明
背景部分は黒
前景部分は半透明
背景部分はそのまま
完成形
front_with_mask.png back_with_mask.png compo_np.png

グラデーションな半透明を作る

スカラーのアルファ値とマスク行列を同等に扱うことができるようになった。
cv2.addWeighted()では重みはスカラー値でしか指定できなかったが、行列を重みとして指定することができるようになった。

次は一つの画像の中でアルファ値が変化しているこんな画像を目指す。

完成形
compo_plus_back_grad.png

グラデーション画像を作る

まず、グラデーション画像を作る関数を考えておく。

make_grad( )
import cv2
import numpy as np
import math

def make_grad(height, width):
    grad = np.empty((height, width, 3), np.uint8)
    for h in range(height):
        value = int(255 * (1 - math.cos(2*math.pi*h/height)) / 2)   # 0~255の正弦波
        grad[h, :] = value
    return grad

front4 = cv2.imread("ghost.png", -1)
height, width = front4.shape[:2]
grad = make_grad(height, width)
cv2.imshow("grad", grad)
cv2.waitKey(0)
cv2.destroyAllWindows()
前景 同サイズの
グラデーション
ghost.png grad.png

グラデーションなマスクを作る

先ほどアルファ値とマスクを合成したが、同じやり方でグラデーションとマスクを合成する。
numpyではスカラーと行列の乗算はk * Aと書き、行列の要素ごとの積はA * Bと書く。つまりスカラーも行列も計算は同じだ。

前景を残し
背景を消すマスク
グラデーション 前景がグラデーションで
背景を消すマスク
mask_p.png grad.png grad_mask.png

ここまで作れば後は同じなのだが、せっかくなので中間画像を置いておく。
前景にマスク処理するとこうなる。

前景 マスク 前景部分は半透明
背景は黒
front3.png grad_mask.png front_with_mask_grad.png

また、背景にマスク(をネガポジ反転したもの)を適用させるとこうなる。

背景 マスク反転 前景部分は半透明
背景は残る
roi.png grad_mask_n.png back_with_mask_grad.png

二つの中間画像を合体すればグラデーションな半透明を持つ画像の合成のできあがりだ。

前景部分は半透明
背景部分は黒
前景部分は半透明
背景部分はそのまま
完成形
front_with_mask_grad.png back_with_mask_grad.png compo_plus_back_grad.png

ソース

ここまで理解のために中間画像を変数に格納して表示してきたが、実際はalpha * src1 + beta * src2 + gamma で一発で計算できるので、本番では中間画像を作らない仕様とした。
せっかくなのでグラデーションが動くサンプルを示す。

アニメGIF
anim_grad.gif
ソースコード(折りたたみ)
import cv2
import numpy as np
import math

def make_grad(height, width):
    grad = np.empty((height, width, 3), np.uint8)
    for h in range(height):
        value = int(255 * (1 - math.cos(2*math.pi*h/height)) / 2)   # 0~255の正弦波
        grad[h, :] = value
    return grad


def sprite_grad(back, front4, grad):
    front = 1 /255 * front4[:, :, :3]               # RGB(3ch)0.0~1.0の小数値
    mask = 1 / 255 * front4[:, :, 3]                # A要素(1ch)0.0~1.0の小数値
    mask = np.dstack((mask, mask, mask))            # 3chにする
    back = 1 / 255 * back                           # 背景も0.0~1.0の小数値にする
    grad = 1 / 255 * grad                           # グラデーションも0.0~1.0の小数値にする

    grad_mask = grad * mask                         # 前景マスクとグラデーションを合成する
    compo = grad_mask * front + (1 - grad_mask) * back  # cv2.AddWeighted()に相当する計算
    result = (255 * compo).astype(np.uint8)         # 0~255の整数値にする
    return result


# 前景
front_RGBA = cv2.imread("ghost.png", -1)        # 4ch

# 背景
back_origin = cv2.imread("background.png")      # 背景画像
height, width = front_RGBA.shape[:2]
x, y = 200, 200
back = back_origin[y:y+height, x:x+width]       # 前景と同サイズの背景

# グラデーション
grad = make_grad(height, width)

while True:
    image = sprite_grad(back, front_RGBA, grad)
    cv2.imshow("image", image)
    if cv2.waitKey(10) == 27:
        break
    grad = np.roll(grad, 1, axis=0)
cv2.destroyAllWindows()

終わりに

0~255の整数値で計算したり、0.0~1.0の小数値で計算したり。
OpenCVの関数を使ったり、numpyの関数を使ったり。
いろいろな方法で実装できたが、結局どれが一番速いのか。
次回は処理速度の検証をします。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?