アルファチャンネルつき png を透過画像で貼り付ける with Python/OpenCV

# TL; DR

`bg.jpg` の左上に、`png_image.png` （アルファチャンネルつき画像）を重ねる場合のコードです。

```python
import cv2

frame = cv2.imread("bg.jpg")
png_image = cv2.imread("alpha.png", cv2.IMREAD_UNCHANGED)  # アルファチャンネル込みで読み込む

# 貼り付け先座標の設定。とりあえず左上に
x1, y1, x2, y2 = 0, 0, png_image.shape[1], png_image.shape[0]

# 合成!
frame[y1:y2, x1:x2] = frame[y1:y2, x1:x2] * (1 - png_image[:, :, 3:] / 255) + \
                      png_image[:, :, :3] * (png_image[:, :, 3:] / 255)
```

bg.jpg | alpha.png | result
---- | ---- | ----
<img height="200" alt="bg_moon_sky_mangetsu_building.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141307/acbce1d2-00b0-bb82-bcbc-3d9bf780eb7f.jpeg"> | <img width="50" alt="stand_naname3_man.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141307/0c40be45-013e-6680-c05c-6ba81c2440b5.png"> | <img height="200" alt="testout.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141307/0fca6937-aba9-7f4d-008c-49c14d6aef01.jpeg">

最後に、関数化したバージョンも載せています。

# 簡単な解説

PNG ファイルには「アルファチャンネル」という各ピクセルの透明度を表すデータが入っています。値域は RGB と同じく 0-255 です。255 のときに 100% 有効で、0 のときに 0% (完全に透明)になります。


```python
# 画像の読み込み
png_image = cv2.imread("alpha.png", cv2.IMREAD_UNCHANGED)  # アルファチャンネル込みで読み込む
```

通常の `cv2.imread()` では、`[h, w, 3]` の `numpy.ndarray` の形になりますが、 `cv2.IMREAD_UNCHANGED` を指定して `cv2.imread()` を呼び出すと、`[h, w, 4]` の形になります。前者ではおなじみの `BGR` 形式になり、後者だと `BGRA` と最後にアルファチャンネルがつきます。

画像を読み込めたら、合成します。といっても、NuPy の通常の行列演算で合成可能です。やっていることは、元々の背景画像・描画する画像を、それぞれアルファチャンネルの数値で配分して、足し合わせています。

```python
# 合成!
frame[y1:y2, x1:x2] = frame[y1:y2, x1:x2] * (1 - png_image[:, :, 3:] / 255) + \
                      png_image[:, :, :3] * (png_image[:, :, 3:] / 255)
```

`png_image[:, :, 3:]` がアルファチャンネルの取り出しです。アルファチャンネルの値域は 0-255 なので、255 で割って 0-1 の比率にします。描画する画像には計算した比率を掛け、背景のほうには比率の "残り" を掛けて、両者を足し合わせることで、最終的な画像を得ることが出来ます。

ちなみに、`png_image[:, :, 3]` と書くと、行列のサイズが合わないと怒られるので注意してください（間違えた）。

# 余録

色々参考になるサイトはありつつ、そのものズバリなコードをうまく検索できなかったので、記事を作ってみました。（当たり前すぎてかえって書かなかったりするのかもしれませんね）

# おまけ: 関数化バージョン・はみ出した座標に対応

```python
import cv2
import numpy as np


def alpha_blend(frame: np.array, alpha_frame: np.array, position: (int, int)):
    """
    frame に alpha_frame をアルファブレンディングで描画する。

    :param frame: ベースとなるフレーム。frame に直接、書き込まれるので、中身が変更される。
    :param alpha_frame: 重ね合わる画像フレーム。アルファチャンネルつきで読み込まれている前提。
    :param position: alpha_frame を描画する座標 (x, y)。負の値などはみ出る値も指定可能。
    :return: 戻り値はなし。frame に直接、描画する。

    usage:
    base_frame = cv2.imread("bg.jpg")
    png_image = cv2.imread("alpha.png", cv2.IMREAD_UNCHANGED)  # アルファチャンネル込みで読み込む
    alpha_blend(base_frame, png_image, (1500, 300))
    """
    # 貼り付け先座標の設定 - alpha_frame がはみ出す場合への対処つき
    x1, y1 = max(position[0], 0), max(position[1], 0)
    x2 = min(position[0] + alpha_frame.shape[1], frame.shape[1])
    y2 = min(position[1] + alpha_frame.shape[0], frame.shape[0])
    ax1, ay1 = x1 - position[0], y1 - position[1]
    ax2, ay2 = ax1 + x2 - x1, ay1 + y2 - y1

    # 合成!
    frame[y1:y2, x1:x2] = frame[y1:y2, x1:x2] * (1 - alpha_frame[ay1:ay2, ax1:ax2, 3:] / 255) + \
                          alpha_frame[ay1:ay2, ax1:ax2, :3] * (alpha_frame[ay1:ay2, ax1:ax2, 3:] / 255)
```

`alpha_frame` がはみ出すケースにも対応しています。ただし、完全にはみ出している `position` を指定した場合は、例外が出ます。


# 参考にさせてもらったサイト

- [OpenCVでアルファチャンネル付きpngを表示する \- Qiita](https://qiita.com/locutus1990/items/f0e2e62a7c4935dffe7c)
- [Python, OpenCV, NumPyで画像のアルファブレンドとマスク処理 \| note\.nkmk\.me](https://note.nkmk.me/python-opencv-numpy-alpha-blend-mask/)

画像は [かわいいフリー素材集 いらすとや](https://www.irasutoya.com/) の素材を使わせてもらいました。