ProcessingとKinectで実写背景に人だけドット絵になる動画を作ってみた

Kinectでリアルタイムドット絵動画

 久しぶりにProcessingとKinectで動画つくってみました。
きっかけは「レトロゲームキャラが現実に現れたらこんな感じ? 「ドット絵×現実」アート」というのを見て、「これはなかなかいい感じだな。あれ?でもこれって今までやったあれとかこれとかそれを組み合わせればリアルタイムに人を認識して実写背景にドット絵を生成することができそうだな」と思いやってみたら、思ったより簡単にできたので公開してみます。


ProcessingとKinectで実写背景に人だけドット絵になる動画を作ってみた - YouTube

 完成度はそれなりです。右側で洗い物をしているのは私の妻です。

 やっていることは特に難しいことはなくて、今までやった「KinectとProcessingで光学迷彩ごっこをしてみた」とか「とびだせどうぶつの森のマイデザイン補助ツールをProcessingでつくる」とかの組み合わせで実現しています。
 こういった今までやったことの積み重ねで新しいことを簡単にできるというのはちょっと良い感じですね。

参考サイト

http://learning.codasign.com/index.php?title=Using_the_Kinect_with_Processing
http://weed.cocolog-nifty.com/wzero3es/2012/02/120202-processi.html

 ソースコードを見たい人、同じことをやってみたい人は続きを読んでみて下さい。

ソースコード

 使い方は、実行すれば動画が勝手に生成されます。ほどよいところでスペースキーを押すと、プログラムと同じフォルダに動画が作られます。

import hypermedia.video.*;
import processing.video.*;

int fps = 30;

OpenCV opencv;
MovieMaker mm;

import SimpleOpenNI.*;
SimpleOpenNI  context;

final int n_pallet = 159;
final int max_color = 15;

final int size_x = 64;
final int size_y = 48;
final int scale = 10;

//final int size_x = 32;
//final int size_y = 24;
//final int scale = 20;

int CountColor(int[] pallet, int n) {
  int cont = 0;

  int []pallet_cnt = new int[n];
  for (int i=0; i < n; i++) {
    pallet_cnt[i] = 0;
    for (int j= 0; j < size_x*size_y; j++) {
      if (pallet[j] == i) {
        pallet_cnt[i]++;
      }
    }
  }

  for (int i=0; i < n; i++) {
    if (pallet_cnt[i] > 0) {
      cont++;
    }
  }
  return cont;
}

float [] pallet_r = {
  255, 255, 239, 255, 255, 189, 206, 156, 82, 
  255, 255, 222, 255, 255, 206, 189, 189, 140, 
  222, 255, 222, 255, 255, 189, 222, 189, 99, 
  255, 255, 255, 255, 255, 222, 189, 156, 140, 
  255, 239, 206, 189, 206, 156, 140, 82, 49, 
  255, 255, 222, 255, 255, 140, 189, 140, 82, 
  222, 206, 115, 173, 156, 115, 82, 49, 33, 
  255, 255, 222, 255, 255, 206, 156, 140, 82, 
  222, 189, 99, 156, 99, 82, 66, 33, 33, 
  189, 140, 49, 49, 0, 49, 0, 16, 0, 
  156, 99, 33, 66, 0, 82, 33, 16, 0, 
  222, 206, 140, 173, 140, 173, 99, 82, 49, 
  189, 115, 49, 99, 16, 66, 33, 0, 0, 
  173, 82, 0, 82, 0, 66, 0, 0, 0, 
  206, 173, 49, 82, 0, 115, 0, 0, 0, 
  173, 115, 99, 0, 33, 82, 0, 0, 33, 
  255, 239, 222, 206, 189, 
  173, 156, 140, 115, 99, 
  82, 66, 49, 33, 0
};
float [] pallet_g = {
  239, 154, 85, 101, 0, 69, 0, 0, 32, 
  186, 117, 48, 85, 0, 101, 69, 0, 32, 
  207, 207, 101, 170, 101, 138, 69, 69, 48, 
  239, 223, 207, 186, 170, 138, 101, 85, 69, 
  207, 138, 101, 138, 0, 101, 0, 0, 0, 
  186, 154, 32, 85, 0, 85, 0, 0, 0, 
  186, 170, 69, 117, 48, 48, 32, 16, 16, 
  255, 255, 223, 255, 223, 170, 154, 117, 85, 
  186, 154, 48, 85, 0, 69, 0, 0, 16, 
  186, 154, 48, 85, 0, 48, 0, 16, 0, 
  239, 207, 101, 170, 138, 117, 85, 48, 32, 
  255, 255, 170, 223, 255, 186, 186, 154, 101, 
  223, 207, 85, 154, 117, 117, 69, 32, 16, 
  255, 255, 138, 186, 207, 154, 101, 69, 32, 
  255, 239, 207, 239, 255, 170, 170, 138, 69, 
  255, 255, 223, 255, 223, 186, 186, 138, 69, 
  255, 239, 223, 207, 186, 
  170, 154, 138, 117, 101, 
  85, 69, 48, 32, 0
};
float [] pallet_b = {
  255, 173, 156, 173, 99, 115, 82, 49, 49, 
  206, 115, 16, 66, 0, 99, 66, 0, 33, 
  189, 99, 33, 33, 0, 82, 0, 0, 16, 
  222, 206, 173, 140, 140, 99, 66, 49, 33, 
  255, 255, 222, 206, 255, 156, 173, 115, 66, 
  255, 255, 189, 239, 206, 115, 156, 99, 66, 
  156, 115, 49, 66, 0, 33, 0, 0, 0, 
  206, 115, 33, 0, 0, 0, 0, 0, 0, 
  255, 239, 206, 255, 255, 140, 156, 99, 49, 
  255, 255, 173, 239, 255, 140, 173, 99, 33, 
  189, 115, 16, 49, 49, 82, 0, 33, 16, 
  189, 140, 82, 140, 0, 156, 0, 0, 0, 
  255, 255, 156, 255, 255, 173, 115, 115, 66, 
  255, 255, 189, 206, 255, 173, 140, 82, 49, 
  239, 222, 173, 189, 206, 173, 156, 115, 49, 
  173, 115, 66, 0, 33, 82, 0, 0, 33, 
  255, 239, 222, 206, 189, 
  173, 156, 140, 115, 99, 
  82, 66, 49, 33, 0
};

 
void setup(){
  context = new SimpleOpenNI(this);

  // enabling depth camera
  if(context.enableDepth() == false)
  {
     println("Can't open the depthMap, maybe the camera is not connected!"); 
     exit();
     return;
  }
 
  // enabling detecting human
  context.enableScene();
 
  // enabling RGB camera
  if(context.enableRGB() == false)
  {
     println("Can't open the rgbMap, maybe the camera is not connected or there is no rgbSensor!"); 
     exit();
     return;
  }
 
  context.alternativeViewPointDepthToImage();
 
  size(context.depthWidth() , context.depthHeight());   

  mm = new MovieMaker(this, context.depthWidth() , context.depthHeight(), "16bit.mov", fps, MovieMaker.VIDEO, MovieMaker.LOSSLESS); 
  frameRate(fps);
}

void draw(){
  context.update(); 
 
  PImage maskImg = makeImgForMask(context.sceneImage());

  PImage maskedImg = context.rgbImage();

//  image(maskedImg, 0, 0); // display background  
  PImage DotImg = ConvImgToDot(maskedImg);
  DotImg.mask(maskImg);

  // background real
  image(maskedImg, 0, 0); // real camera image background
  //  background(0,64,0); // green mask background
  image(DotImg, 0, 0); // display

  mm.addFrame(); 
}

PImage makeImgForMask(PImage img){
  color cBlack = color(0, 0, 0);
  color cWhite = color(255, 255, 255);
 
  for (int x = 0; x < img.width; x++){
    for (int y = 0; y < img.height; y++){
      color c = img.get(x, y);
      if (red(c) == green(c) & green(c) == blue(c)){
        // none human
        img.set(x, y, cBlack); // mask in black
      }else{
        // human
        img.set(x, y, cWhite); // unmask in white
      }
    }
  }
  return img;
}

PImage ConvImgToDot(PImage img){
  PImage writeImg;
  writeImg = createGraphics(img.width, img.height, P2D);

  img.loadPixels();

  float[] img_r = new float[img.width*img.height];
  float[] img_g = new float[img.width*img.height];
  float[] img_b = new float[img.width*img.height];

  float[] img16_r = new float[size_x*size_y];
  float[] img16_g = new float[size_x*size_y];
  float[] img16_b = new float[size_x*size_y];
  
  int[] img16_pallet = new int[size_x*size_y];

  for(int i = 0; i < img.width*img.height; i++){
    color tmp_color = img.pixels[i];
    img_r[i] = red(tmp_color);
    img_g[i] = green(tmp_color);
    img_b[i] = blue(tmp_color);
  }

  int div_x = int(img.width / size_x);
  int div_y = int(img.height / size_y);

  float tmp_r;
  float tmp_g;
  float tmp_b;

  // make dot image
  for(int i = 0; i < size_x*size_y; i++){
    tmp_r = 0;
    tmp_g = 0;
    tmp_b = 0;
    int tmp_y = int(i / size_x);
    int tmp_x = i - size_x*tmp_y;

    for(int j = 0; j < div_x; j++){
       for(int k = 0; k < div_y; k++){
         tmp_r += img_r[tmp_x*div_x + tmp_y*img.width*div_y+ j + k*img.width];
         tmp_g += img_g[tmp_x*div_x + tmp_y*img.width*div_y+ j + k*img.width];
         tmp_b += img_b[tmp_x*div_x + tmp_y*img.width*div_y+ j + k*img.width];
       }
    }

    tmp_r = tmp_r/(div_x*div_y);
    tmp_g = tmp_g/(div_x*div_y);
    tmp_b = tmp_b/(div_x*div_y);
    
    img16_r[i] = tmp_r;
    img16_g[i] = tmp_g;
    img16_b[i] = tmp_b;
  }

  writeImg.loadPixels();

  // matching pallet
  for (int i = 0; i < size_x*size_y; i++) {
    float []tmp_color_distance = new float[n_pallet];
    for (int j= 0; j < n_pallet; j++) {
      tmp_color_distance[j] =
        (img16_r[i] - pallet_r[j])*(img16_r[i] - pallet_r[j]) + 
        (img16_g[i] - pallet_g[j])*(img16_g[i] - pallet_g[j]) + 
        (img16_b[i] - pallet_b[j])*(img16_b[i] - pallet_b[j]);
    }
    float tmp_color_min = tmp_color_distance[0];
    img16_pallet[i] = 0;
    for (int j= 1; j < n_pallet; j++) {
      if (tmp_color_min > tmp_color_distance[j]) {
        tmp_color_min = tmp_color_distance[j];
        img16_pallet[i] = j;
      }
    }
  }

  // reduce pallet
  while (CountColor (img16_pallet, n_pallet) > max_color) {
    int []pallet_cnt = new int[n_pallet];
    for (int i=0; i < n_pallet; i++) {
      pallet_cnt[i] = 0;
      for (int j= 0; j < size_x*size_y; j++) {
        if (img16_pallet[j] == i) {
          pallet_cnt[i]++;
        }
      }
    }

    int min_colornumb = 0;
    int first_cnt=0;
    int min_tmp = 0;
    while (pallet_cnt[first_cnt] == 0) {
      first_cnt++;
    }

    int min_colorcnt = pallet_cnt[first_cnt];
    for (int i=first_cnt; i < n_pallet; i++) {
      if (min_colorcnt > pallet_cnt[i] && pallet_cnt[i] > 0) {
        min_tmp = 1;
        min_colorcnt = pallet_cnt[i];
        min_colornumb = i;
      }
    }

    float min_colordist = 0;
    int neardist_colornumb = 0;

    if (min_tmp == 0) {
      min_colornumb = first_cnt;
      int tmp_cnt = first_cnt+1;
      while (pallet_cnt[tmp_cnt] == 0) {
        tmp_cnt++;
      }
      min_colordist =
        (pallet_r[min_colornumb]-pallet_r[tmp_cnt])*(pallet_r[min_colornumb]-pallet_r[tmp_cnt])
        + (pallet_g[min_colornumb]-pallet_g[tmp_cnt])*(pallet_g[min_colornumb]-pallet_g[tmp_cnt])
          + (pallet_b[min_colornumb]-pallet_b[tmp_cnt])*(pallet_b[min_colornumb]-pallet_b[tmp_cnt]);
      neardist_colornumb = tmp_cnt;
    }
    else {
      min_colordist =
        (pallet_r[min_colornumb]-pallet_r[first_cnt])*(pallet_r[min_colornumb]-pallet_r[first_cnt])
        + (pallet_g[min_colornumb]-pallet_g[first_cnt])*(pallet_g[min_colornumb]-pallet_g[first_cnt])
          + (pallet_b[min_colornumb]-pallet_b[first_cnt])*(pallet_b[min_colornumb]-pallet_b[first_cnt]);
      neardist_colornumb = first_cnt;
    }

    for (int i=first_cnt; i < n_pallet; i++) {
      if (pallet_cnt[i] > 0 && i != min_colornumb) {
        float tmp_dist = 
          (pallet_r[min_colornumb]-pallet_r[i])*(pallet_r[min_colornumb]-pallet_r[i])
          + (pallet_g[min_colornumb]-pallet_g[i])*(pallet_g[min_colornumb]-pallet_g[i])
            + (pallet_b[min_colornumb]-pallet_b[i])*(pallet_b[min_colornumb]-pallet_b[i]);
        if (min_colordist > tmp_dist) {
          min_colordist = tmp_dist;
          neardist_colornumb = i;
        }
      }
    }

    for (int i=0; i < size_x*size_y; i++) {
      if (img16_pallet[i] == min_colornumb) {
        img16_pallet[i] = neardist_colornumb;
      }
    }
  }

  // pallet update
  for (int i = 0; i < size_x*size_y; i++) {
    img16_r[i] = pallet_r[img16_pallet[i]];
    img16_g[i] = pallet_g[img16_pallet[i]];
    img16_b[i] = pallet_b[img16_pallet[i]];
  }

  // making dot image
  for(int i = 0; i < size_x*size_y; i++){
    int tmp_y = int(i / size_x);
    int tmp_x = i - size_x*tmp_y;

    for(int j = 0; j < scale; j++){
      for(int k = 0; k < scale; k++){
        writeImg.pixels[tmp_y*size_x*scale*scale+tmp_x*scale+j*size_x*scale+k] = color(img16_r[i], img16_g[i], img16_b[i]);
      }
    }
  }
  writeImg.updatePixels();

  return writeImg;
}

void keyPressed() {
  if (key == ' ') { 
  mm.finish(); 
  println("save movie."); 
  exit();
  }
}