kivantium活動日記

プログラムを使っていろいろやります

第21羽 対お姉ちゃん用じゃんけん部隊、通称チマメ隊

この記事はごちうさAdvent Calendar 2015の21日目です。
gochiusa.connpass.com

はじめに

ココア「チノちゃーん!じゃんけんしようよ〜!」
チノ「いまボトルシップを作るのに忙しいので向こう行っててください」
ココア「ヴェアアアア!!」

チノちゃんとじゃんけんしたくなったココアさんですが、チノちゃんはいま手が離せないようです。

ココア「いいもん!自分でチノちゃんとじゃんけんするプログラム作っちゃうもん!」

ココア先輩の優雅なOpenCVチュートリアル

ココア「まずは画像処理に必要なOpenCVをインストールするよ!」

新しいもの好きのココアさんは自分でビルドすることにしたようです。ココアさんの開発用パソコンにはUbuntu 14.04が入っていることにしておきます。

ココア「ソースコードのダウンロードは公式サイトから出来たし、インストールはWikiの説明を見るだけでできたよ♪」

ソースコードをダウンロードして解凍したフォルダに入った後、

sudo apt-get install build-essential
sudo apt-get install cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
sudo apt-get install python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev
mkdir release
cd release
cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local ..
make
sudo make install

を実行したココアさん。プログラムも得意なようです。
ココア「将来は街の国際バリスタプログラマ弁護士もいいかな〜♪」

ちなみに古いバージョンでよければ

sudo apt-get install libopencv-dev

だけでインストールできます。

肌色の部位抽出完了(とりみんぐこんぷりーと)

ココア「まずはカメラ画像から肌色の部分を抜き出すプログラムを書くよ!」

ジャンケンの手を見分けるためにはカメラに映っている手の部分を切り出すと分かりやすいので肌色の部分を切り出す方針にしたみたいです。
色で切り抜くにはRGBからHSVに変換して閾値内の部分を取れば良さそうだと考えたココアさんはこんなコードを書きました。

プログラミング言語は例によって「CPP=こころぴょんぴょん」という理由でC++にしました。

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[]){
    //カメラの設定
    VideoCapture cap(0); // 環境に応じて0や1を入れる
    if(!cap.isOpened()){
        cout << "Camera not Found!" << endl;
        return -1;
    }
    cap.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
    cap.set(CV_CAP_PROP_FRAME_WIDTH, 640);

    Mat src, hsv, skin;
    
    while(true){
        // カメラ画像の取り込み
        cap >> src;
        // HSV色空間への変換
        cvtColor(src, hsv, CV_BGR2HSV);
        // 肌色部分の抜き出し
        inRange(hsv, Scalar(0, 20, 20), Scalar(25, 255, 255), skin);
        
        // 抜き出した部分をなめらかにする処理
        Mat structElem = getStructuringElement(MORPH_RECT, Size(3, 3));
        morphologyEx(skin, skin, MORPH_CLOSE, structElem);

        //画像の表示
        imshow("camera", src);
        imshow("skin", skin);
        // qが入力されたら終了
        int key = waitKey(10) % 256;
        if (key == 'q') break;
    }
}

ちなみにコンパイル

g++ janken.cpp `pkg-config opencv --cflags --libs`

という感じでやりました。

結果はこんな感じでした。上が入力画像、下が出力画像です。
f:id:kivantium:20151220232013p:plain:w400
f:id:kivantium:20151220232035p:plain:w400

ちゃんと肌色の部分が取れていますね。残念ながらこの簡単な方法ではカメラ画像に肌色っぽいものが映っているとうまくいかないので後ろに青い板を置きました……

ココア「よーし、じゃあいつものようにデータを集めて機械学習するよ!今回はどうしよう、やっぱり最新のTensorFlowかな。でも遅いからSVMとかのもっと高速な手法を使ったほうがいいかも。うーんどうしよう……」
??「機械学習すればいいってもんじゃないんじゃないかな?」
ココア「どういう意味?」
??「OpenCVには画像処理に大切なものがつまってるんだよ(ポロロン」

ひと目で、尋常でないライブラリだと見抜いたよ

ココア「じゃあ今回は機械学習を使わない方法でやってみるよ!」

締め切りに間に合わなくなりそうになったココアさんは検索して見つけたVisual Finger Counterというページのソースコードを流用してへこみの数から指の数を数えて手を調べるプログラムを書きました。

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[]){
    //カメラの設定
    VideoCapture cap(0); // 環境に応じて0や1を入れる
    if(!cap.isOpened()){
        cout << "Camera not Found!" << endl;
        return -1;
    }
    // 取り込む画像サイズの指定
    cap.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
    cap.set(CV_CAP_PROP_FRAME_WIDTH, 640);

    Mat src, hsv, skin;
    
    while(true){
        // カメラ画像の取り込み
        cap >> src;
        // HSV色空間への変換
        cvtColor(src, hsv, CV_BGR2HSV);
        // 肌色部分の抜き出し
        inRange(hsv, Scalar(0, 20, 20), Scalar(25, 255, 255), skin);
        
        // 抜き出した部分をなめらかにする処理
        Mat structElem = getStructuringElement(MORPH_RECT, Size(3, 3));
        morphologyEx(skin, skin, MORPH_CLOSE, structElem);

        // 最も長い輪郭線を選ぶ
        vector<vector<Point> > contours;
        vector<Vec4i> hierarchy;
        findContours(skin, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);

        double largestArea = 0.0;
        int largestContourIndex = 0;
        for(int i=0; i<contours.size(); ++i){
            double a = contourArea(contours[i], false); 
            if(a > largestArea){
                largestArea = a;
                largestContourIndex = i;                    
            }
        }

        // 欠けた部分を求める
        vector<vector<int> > hulls (1);
        convexHull(contours[largestContourIndex], hulls[0], false, false);
        std::vector<Vec4i> defects;
        convexityDefects(contours[largestContourIndex], hulls[0], defects);

        // 小さいものや離れすぎているものを除いて指の数を数える
        int fingerCount = 1;
        for (int i = 0; i< defects.size(); i++){
            int start_index = defects[i][0];
            CvPoint start_point = contours[largestContourIndex][start_index];
            int end_index = defects[i][1];
            CvPoint end_point = contours[largestContourIndex][end_index];
            double d1 = (end_point.x - start_point.x);
            double d2 = (end_point.y - start_point.y);
            double distance = sqrt((d1*d1)+(d2*d2));
            int depth =  defects[i][3]/1000;

            if (depth > 10 && distance > 2.0 && distance < 200.0){
                fingerCount ++;
            }
        }

        // 指の数に応じて現在の手を表示する
        if (fingerCount <= 2) cout << "グー" << endl;
        else if (fingerCount <= 4) cout << "チョキ" << endl;
        else cout << "パー" << endl;

        //画像の表示
        imshow("camera", src);
        imshow("skin", skin);
        // qが入力されたら終了
        int key = waitKey(10) % 256;
        if (key == 'q') break;
    }
}

出した手をちゃんと判定するプログラムを書くことができました。

ココア「機械学習を使わなくても判定できたよ!ところでさっきの人は誰だったんだろう……」

グーを探す日常

ココア「最後にチノちゃんの絵をつけるよ!」
チノちゃんとじゃんけんしている気分になるために絵をつけて完成です。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <random>


using namespace std;
using namespace cv;

int main(int argc, char *argv[]){
    //カメラの設定
    VideoCapture cap(0); // 環境に応じて0や1を入れる
    if(!cap.isOpened()){
        cout << "Camera not Found!" << endl;
        return -1;
    }
    // 取り込む画像サイズの指定
    cap.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
    cap.set(CV_CAP_PROP_FRAME_WIDTH, 640);

    // チノちゃんムービーの読み込み
    VideoCapture movie("movie.mp4");
    if(!movie.isOpened()){
        cout << "Movie not found!" << endl;
        return -1;
    }

    Mat frame, src, hsv, skin;
    
    while(true){
        // sキーが押されるまで待機
        while(true) {
            cap >> src;
            imshow("camera", src);
            int key = waitKey(33) % 256;
            if (key == 's') break;
            if (key == 'q') return -1;
        }
        // チノちゃんムービーの再生
        movie.set(CV_CAP_PROP_POS_FRAMES, 0);
        while(true) {
            movie >> frame;
            if (frame.empty()) break;
            imshow("movie", frame);
            imshow("camera", src);
            waitKey(33); // 30fpsなので
        }

        // 手の決定
        random_device rd;
        mt19937 mt(rd());
        uniform_int_distribution<int> rand(0, 2);

        int hand = rand(mt);

        if(hand == 0) frame = imread("rock.png");
        else if(hand == 1) frame = imread("paper.png");
        else frame = imread("scissor.png");
        imshow("movie", frame);
        waitKey(1000);

        // 10フレームの平均で判定
        int fingerCount = 0;
        for (int loop=0; loop<10; ++loop) {
            // カメラ画像の取り込み
            cap >> src;
            // HSV色空間への変換
            cvtColor(src, hsv, CV_BGR2HSV);
            // 肌色部分の抜き出し
            inRange(hsv, Scalar(0, 20, 20), Scalar(25, 255, 255), skin);
            
            // 抜き出した部分をなめらかにする処理
            Mat structElem = getStructuringElement(MORPH_RECT, Size(3, 3));
            morphologyEx(skin, skin, MORPH_CLOSE, structElem);

            // 最も長い輪郭線を選ぶ
            vector<vector<Point> > contours;
            vector<Vec4i> hierarchy;
            findContours(skin, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);

            double largestArea = 0.0;
            int largestContourIndex = 0;
            for(int i=0; i<contours.size(); ++i){
                double a = contourArea(contours[i], false); 
                if(a > largestArea){
                    largestArea = a;
                    largestContourIndex = i;                    
                }
            }

            // 欠けた部分を求める
            vector<vector<int> > hulls (1);
            convexHull(contours[largestContourIndex], hulls[0], false, false);
            std::vector<Vec4i> defects;
            convexityDefects(contours[largestContourIndex], hulls[0], defects);

            // 小さいものや離れすぎているものを除いて指の数を数える
            for (int i = 0; i< defects.size(); i++){
                int start_index = defects[i][0];
                CvPoint start_point = contours[largestContourIndex][start_index];
                int end_index = defects[i][1];
                CvPoint end_point = contours[largestContourIndex][end_index];
                double d1 = (end_point.x - start_point.x);
                double d2 = (end_point.y - start_point.y);
                double distance = sqrt((d1*d1)+(d2*d2));
                int depth =  defects[i][3]/1000;

                if (depth > 10 && distance > 2.0 && distance < 200.0){
                    fingerCount++;
                }
            }
        }

        // 指の数に応じて勝敗を判定する
        int status;
        cout << fingerCount << endl;
        // 自分がグー
        if (fingerCount < 20){
            cout << "グー" << endl;
            if (hand == 0) status = 0;      // あいこ
            else if (hand == 1) status = 1; // チノちゃんの勝ち
            else status = 2;                // チノちゃんの負け
        }
        // 自分がチョキ
        else if (fingerCount <= 45) {
            cout << "チョキ" << endl;
            if (hand == 0) status = 1;
            else if (hand == 1) status = 2;
            else status = 0;
        }
        // 自分がパー
        else {
            cout << "パー" << endl;
            if (hand == 0) status = 2;
            else if (hand == 1) status = 0;
            else status = 1;
        }
        // 結果に応じてチノちゃんの表情が変わる
        if(status == 0) frame = imread("draw.png");
        else if(status == 1) frame = imread("win.png");
        else frame = imread("lose.png");
        imshow("movie", frame);
    }
}

出来上がったものはこんな感じです。
f:id:kivantium:20151221072404p:plain:w500
動画です。

おわりに

ココア「チノちゃーん、見て!こんなプログラムを書いたよ!」
チノ「ココアさんはずっとプログラムと遊んでてください」

チノちゃんと仲良くなるのは難しいようです……