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

kivantium活動日記

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

結城友奈はサンタである

OpenCV

一、前書きはきちんと

2014年も終わりに近づき冬シーズンのアニメも佳境を迎えるなか、いよいよクリスマスがやってきます。絶望的な戦いに臨む讃州中学勇者部のみなさんにもクリスマス気分を味わってもらいたいですね。というわけで、勇者部のみなさんにサンタ帽をかぶってもらおうと思います。

一、なるべく手動にしない

サンタ帽を写真にかぶせるだけならGIMPなりPhotoshopなりを使えば簡単にできるわけですが、21世紀を生きる人間がいつまでも手動に頼るわけにはいきません。なるべく自動化したいものです。幸いなことにアニメ顔の顔認識は簡単にできるので自動でサンタ帽をかぶらせてあげるプログラムを書くことができそうです。ではやってみましょう

一、よくググって、よく見つける

サンタ帽のフリー素材はいくつかあったので、サンタクロースの帽子の無料イラストから使います。帽子をかぶらせる素材は顔がすべて認識できるという条件で探すとかなり限られてしまうのですが、電撃オンラインの先行カットのものを使います。(夏凛ちゃんが写っていないのが残念です……)

ではコードを書いていきます。顔領域を認識してその上にサンタ帽の画像をのせるだけの簡単な処理なのですが、OpenCVは透過部分を自動的に処理してくれませんでした。結局透過部分の処理はググって見つけたHow to draw a transparent image over live camera feed in opencvにあったコードを利用しました。

santa.pngが帽子画像で、帽子以外の部分はGIMPで透明にしてあります。(自動化とは何だったのか……)

#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(int argc, char* argv[]){
    CascadeClassifier face_cascade;
    //カスケードのロード
    face_cascade.load("lbpcascade_animeface.xml");

    Mat src = imread(argv[1]);
    if(src.empty()){
        cout << "Image not found!" << endl;
        return -1;
    }

    //アルファチャンネルも含めて読み込む
    Mat santa = imread("santa.png", -1);
    Mat resized, frame_gray;
    vector<Rect> faces;

    //画像のグレースケール化
    cvtColor(src, frame_gray, COLOR_BGR2GRAY );
    //ヒストグラムの平坦化
    equalizeHist(frame_gray, frame_gray);
    //顔の認識
    face_cascade.detectMultiScale(frame_gray, faces);
    //顔の上にサンタ帽子をかぶせる
    for(int i=0; i<faces.size(); i++){
        resize(santa, resized, Size(faces[i].width, santa.rows*faces[i].width/santa.cols));
        //帽子がちょうどいい位置にくるように0.9倍する
        Mat dst = src(Rect(faces[i].x, faces[i].y>resized.rows*0.9 ? faces[i].y-resized.rows*0.9 : 0, resized.cols, resized.rows));
        //透明度を考えた重ねあわせ
            const Vec3b* src_pixel = dst.ptr<Vec3b>(y);
            const Vec4b* ovl_pixel = resized.ptr<Vec4b>(y);
            Vec3b* dst_pixel = dst.ptr<Vec3b>(y);
            for (int x = 0; x < dst.cols; x++, src_pixel++, ovl_pixel++, dst_pixel++){
                double alpha = (*ovl_pixel).val[3] / 255.0;
                for (int c = 0; c < 3; c++){
                    (*dst_pixel).val[c] = (uchar) ((*ovl_pixel).val[c]*alpha+(*src_pixel).val[c]*(1.0-alpha));
                }
            }
        }
        //透過処理が不要ならこれだけでOK
        //resized.copyTo(src(Rect(faces[i].x, faces[i].y>santa.rows ? faces[i].y-santa.rows : 0, resized.cols, resized.rows)));
    }
    //結果の表示
    imshow("Result", src);
    waitKey(0);
    //結果の表示
    imwrite("result.png", src);
    return 0;
}

結果がこちら。
f:id:kivantium:20141223230142p:plain:w400
勇者部のみなさんにクリスマスがやってきました。

一、悩んだら過去記事!

この結果で十分な結果なのですが、背景画像と帽子の明るさが若干違うので微妙に違和感を感じます。どうにかしたいと思って解決策を悩んでいたら、以前に今日も一日がんばるぞい!(失敗作)という思いっきり画像合成を扱った記事を書いたことを思い出しました。失敗作を供養するのは今しかない!というわけでポワソン画像合成を行ってみました。

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

using namespace std;
using namespace cv;

#define LOOP_MAX 1000
#define EPS 2.2204e-016
#define NUM_NEIGHBOR 4

using namespace cv;

int quasi_poisson_solver(Mat &img_src, Mat &img_dst, Mat &img_mask, int channel, int offset[]){
  int i,j,loop,neighbor,count_neighbors,ok;
  float error,sum_f,sum_vpq,fp;
  int naddr[NUM_NEIGHBOR][2] = {{-1,0},{0,-1},{0,1},{1,0}};
  cv::Mat img_new = (cv::Mat_<double>(img_dst.rows,img_dst.cols));
  for(i=0;i<img_dst.rows;i++){
    for(j=0;j<img_dst.cols;j++){
      img_new.at<double>(i,j)=(double)img_dst.at<Vec3b>(i,j)[channel];
    }
  }
  for(loop=0;loop<LOOP_MAX;loop++){
    ok = 1;
    printf("%d\n",loop);
    for(i=0;i<img_mask.rows;i++){
      for(j=0;j<img_mask.cols;j++){
        if((int)img_mask.at<Vec3b>(i,j)[0]>0){
          sum_f=0.0;
          sum_vpq=0.0;
          count_neighbors=0;
          for(neighbor=0;neighbor<NUM_NEIGHBOR;neighbor++){
            if(i+offset[0]+naddr[neighbor][0]>=0
              &&j+offset[1]+naddr[neighbor][1]>=0
              &&i+offset[0]+naddr[neighbor][0]<img_dst.rows
              &&j+offset[1]+naddr[neighbor][1]<img_dst.cols){
              sum_f+=img_new.at<double>(i+offset[0]+naddr[neighbor][0],j+offset[1]+naddr[neighbor][1]);
              sum_vpq+=(float)img_src.at<Vec3b>(i,j)[channel]
              -(float)img_src.at<Vec3b>(i+naddr[neighbor][0],j+naddr[neighbor][1])[channel];
              count_neighbors++;
            }
          }  
          fp = (sum_f + sum_vpq)/(float)count_neighbors;
          error = fabs(fp - img_new.at<double>(i+offset[0],j+offset[1]));
          if(ok&&error>EPS*(1+fabs(fp))){
          ok=0;
          }
        img_new.at<double>(i+offset[0],j+offset[1])=fp;
        }
      }
    }
    if(ok){
      break;
    }
  }
  for(i=0;i<img_dst.rows;i++){
    for(j=0;j<img_dst.cols;j++){
      if(img_new.at<double>(i,j)>255){
        img_new.at<double>(i,j)=255.0;
        }
       else if(img_new.at<double>(i,j)<0){
       img_new.at<double>(i,j)=0.0;
    }
    img_dst.at<Vec3b>(i,j)[channel] = (uchar)img_new.at<double>(i,j);
    }
  }
  return 1;
}

int main(int argc, char* argv[]){
    CascadeClassifier face_cascade;
    //カスケードのロード
    face_cascade.load("lbpcascade_animeface.xml");

    Mat src = imread(argv[1]);
    if(src.empty()){
        cout << "Image not found!" << endl;
        return -1;
    }

    Mat santa = imread("santa.png");
    Mat mask = imread("mask.png");
    Mat resized, mask_resized, frame_gray;
    vector<Rect> faces;
    int offset[2] = {0,0};

    //画像のグレースケール化
    cvtColor(src, frame_gray, COLOR_BGR2GRAY );
    //ヒストグラムの平坦化
    equalizeHist(frame_gray, frame_gray);
    //顔の認識
    face_cascade.detectMultiScale(frame_gray, faces);
    //顔の上にサンタ帽子をかぶせる
    for(int i=0; i<faces.size(); i++){
        resize(santa, resized, Size(faces[i].width, santa.rows*faces[i].width/santa.cols));
        resize(mask, mask_resized, Size(faces[i].width, mask.rows*faces[i].width/mask.cols));
        Mat dst = src(Rect(faces[i].x, faces[i].y>resized.rows*0.9 ? faces[i].y-resized.rows*0.9 : 0, resized.cols, resized.rows));
        for(int i=0;i<3;i++){
            quasi_poisson_solver(resized,dst,mask_resized,i,offset);
        }
    }
    //結果の表示
    imshow("Result", src);
    waitKey(0);
    //結果の表示
    imwrite("result.png", src);
    return 0;
}

結果がこれです。
f:id:kivantium:20141223230707p:plain:w400
色をなじませようと努力してくれたのは認めますが、人間の目をごまかせるレベルではなかったようです。ポワソン画像合成くん……。

一、なせば大抵なんとかなる

というわけで1番目のプログラムを使って勇者部以外の人にもサンタ帽をかぶってもらいました。
f:id:kivantium:20141223231646p:plain:w400
f:id:kivantium:20141223231203p:plain:w400
f:id:kivantium:20141223231321p:plain:w400
f:id:kivantium:20141223231435p:plain:w400

以上、一足早いクリスマス特集でした。