OpenCVで透明人間

先日，透明人間になって写真や動画を撮影できるAndroidアプリ[「Invisible Camera」](https://play.google.com/store/apps/details?id=com.Takaya.InvisibleCamera)をリリースしました．OpenCV for Unityで作りました．せっかくなのでOpenCVで透明人間になる方法をメモとして残します．

<blockquote class="twitter-tweet" data-lang="ja"><p lang="und" dir="ltr"><a href="https://t.co/dBQhTwqNO4">pic.twitter.com/dBQhTwqNO4</a></p>&mdash; 敬也 (@takaya901) <a href="https://twitter.com/takaya901/status/1077173442770862080?ref_src=twsrc%5Etfw">2018年12月24日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

なお，下記ソースコードは以下のようにusing staticでクラス名を省略しています．

```cs
using static OpenCVForUnity.Imgproc;
using static OpenCVForUnity.Core;
```

####HSV画像に変換
```cs
GaussianBlur(webcamMat, blurred, new Size(7, 7), 0);
cvtColor(blurred, hsv, COLOR_RGBA2RGB);
cvtColor(hsv, hsv, COLOR_RGB2HSV);
```
webcamMatはカメラ映像をMatに変換したものです．OpenCV for Unityでは，WebCamTextureToMatHelperというクラスのGetMat()メソッドを使うとカメラ映像をRGBAのMatとして取得できます．別にRGBでも構いません．

後述のinRangeメソッドが使えるように，入力画像をHSVに変換します．ノイズ除去はお好みで．
COLOR_RGBA2RGBはないのでRGBを経由しています．

####肌と髪の後ろにある背景を抽出する
```cs
//肌領域が255，それ以外が0のマスクを作成
//Hが0~180で循環しているので，両端とも取り出す
inRange(hsv, SKIN_LOWER1, SKIN_UPPER1, skinMask1);
inRange(hsv, SKIN_LOWER2, SKIN_UPPER2, skinMask2);

//髪（黒）領域が255，それ以外が0のマスクを作成
bitwise_or(skinMask1, skinMask2, skinMask1);
inRange(hsv, HAIR_LOWER, HAIR_UPPER, hairMask);

//肌と髪の領域が255のマスクを作成
bitwise_or(skinMask1, hairMask, skinAndHairMask);

//肌と髪の領域と同じ位置の背景を切り出す
bitwise_and(bg, bg, bgOnSkinAndHair, skinAndHairMask);
```
bgは背景（Background）のRGBA画像です．

inRangeメソッドで指定した範囲内の画素を255（白），範囲外の画素を0（黒）としたマスクを作成します．範囲は以下のように設定しました．

```cs
Scalar SKIN_LOWER1 = new Scalar(  0,  30,  88); //H（0~180）の左側  
Scalar SKIN_UPPER1 = new Scalar( 25, 173, 255);             
Scalar SKIN_LOWER2 = new Scalar(160,  30,  88); //H（0~180）の右側
Scalar SKIN_UPPER2 = new Scalar(180, 173, 255);            
Scalar HAIR_LOWER  = new Scalar(  0,   0,   0);                   
Scalar HAIR_UPPER  = new Scalar(255, 150,  90);              
```
bitwise_orは，ざっくり言うと2つのマスクの0でない領域を足し合わせます．

bitwise_andは，本来は2つの画像の両方で0でない領域だけを抽出するメソッドです．上記コードでは同じ画像のandを取っているのでそれだけだとbgがbgOnSkinAndHairにコピーされるだけですが，第4引数にマスク画像を指定することでマスク画像で0でない領域のみ抽出することができます．これはbitwise_orでも同じです．

skinAndHairMaskとbgOnSkinAndHairはそれぞれ以下のような画像になります（毎回撮り直しているので少しずれてますが気にしないでください）．

<img width="512" alt="Screen Shot 2018-12-27 at 4.56.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/239014/2660f4c2-371b-019a-9d92-e7a4de7991ba.png">

<img width="510" alt="Screen Shot 2018-12-27 at 5.22.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/239014/2391c37f-1de8-1fe5-080c-7c3e56f6880b.png">

####カメラ映像から肌と髪以外の領域を抽出する
```cs
bitwise_not(skinAndHairMask, skinAndHairMask);
bitwise_and(webcamMat, webcamMat, withoutSkinAndHair, skinAndHairMask);
```
skinAndHairMaskを反転してからカメラ映像に適用することで，髪と肌以外の領域を抽出します．反転後のskinAndHairMaskとwithoutSkinAndHairは以下のようになります．

<img width="512" alt="uTZ16fzhmZnyA1V1545856493_1545856500.png" src="https://qiita-image-store.s3.amazonaws.com/0/239014/9bd5ca35-19df-8b01-1712-e2fef972b109.png">

<img width="518" alt="Screen Shot 2018-12-27 at 5.26.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/239014/c3bde216-0712-bc77-4131-4e1815646315.png">

####肌と神の領域を背景で置換
```cs
bitwise_or(withoutSkinAndHair, bgOnSkinAndHair, invisible);
```
withoutSkinAndHairの0領域にbgOnSkinAndHairの0でない領域がはめ込まれるイメージです．invisibleは以下のようになります．

<img width="548" alt="Screen Shot 2018-12-27 at 5.29.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/239014/a59229a3-5e3d-9440-2179-e80d75e1e401.png">