読者です 読者をやめる 読者になる 読者になる

kivantium活動日記

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

エンゼル体操の切り抜き

みんな大好きSHIROBAKOの16話に非常に印象的な体操を踊るシーンがありました。中毒動画を眺めているうちに背景が固定しているなら絵麻ちゃんだけ切り出せるのではないかとふと思ったので、やってみました。

背景抽出その1

背景と違う部分を切り出す処理は画像処理でよく使う手法なのでどこかにサンプルコードが転がっているだろうと思ったのでまずは検索したところ、Stack Overflowに
c++ Opencv accumulateWeighted - strange behaviourという質問がありました。質問にあったコードを修正したものがこちら。

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

using namespace cv;
using namespace std;


int main(int argc, char* argv[])
{
    VideoCapture cap("angel.mp4");
    Mat frame1;
    cap.read(frame1);
    Mat acc = Mat::zeros(frame1.size(), CV_32FC3);

    Mat frame;
    Mat gray;
    while(1){
        cap.read(frame);
        if(frame.empty()) break;
        accumulateWeighted(frame, acc, 0.03);
        imshow("frame", frame);
        imshow("acc", acc/255);
        waitKey(1);
    }
    imwrite("background.png", acc);
    return 0;
}

ここでangel.mp4はニコニコで配信していたSHIROBAKO 16話からエンゼル体操の一部(アイン・ツヴァイ……のあたり)を切り抜いた動画ファイルです。

このコードの実行結果はこうなりました。
f:id:kivantium:20150210225246p:plain:w300
確かに背景っぽい部分を抜き出すことが出来ていますが、絵麻ちゃんの姿が写りこんでしまってあまりきれいな背景画像になっていません。絵麻ちゃんは激しく動いているため、背景の全ピクセルが写っているのでうまくやればもっときれいな画像が得られるはずです。

背景抽出その2

と考えたところでテスト期間に突入したのでしばらく作業は中断していたのですが、テストが終わったので進捗が出ました。

絵麻ちゃんが動いている部分のピクセルは値が激しく移り変わるのに対して、背景のピクセルはエンコードでのノイズを除けば変わらないはずです。そこで、各ピクセルごとに動画内で最も長い時間写っている色を与えてやれば、それが背景画像になると考えられます。(実際には上側で服の色が最頻値になってしまったので微妙に調整しています。)このアイデアを実装したのが次のコードです。

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

using namespace cv;
using namespace std;

int main () {
    //動画の読み込み
    VideoCapture input("angel2.mp4");
    if(!input.isOpened()){
        cout << "Video not found!" << endl;
        return -1;
    }
    //画像を入れるMat配列の用意
    int frame_count = input.get(CV_CAP_PROP_FRAME_COUNT);
    Mat *frames = new Mat[frame_count];
    for(int i=0;i<frame_count;i++){
        input >> frames[i];
    }

    //背景画像のMat
    Mat back(frames[0].size(), CV_8UC3);
    //それぞれのピクセルについて最頻値を求める
    for(int y=0; y<frames[0].rows; y++){
        for(int x=0; x<frames[0].cols; x++){
            //全色に配列を用意するのは無駄なので連想配列を使う
            map<int, int> mapB, mapG, mapR;
            for(int i=0;i<frame_count;i++){
                //ピクセルのBGR値を取得
                int step = frames[i].step;
                int B = frames[i].data[step*y+3*x+0];
                int G = frames[i].data[step*y+3*x+1];
                int R = frames[i].data[step*y+3*x+2];
                //その色が初めてかどうかで処理を分ける
                if(mapB.find(B) == mapB.end()) mapB[B] = 1;
                else mapB[B]++;
                if(mapR.find(R) == mapR.end()) mapR[R] = 1;
                else mapR[R]++;
                if(mapG.find(G) == mapG.end()) mapG[G] = 1;
                else mapG[G]++;
            }
            int mode = 0;        //最頻色の度数
            int second = 0;      //二番目に多い色の度数
            int mode_color = 0;  //最頻値の色
            int second_color = 0;//二番目に多い色
            int step = back.step;
            map<int, int>::iterator it;

            //連想配列の中で1番,2番目に頻度が高い色とその度数を探索
            for(it = mapB.begin(); it != mapB.end(); it++){
                if (it->second > mode) {
                    second_color = mode_color;
                    second = mode;
                    mode_color = it->first;
                    mode = it->second;
                }
            }
            //上半分は服の色が最頻値になるので2番目を使う
            if(y<210) back.data[step*y+3*x+0] = second > mode/2 ? second_color : mode_color;
            else back.data[step*y+3*x+0] = mode_color;
            mode = 0;

            //Gについても同様
            for(it = mapG.begin(); it != mapG.end(); it++){
                if (it->second > mode) {
                    second_color = mode_color;
                    second = mode;
                    mode_color = it->first;
                    mode = it->second;
                }
            }
            if(y<210) back.data[step*y+3*x+1] = second > mode/2 ? second_color : mode_color;
            else back.data[step*y+3*x+1] = mode_color;

            //Rについても同様
            mode = 0;
            for(it = mapR.begin(); it != mapR.end(); it++){
                if (it->second > mode) {
                    second_color = mode_color;
                    second = mode;
                    mode_color = it->first;
                    mode = it->second;
                }
            }
            if(y<210) back.data[step*y+3*x+2] = second > mode/2 ? second_color : mode_color;
            else back.data[step*y+3*x+2] = mode_color;
        }
    }
    //表示と保存
    imshow("background", back);
    imwrite("background.png", back);
    waitKey(0);
}

angel2.mp4は特に動きの激しい部分だけを切り出して最頻値が背景になりやすくなるようにした動画ファイルです。このプログラムの結果がこれです。
f:id:kivantium:20150210230120p:plain:w300
ほとんど完璧な背景画像を得ることができました。

もちろん、カメラからの画像などを利用するときには全く同じ色が得られることはほとんどないのでこの手法を使うことはできませんが、アニメの解析には有用でしょう。ループが何十にもなっているので実行に時間がかかるのが難点です。何かいい方法ないだろうか。

背景差分

背景がとれたので背景と差がある部分を抜き出せば体操だけが切り出せるでしょう。
Python/OpenCVと背景差分法で移動物体の検出を参考に書いたプログラムがこれです。

#include <iostream>
#include "opencv2/opencv.hpp"
 
using namespace cv;
using namespace std;
 
int main () {
    //動画の読み込み
    VideoCapture input("angel.mp4");
    if(!input.isOpened()){
        cout << "Video not found!" << endl;
        return -1;
    }

    // 出力先の設定
    VideoWriter output;
    output.open("out.avi",  
            CV_FOURCC_MACRO('D','X','5','0'),
            input.get(CV_CAP_PROP_FPS), 
            Size(input.get(CV_CAP_PROP_FRAME_WIDTH), input.get(CV_CAP_PROP_FRAME_HEIGHT)));
    if (!output.isOpened()){
        cout  << "Could not open the output video" << endl;
        return -1;
    }
    //Matの用意
    Mat frame, back, diff, gray, bin;
    //背景画像の読み込み
    back = imread("background.png");
    for(;;){
        //画像の取り込み
        input >> frame;
        //動画が終わっていたら抜ける
        if(frame.empty()) break;
        //背景画像との差分を取得
        absdiff(frame, back, diff);
        //差分画像をグレイスケールに
        cvtColor(diff, gray, CV_BGR2GRAY);
        //二値化
        threshold(gray, bin, 10, 255, THRESH_BINARY);
        //ノイズ除去
        medianBlur(bin, bin, 11);

        //動体領域以外は白くする
        for(int y=0; y<frame.rows; y++){
            for(int x=0; x<frame.cols; x++){
                if(bin.data[bin.step*y+x]==0){
                    frame.data[frame.step*y+3*x+0] = 255;
                    frame.data[frame.step*y+3*x+1] = 255;
                    frame.data[frame.step*y+3*x+2] = 255;
                }
            }
        }
        //表示
        imshow("frame", frame);
        waitKey(30);
        //保存
        output << frame;
    }
}

これを先日の記事で紹介した方法でアニメーションGIFにしました。

結果

f:id:kivantium:20150210230523g:plain:w300
スカートなどの色が背景に近い部分の切り抜きに難点はありますが、おおむねきれいに切り抜くことができている様子が分かります。

この手法は背景が固定していれば使えるので、他にもあんこう踊りなどの切り抜きにも応用できます。切り抜いたから何なのかという疑問はありますが、楽しいのでいいでしょう。おしまい。