二次元画像を「映え」させる【GrabCut】

##はじめに

最近流行っているじゃないですか、iPhoneXやPixel 3で人やものを撮影すると背景がぼけるこれ、
ポートレートモード。
<img width="500" alt="SS" src="https://qiita-image-store.s3.amazonaws.com/0/119392/d1d791bb-4676-54f0-6ec8-9c0a7638c132.jpeg">

モデル: 同じチームの[ozpa](http://ozpa-h4.com/)さん
[iPhoneXS cameras](https://www.apple.com/jp/iphone-xs/cameras/) 


これをですね、**深度情報**もない**顔情報**も上手く取れない、アニメ、ゲーム、マンガなどの**二次元画像**でもやりたい。
**二次元画像を「映え」させたい**。

ということで、GrabCutというアルゴリズムを用いて前景/後景を対話的に分離し、
背景をぼかして「映え」させる、**iOSアプリ**を作りました。

結果は、

これが
<img width="500" alt="SS-before" src="https://qiita-image-store.s3.amazonaws.com/0/119392/1c6955f2-a204-5d0c-e02b-73b0b38357dc.jpeg">
こう
<img width="500" alt="SS-after" src="https://qiita-image-store.s3.amazonaws.com/0/119392/aec41828-f664-7607-5723-a170a0a6be3e.jpeg">

これが
<img width="500" alt="SS-before" src="https://qiita-image-store.s3.amazonaws.com/0/119392/1b889e02-bde6-ee80-ebd6-a59054606295.jpeg">
こう
<img width="500" alt="SS-after" src="https://qiita-image-store.s3.amazonaws.com/0/119392/42668207-d889-30d8-a351-09ffbdcc06f2.jpeg">



こいつらはこんな手順で
<img width="500" alt="SS-after" src="https://qiita-image-store.s3.amazonaws.com/0/119392/8599fc23-e7b9-af16-ed72-1db4157676a1.gif">

簡単に✨**映える**✨


アルゴリズムと実装の説明をします。

##GrabCut
GrabCutはこちらの[論文](https://dl.acm.org/citation.cfm?id=1015720)で提案された、なるべく少ないユーザ入力から画像中の前景領域を抽出するアルゴリズムです。
**Gaussian Mixture Model（GMM）**というクラスタリング手法を用いて、画素が前景である確率、後景である確率を求めます。

####クラスタリングのイメージ
まず、画像を下記のような隣接する画素同士を連結したグラフとして扱います。
<img width="300" alt="SS-after" src="https://qiita-image-store.s3.amazonaws.com/0/119392/26b919fa-2b1a-92b3-785f-fa9e60d40cee.png">

次に、例えば、類似した2つの画素は同一の前景または後景である可能性が高く、分離して欲しくないので、2点間の辺に重めのコストをかけます。
全ての辺のコストを計算し、最もコストがかからない、できるだけ滑らかな境界の切断を繰り返し求める、最小化問題を解く雰囲気で領域を分割します。

AとBの境界の切断例
<img width="600" alt="SS-after" src="https://qiita-image-store.s3.amazonaws.com/0/119392/4c2ea7b3-a0f2-aa05-3965-7795b9874955.png">

####ユーザによる入力
面白いのは、結果をよくするためにユーザによる入力を対話的に利用すること。
ユーザは初期値として、前景領域の周りに長方形を描きます。初期値を元に上記のクラスタリングを適応すると、たまに、そうじゃないんだよおおお、おしいいいってことがあります。そうじゃない場所に、「ここは後景ではなく前景だよ」といった線を引くと、それを元にコストを再計算。最終的に綺麗に分割できます。

入力← → 出力
<img width="400" alt="SS-after" src="https://qiita-image-store.s3.amazonaws.com/0/119392/500aa491-9da2-c8fa-81fb-19c21c123169.jpeg">
長方形と白ペン黒ペンで前景/後景情報を入力している
引用: [OpenCV-Pythonチュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_grabcut/py_grabcut.html)

##実装
今回はアルゴリズムの実装をOpenCV、インターフェースの実装をみなさんにお任せして、
OpenCVでどう映えアプリの機能を実装するかをコメント多めで載せまする。

###アプリ作成手順
1. OpenCVを使えるようにする
2. GrabCutする🔪
3. 背景をぼかす📷
4. 合成する

####1. OpenCVを使えるようにする

1. [公式](https://opencv.org/releases.html)からframeworkをダウンロード
2. プロジェクトにドラッグ&ドロップ
3. コードを書くファイルを用意

[File] > [New] > [Flie] > [Cocoa Touch Class] の Language: Objective-Cを追加
ここでは「OpenCVManager.h / OpenCVManager.m」と命名

Create Bridging Header
OpenCVManager.m > OpenCVManager.mm に変更
4.  OpenCVのimport

```objective_c:OpenCVManager.mm
//最初にOpenCVのヘッダーをimportすること
#import <opencv2/opencv.hpp>
#import <opencv2/imgcodecs/ios.h>
#import "OpenCVManager.h"
```

####2. GrabCutする
はじめに、ユーザが入力した長方形から、前景らしい領域を白塗りしたマスク画像を生成します。

```objective_c:OpenCVManager.mm
-(UIImage*)doGrabCut:(UIImage*)sourceImage foregroundRect:(CGRect)rect iterationCount:(int)iterationCount {
    //UIImageをMatに変換
    cv::Mat image;
    UIImageToMat(sourceImage, image);
    //RGBA > RGB
    cv::cvtColor(image, image, CV_RGBA2RGB);
    //CGRectをRectに変換
    cv::Rect rectangle(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);

    //GrabCut
    //mask: 結果
    //bgModel, fgMolde: 処理で使用する配列
    //iterationCount: 処理を繰り返す回数
    cv::grabCut(image, mask, rectangle, bgModel, fgModel, iterationCount, cv::GC_INIT_WITH_RECT);

    //結果から、前景らしい領域（GC_PR_FGD）を抽出
    cv::Mat1b fgMask;
    cv::compare(mask, cv::GC_PR_FGD, fgMask, cv::CMP_EQ);
    return MatToUIImage(fgMask);
}
```

次に👆の結果をさらによくするために、前景領域、後景領域の指定をユーザから雑に入力されたマスク画像を用いて、再計算を繰り返します。

```objective_c:OpenCVManager.mm
-(UIImage*)doGrabCut:(UIImage*)sourceImage maskImage:(UIImage*)maskImage iterationCount:(int)iterationCount {
    cv::Mat image;
    UIImageToMat(sourceImage, image);
    cv::cvtColor(image, image, CV_RGBA2RGB);

    //ユーザ入力されたマスク画像を既存の結果と合成する
    cv::Mat maskImg;
    UIImageToMat(maskImage, maskImg);
    cv::Mat1b markers = [self cvMatMaskFromUIImage:maskImage];

    //GrabCut
    cv::grabCut(image, markers, cv::Rect(), bgModel, fgModel, iterationCount, cv::GC_INIT_WITH_MASK);

    //GC_FGDをGC_PR_FGDに変換
    cv::MatIterator_<unsigned char> itd = markers.begin();
    cv::MatIterator_<unsigned char> itd_end = markers.end();
    for(int i=0; itd != itd_end; ++itd, ++i) {
        if (*itd == cv::GC_FGD) {
            *itd = cv::GC_PR_FGD;
        }
    }

    cv::Mat1b fgMask;
    cv::compare(markers, cv::GC_PR_FGD, fgMask, cv::CMP_EQ);
    return MatToUIImage(fgMask);
}
```

ユーザから入力されたマスク画像を既存の結果と合成するコードはこちら


```objective_c:OpenCVManager.mm
-(cv::Mat1b)cvMatMaskFromUIImage:(UIImage*)image {
    //マスク画像の画素を抽出
    CGImageRef imageRef = [image CGImage];
    NSUInteger width = CGImageGetWidth(imageRef);
    NSUInteger height = CGImageGetHeight(imageRef);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    unsigned char *rawData = (unsigned char*) calloc(height * width * 4, sizeof(unsigned char));
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * width;
    NSUInteger bitsPerComponent = 8;
    CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    CGContextRelease(context);

    //既存の結果と合成
    cv::Mat1b markers = mask;
    uchar* data =  markers.data;
    for(int x = 0; x < width; x++) {
        for( int y = 0; y < height; y++) {
            NSUInteger byteIndex = ((image.size.width  * y) + x ) * 4;
            UInt8 red   = rawData[byteIndex];
            UInt8 green = rawData[byteIndex + 1];
            UInt8 blue  = rawData[byteIndex + 2];
            UInt8 alpha = rawData[byteIndex + 3];
            if(red == 0 && green == 0 && blue == 255 && alpha == 255) {//青領域を前景
                data[width * y + x] = cv::GC_FGD;
            } else if(red == 255 && green == 0 && blue == 0 && alpha == 255) {//赤領域を後景
                data[width * y + x] = cv::GC_BGD;
            }
        }
    }
    free(rawData);
    return markers;
}

```

####3. 背景をぼかす
「映え」るぼかしをしましょう。[ガウシアン](http://opencv.jp/opencv-2.1/cpp/image_filtering.html#GaussianBlur)推し。

```objective_c:OpenCVManager.mm
//image: 入力
//blurImage: 出力
//sigumaX: XYの標準偏差、X=Y=0の時blurSizeから決定する
double sigumaX = 0;
cv::GaussianBlur(image, blurImage, cv::Size(blurSize, blurSize), sigumaX);
```
（境界のぼかしを良い感じにする手法は別で紹介予定）

####4. 合成する
生成した、前景マスク、ぼかし画像、元画像を合成しましょう。

```objective_c:OpenCVManager.mm
//前景マスクから後景マスク生成
bitwise_not(fgMask, bgMask);

//合成
cv::Mat bgImg, fgImg, result;
cv::bitwise_and(blurImage, blurImage, bgImg, bgMask);
cv::bitwise_and(image, image, fgImg, fgMask);
cv::add(bgImg, fgImg, result);
return MatToUIImage(result);

```

あとはこやつらをSwiftから呼び出して処理すれば完成🎉
<img width="700" alt="SS" src="https://qiita-image-store.s3.amazonaws.com/0/119392/d0afbccb-6bf5-a86d-e0d0-8b04850bad50.png">
<img width="700" alt="SS" src="https://qiita-image-store.s3.amazonaws.com/0/119392/4a1e201b-2bb1-9c10-0581-f5c9845f725f.png">


##おわりに+追記
**2019/03/27**
ここからさらにさらに！実装を追加して、理想の二次元映えを突き詰めたアプリがこちらになります！
try! Swiftでお話しさせていただきました✨
[koooootake/Portrait-without-Depth-ios](https://github.com/koooootake/Portrait-without-Depth-ios)
登壇資料はこちら
[speakerdeck](https://speakerdeck.com/koooootake/making-portrait-mode-yourself)


自分が欲しいものを作るの楽しいいい
本当に映えさせたい3Dゲームのスクリーンショットや二次元美少女画像の結果は[@koooootake](https://twitter.com/koooootake)にあげるのでみてね！

前景/後景分離は、深度、顔、の他にもディープなラーニングなど様々なアプローチがありそうなので実装してみたい（追記: [note](https://note.mu/koooootake/n/n7fb25676359f)に実装の試行錯誤書きました）。話題の[remove](https://www.remove.bg/)とか、素晴らしい。

####宣伝
[DeNA Tech Con](https://techcon.dena.com/2019/)、2月6日、きてね！
神武が所属するマンガボックス、[サーバー開発エンジニア急募](https://www.wantedly.com/projects/201893?fbclid=IwAR3e4bMBEhoQBNPUlBTcy0zEy0cw4IoBbi_Uy4TqsDDH1OHVXbRJIHI3tVY)、きてね！