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

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

# 目次
[OpenCVでスプライトを半透明にする #1　～おばけを出現させる～](https://qiita.com/mo256man/items/5dbad10308a7d2698400)
OpenCVでスプライトを半透明にする #2　～グラデーションな半透明を作る～　←今ここ

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

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

```python: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()`を使った。
だが、自分でも気づかぬうちにより汎用的な方法を思いついていた。[この記事](https://qiita.com/mo256man/items/8a9bb7bb66e107fd7153)で。

https://qiita.com/mo256man/items/8a9bb7bb66e107fd7153

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

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

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

```python: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にしておこう。

```python: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`、スカラーと行列の乗算をするだけ。

|前景部分は半透明<br>背景を消すマスク|
|:-:|
|![mask_with_alpha.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/f22a7251-a0e6-4fe8-dae1-50884d3b161d.png)|

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

### 前回
|前景部分は合成<br>背景部分は黒|前景部分は黒<br>背景部分はそのまま|→|完成形|
|:-:|:-:|:-:|:-:|
|![compo_p.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/1236f121-bc9e-0806-f359-6578fc5a2055.png)|![compo_n.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/596a7f50-2022-f302-cb5e-4e7aa0239bc3.png)|→|![compo_np.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/4c1a93ce-006a-af41-a4ba-5f12a7679cc0.png)|

### 今回
|前景部分は半透明<br>背景部分は黒|前景部分は半透明<br>背景部分はそのまま|→|完成形|
|:-:|:-:|:-:|:-:|
|![front_with_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/0a787ef2-8cc7-d71e-e152-78e1da402704.png)|![back_with_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/be9ff023-99f6-9c8a-51fd-f7ff2a1974a2.png)|→|![compo_np.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/4c1a93ce-006a-af41-a4ba-5f12a7679cc0.png)|


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

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

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

|完成形|
|:-:|
|![compo_plus_back_grad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/6b6721d9-f258-3b45-8f98-9651acdaf838.png)|

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

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

```python: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()
```

|前景|同サイズの<br>グラデーション|
|:-:|:-:|
|![ghost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/8acb7b35-82aa-c9c1-40cc-63ee338b6072.png)|![grad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/9a7615b9-4510-c148-f51f-1682a5087ccd.png)|


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

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

|前景を残し<br>背景を消すマスク|グラデーション|→|前景がｸﾞﾗﾃﾞｰｼｮﾝで<br>背景を消すマスク|
|:-:|:-:|:-:|:-:|
|![mask_p.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/4884a602-91a2-c620-dfda-9347a0cd6f86.png)|![grad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/9a7615b9-4510-c148-f51f-1682a5087ccd.png)|→|![grad_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/401b81e3-f3b9-f6d4-aa3c-1ff4d70ab543.png)|

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

|前景|マスク|→|前景部分は半透明<br>背景は黒|
|:-:|:-:|:-:|:-:|
|![front3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/f4c45df1-1eec-7f34-6202-724608ee69da.png)|![grad_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/401b81e3-f3b9-f6d4-aa3c-1ff4d70ab543.png)|→|![front_with_mask_grad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/60b8c31d-3bda-5ea5-0562-092cc00765c2.png)|

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

|背景|マスク反転|→|前景部分は半透明<br>背景は残る|
|:-:|:-:|:-:|:-:|
|![roi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/f66d3753-d57d-e47e-ebf8-6c6264c341b0.png)|![grad_mask_n.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/68ae42f8-5528-6d2f-850f-bb2438f181c5.png)|→|![back_with_mask_grad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/655ec722-32d5-6f82-b868-e66db174b54e.png)|

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

|前景部分は半透明<br>背景部分は黒|前景部分は半透明<br>背景部分はそのまま|→|完成形|
|:-:|:-:|:-:|:-:|
|![front_with_mask_grad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/a0f061d4-e647-b987-8a43-314e23d7c12e.png)|![back_with_mask_grad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/655ec722-32d5-6f82-b868-e66db174b54e.png)|→|![compo_plus_back_grad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/6b6721d9-f258-3b45-8f98-9651acdaf838.png)|

## ソース

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

|アニメGIF|
|:-:|
|![anim_grad.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/f2965bff-4baa-9914-752a-801c3d2d05c8.gif)|

<details><summary>ソースコード（折りたたみ）</summary>

```python:
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()
```
</details>

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