alphamapを使って合成してみた


# Background

通常のマスク画像を使った画像合成では、表示させたい部分を白くして行列のかけ算で処理します。([Python, OpenCV, NumPyで画像のアルファブレンドとマスク処理](https://note.nkmk.me/python-opencv-numpy-alpha-blend-mask/)を参照)

しかし、alphamatで出力された画像の画素は`0~255`の範囲で、`0(黒)or255(白)`とくっきりと分かれてないです。ここでは、alphamatを使って背景画像と合成をかけてみようと思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/67fc9efb-2183-aa72-2a1c-26f74c2c4d1d.jpeg" width="30%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/c72d39d2-f98b-e9ec-0fae-31828c375bde.png" width="30%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/dedc0ef7-ea4f-8143-15a6-a89550f3216f.png" width="30%">


(alphamatの画像処理方法は[OpenCVでalphamatを使ってみた](https://qiita.com/satsukiya/items/edcf552cc02e5b0288b1)を参照)

# Method

ポイントは3つ
1. 処理する画像は`0~255`から`0.0~1.0`に標準(ノルム)化にする 
2. アルファ値(透過度)のまま画像とかけ算をする
3. マスクした箇所と背景の箇所をそれぞれ別個で処理する

##### 処理する画像は`0~255`から`0.0~1.0`に標準(ノルム)化にする 
次のアルファ値とのかけ算し易いように`0.0~1.0`に変換します。おそらく標準では `CV_8UC3` `(unsigned char 8bit - 3チャネル)`なのでfloat型 `CV_32FC3`にして、 `1/255`にします。

例)

```c++
    srcMat.convertTo(srcMat, CV_32FC3, 1.0/255);
``` 

また、 もし処理結果を保存したいときは`0.0~1.0`のままだと真っ黒な画像が出来てしまうので、`CV_8UC3`に再変換する必要があります。

例)

```c++
    srcMat.convertTo(srcMat, CV_8UC3, 255);
``` 

##### アルファ値(透過度)のまま画像とかけ算をする

各画素同士のかけ算をする場合は `cv::multiply` を使います。 出力結果は3つ目の引数に反映されます。

```c++
    cv::multiply(maskMat, srcMat, srcMat);
```

##### マスクした箇所と背景の箇所をそれぞれ別個で処理する

今回の場合はマスク画像の白黒によって、表示するorしないといった判定だけではなくアルファ値によって表示を調整しています。例としてあるマスク画素のアルファ値が20%の場合は、表示画素20%:背景画素80%の配分で合成します。

```c++
    cv::multiply(maskMat, srcMat, srcMat);
    cv::multiply(cv::Scalar::all(1.0)-maskMat, backMat, backMat);
    cv::add(srcMat, backMat, dstMat);
```

# Process

#### c++

```c++
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/core/utility.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/alphamat.hpp>

int main(int argc, const char* argv[])
{
    cv::String keys = "{src||}""{back||}""{mask||}";
    cv::CommandLineParser parser(argc, argv, keys);
    cv::String src_path = parser.get<cv::String>("src");
    cv::String mask_path = parser.get<cv::String>("mask");
    cv::String back_path = parser.get<cv::String>("back");

    std::string name = "Effect";
    cv::Mat srcMat = cv::imread(src_path, cv::IMREAD_COLOR);
    cv::Mat maskMat = cv::imread(mask_path);
    cv::Mat backMat = cv::imread(back_path, cv::IMREAD_COLOR);

    srcMat.convertTo(srcMat, CV_32FC3, 1.0/255);
    backMat.convertTo(backMat, CV_32FC3, 1.0/255);
    maskMat.convertTo(maskMat, CV_32FC3, 1.0/255);

    cv::Mat dstMat = cv::Mat::zeros(srcMat.size(), srcMat.type());

    cv::multiply(maskMat, srcMat, srcMat);
    cv::multiply(cv::Scalar::all(1.0)-maskMat, backMat, backMat);

    cv::add(srcMat, backMat, dstMat);

    srcMat.convertTo(srcMat, CV_8UC3, 255);
    backMat.convertTo(backMat, CV_8UC3, 255);
    dstMat.convertTo(dstMat, CV_8UC3, 255);
    
    cv::namedWindow(name, cv::WINDOW_AUTOSIZE);

    while(true){

        cv::imshow("src-mask", srcMat);
        cv::imshow("dst-mask", backMat);
        cv::imshow(name, dstMat);
        int key = cv::waitKey(0);
        if (key == 27){
            break;
        } else if(key == 's'){
            cv::imwrite("output_front.png", srcMat);
            cv::imwrite("output_back.png", backMat);
            cv::imwrite("output_blend.png", dstMat);
        }
    }

    cv::destroyAllWindows();    
    return 0;
}

```

```sh
./blend -src=[元画像] -mask=[マスク画像] -back=[背景画像]
```

# Consequence

##### 読み込んだ画像

|元画像|trimap(マスク画像)|背景画像|
|:--:|:--:|:--:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/67fc9efb-2183-aa72-2a1c-26f74c2c4d1d.jpeg" width="100%">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/f32f15bf-33d3-9542-565e-a4300df186ac.png" width="100%">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/4a86a5b2-9134-eeee-31f7-e930654cb261.png" width="100%">|

trimap(マスク画像)はちょっと変えています。髪全体にすると黒い部分が残ってしまい、背景画素が使われるので、輪郭部分を灰色にしています。

##### 処理中の画像

|元画像 × マスク画像|背景画像 × マスク画像|
|:--:|:--:|:--:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/dac84fc2-23da-ecda-ac81-325447ced566.png" width="100%">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/8f2e5831-ea83-d08c-cf48-5fa999606295.jpeg" width="100%">|

##### 処理した画像

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/68f5280f-2d41-fcfd-82f6-1c4ef4b4dd87.jpeg" width="100%">

輪郭に白い部分が残って少し気になりますが、微細な輪郭を残したまま合成ができていることがわかります。

# PostScript
今回の輪郭の設定はアノテーションツールを使って手動でセットしました。
DNNを使って物体をセグメンテーション(領域分割)してくれる方法があるので、輪郭の部分を灰色にしたtrimapを作成することで自動化ができそうです。
しかし、今回使った高解像度の画像を使うと処理時間に数分かかります。:stopwatch:


# Reference
- [Alpha Blending using OpenCV (C++ / Python)](https://learnopencv.com/alpha-blending-using-opencv-cpp-python/)
- [cv::Multiply](http://opencv.jp/opencv-2svn/cpp/operations_on_arrays.html#cv-multiply)
- [OpenCVでalphamatを使ってみた](https://qiita.com/satsukiya/items/edcf552cc02e5b0288b1)
- [ぱくたそ:新しいアプリを宣伝するキラキラ広報のフリー素材](https://www.pakutaso.com/20160953274post-9133.html)
- [Designing Effective Inter-Pixel Information Flow for Natural Image Matting](http://yaksoy.github.io/papers/CVPR17-ifm.pdf)