OpenCVを使った肌色領域の検出(C++)

#初めに
どうもこんにちは。学校の課題でOpenCVを使った実習があったのでそこで自分がやったことを軽くまとめておきます。具体的には肌色領域を検出して手の動きを測定しました。今回は肌色検出の部分を紹介します。

#手順
ここから画像の前処理についてつらつら書いていくので、コードだけに興味がある人は飛ばしてもらって構わないです。

## 実行環境
OS:Ubuntu 18.04.3 LTS
コンパイラ:7.4.0

##平滑化
今回はガウシアンフィルタによる平滑化を行いました。画像処理ではとりあえず平滑化はやっとけみたいな感じがありますよね。バイラテラルフィルタを使うとエッジが比較的きれいにとれますが、処理が重くなっていますのでカメラとかを使っている人でフレームレートが気になる人は無難にガウシアンフィルタを使いましょう。結果は以下のような感じになります。

```cpp
cv::Size ksize = cv::Size(5, 5); // ここのパラメータは適宜
cv::GaussianBlur(input_img, output_img, ksize, 0);
```

<img alt="original.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/a355f947-3360-dcae-bd0a-e20c579ce554.jpeg" width=50%><img alt="blured.jpg"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/7b9d5252-6088-4e5c-3d9a-2399ee02915c.jpeg" width=50%>
よく見ないとほとんど違いがわかりませんw
ちょっとぼやけてます。

##色範囲による抜き出し
色範囲による抜き出しは肌色の検知を行うのであればRGB色空間よりもHSV色空間とかのほうがいいでしょう。色相以外に明度、彩度で範囲指定をできるので。YCrCb色空間とかでもうまくいくらしいです。


```cpp
cv::cvtColor(input_img, hsv_img, CV_BGR2HSV); // HSV色空間への変換
cv::inRange(hsv_img, MIN_HSVCOLOR, MAX_HSVCOLOR, msk_img); // HSVの値の範囲によるマスク生成
```

<img alt="blured.jpg"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/7b9d5252-6088-4e5c-3d9a-2399ee02915c.jpeg" width=50%><img alt="range_mask.jpg"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/aa0bc914-1c24-0246-102d-a8029e77d99c.jpeg" width=50%>
余分な部分を結構切り取れましたが、まだ結構ノイズがのってますね。これからノイズを除去していきます。

##モルフォロジー変換
なんか凄そうな名前がついていますが、簡単に言うとマスク領域の収縮と膨張を行います。収縮を行うことでマスク領域のうち面積の小さいものが消失し、膨張を行うことで収縮した分をもとに戻します。これにより小さなノイズを除去できます。この工程を任意の回数行います。ただあんまりやりすぎるとマスク領域が潰れてしまうので注意。

```cpp
cv::erode(msk_img, msk_img, cv::Mat(), cv::Point(-1,-1), 2); // 収縮
cv::dilate(msk_img, msk_img, cv::Mat(), cv::Point(-1,-1), 3); // 膨張
```
最後の引数で収縮・膨張回数を指定します。この変換を一発で行うmorphologyEx()という関数も用意されていますが、今回は収縮と膨張の回数を別にしたかったので分けています。

下の画像をみると、収縮段階で手の後ろの細い線が消えて指が細くなり、膨張段階で指の太さがもとに戻っているのがわかります。
<img alt="erode.jpg"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/c2e9adf5-83bb-cff1-264a-07eaf04f2643.jpeg" width=50%><img alt="dilate.jpg"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/2d4665ff-5d75-0362-b212-78307c6d3da7.jpeg" width=50%>
まだ下の方にノイズが残っているので最後にそれを除去します。

##マスク領域面積による抜き出し
ここまで処理したマスク画像を見ると手の領域に比べてノイズの領域は目積が小さいですよね。なので、マスク領域のうち比較的面積が大きいものを抜き出して手を取りたいと思います。

```cpp
std::vector<std::vector<cv::Point>> contours;
cv::findContours(msk_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); // 輪郭を求める
std::vector<std::vector<cv::Point>> contours_subset;
for(int i=0; i<contours.size();i++){
    double area = contourArea(contours.at(i)); // 輪郭線から領域を求める
    if(area>7000 && area<50000){ // 領域面積の指定
        contours_subset.push_back(contours.at(i));
    }
}

msk_img = cv::Mat::zeros(msk_img.rows, msk_img.cols, CV_8UC1); 
drawContours(msk_img,contours_subset,0,cv::Scalar(255),-1); // 該当領域の描画
```
今回は手に相当する領域が一つしかないと仮定しています。最終行の0を-1に書き換えるとすべての指定した面積の領域が抜き取れます。
<img alt="dilate.jpg"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/2d4665ff-5d75-0362-b212-78307c6d3da7.jpeg" width=50%><img alt="masked.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/42e39d14-2bc3-7d27-1e54-5ff1f3cd8436.jpeg" width=50%>
ノイズが消えましたね。

#結果
では、マスク領域を適用して初めの状態と比べてみましょう。

```cpp
input_img.copyTo(result, msk_img);
```
<img alt="original.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/a355f947-3360-dcae-bd0a-e20c579ce554.jpeg" width=50%><img alt="original.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/d3a7190a-b24c-2fba-1214-104c9e6f9836.jpeg" width=50%>
きれいに手の部分だけ抜き取れましたね。ただ照明環境に激しく依存するので、色範囲による抜き取りのパラメータは環境が変わるごとに調整する必要があります。人の肌の色が極彩色とかだとブレが少なくていいんでしょうけどね。

#コード全体
コード全体としては以下のようになります。

```cpp
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

// 色相によるマスク処理時範囲
#define MIN_HSVCOLOR cv::Scalar(0, 60, 80)
#define MAX_HSVCOLOR cv::Scalar(10, 160, 240)

cv::Mat input;
cv::Mat tmp_img;
cv::Mat msk_img, hsv_img, result;

int main(){
    input = cv::imread("original.jpg", 1);

    cv::Size s = input.size();

    tmp_img.create(s, CV_32FC3);
    result.create(s, CV_32FC3);
    msk_img.create(s, CV_8UC1);

    tmp_img = input.clone();

    // 平滑化
    cv::Size ksize = cv::Size(5, 5);
    cv::GaussianBlur(tmp_img, tmp_img, ksize, 0);
    //cv::bilateralFilter(tmp_img1, tmp_img, -1, 50, 5);

    // HSV色空間に変換
    cv::cvtColor(tmp_img, hsv_img, CV_BGR2HSV);

    // HSV色空間における肌色の検出
    cv::inRange(hsv_img, MIN_HSVCOLOR, MAX_HSVCOLOR, msk_img);

    //　マスク領域の膨張・縮小
    cv::erode(msk_img, msk_img, cv::Mat(), cv::Point(-1,-1), 2);
    cv::dilate(msk_img, msk_img, cv::Mat(), cv::Point(-1,-1), 3);

    // 輪郭線から手を探す
    std::vector<std::vector<cv::Point>> contours;
    cv::findContours(msk_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
    std::vector<std::vector<cv::Point>> contours_subset;
    for(int i=0; i<contours.size();i++){
        double area = contourArea(contours.at(i));
        if(area>7000 && area<50000){
            contours_subset.push_back(contours.at(i));
        }
    }
    msk_img = cv::Mat::zeros(msk_img.rows, msk_img.cols, CV_8UC1);
    drawContours(msk_img,contours_subset,0,cv::Scalar(255),-1);
    msk_img = cv::Mat::zeros(msk_img.rows, msk_img.cols, CV_8UC1);
    drawContours(msk_img,contours_subset,0,cv::Scalar(255),-1);

    // マスク処理後画像の生成
    tmp_img = input.clone();
    result = cv::Scalar(0.0, 0.0, 0.0);
    tmp_img.copyTo(result, msk_img);
    cv::imwrite("result.jpg", result);    
}
```
#終わりに
個人的な実装した感想としてC++はあまり参考文献がない印象です。C++に特にこだわりがない人はPythonで書いたほうが色々と楽だと思います。
久しぶりに長い記事を書いて疲れました。続きの内容についてはまた気が向いたら書くことにします。では、また。