4.4 画像処理

注意! 2020年6月現在、macOS Catalinaでは、videoライブラリにエラーが出るようです。もしエラーが出る場合は、以下のサイトを参考にして修正してみてください(なかなか面倒です)。

https://note.com/soohei/n/n87bca61ba561

画像処理は、インタラクティブアート関連作品を作る際によく利用されますが、近年ではKinectで人体の認識をおこない、手や顔などの座標情報をプログラムが検知して映像が反応する、といった使い方が特に多いでしょう。また、スマートフォンアプリでは人の顔の部位を認識するプログラムも多用されています。実際には、そういったプログラムはかなり難易度が高いので、この節では、まず画像処理の原理的なアルゴリズムの理解を試みます。
しかし、原理的な部分を理解しておくと、思いもかけない面白い作品ができてしまうこともあるのが、プログラミングの面白いところです。

4.4.1 カメラ映像の表示

まずは、カメラからの映像をprocessing上で表示してみましょう。カメラが付いているパソコンであれば、カメラ映像は簡単に表示できます。ない場合はWebカメラなどを接続しましょう。
まずは、次のサンプルを実行してみましょう。

2016_10_07_20_54
図4.4-a
リスト4.4-a
//ビデオライブラリをインポート
import processing.video.*;
Capture video;  //Capture型の変数videoを宣言
void setup() {
  size(640, 480);
  
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //使用できるカメラのリストを出力  
  for (int i = 0; i < cameras.length; i++) {
    println(cameras[i]);
  }
  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  video = new Capture(this, cameras[0]);
  video.start();  //カメラを開始
}
void draw() {
  //もし、カメラの準備ができていたら、
  if (video.available() == true) {
    video.read();    //カメラからの映像を読み込む
    image(video, 0, 0);    //0,0は、映像の左上のx,y座標
  }
}

映像を映すだけなら非常に短いコードです。

4.4.2 tint()を使った効果

ビデオの場合でも画像に色をつけるときと同じで、tint()を使うことができます。draw()の中でimage()関数を使ってビデオを表示するコードの直前にtintを使ってみてください。
また、tint(r, g, b, alpha)のalpha(不透明度)を低くすると、フェード効果を得ることができます。

2016_10_07_20_57
図4.4-b
リスト4.4-b
//ビデオライブラリをインポート
import processing.video.*;
Capture video;  //Capture型の変数videoを宣言
void setup() {
  size(640, 480);
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  video = new Capture(this, cameras[0]);  video.start();
}
void draw() {
  //もし、カメラの準備ができていたら、
  if (video.available() == true) {
    video.read();    //カメラからの映像を読み込む
    tint(255, 100, 100);  //色をつける
    //不透明度を設定すると、フェードがかかる
    //tint(255, 100, 100, 20);
    image(video, 0, 0);    //0,0は、映像の左上のx,y座標
  }
}

4.4.3 座標変換を使った変形

draw()関数の中でimage()を実行する時、scale()を使うと簡単に反転できます。「4.1 座標変換」でscale()は学習しましたね。左右反転を図式化すると【図4.4-c】になりますが、x座標が反転した場合、右に移動したければx座標はマイナス方向に設定する必要があります。

%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab_000
図4.4-c

左右反転

scale(-1, 1);
//x座標が反転した場合、右に移動したければx座標はマイナス方向になる
image(video, -width, 0);

上下反転

scale(1, -1);
image(video, 0, -height);

上下左右反転

scale(-1, -1);
image(video, -width, -height);

これらを組み合わせると、ひとつの画面に様々な向きの映像を映すこともできます。

2016_10_07_20_59
図4.4-d
リスト4.4-c
import processing.video.*;

Capture video;

void setup() {
  size(640, 480);
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }

  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  //ビデオの画像を1/2のサイズにする
  video = new Capture(this, 320, 240, cameras[0]);
  video.start();
}

void draw() {
  //もし、キャプチャーの準備ができていたら、
  if (video.available() == true) {
    video.read();    //カメラからの画像を読み込む
    pushMatrix();
    image(video, 0, 0);  //正像
    popMatrix();

    pushMatrix();
    scale(-1, 1);
    image(video, -width, 0);  //左右反転
    popMatrix();

    pushMatrix();    
    scale(1, -1);
    image(video, 0, -height);  //上下反転
    popMatrix();

    pushMatrix();
    scale(-1, -1);
    image(video, -width, -height);  //上下左右反転
    popMatrix();
  }
}

translate()やrotate()も使えるので、キャプチャ映像を移動、回転することもできます。もちろんscale()で拡大縮小もできます。

11_16_15__17_48
図4.4-e
リスト4.4-d
import processing.video.*;
Capture video;
float rad = 0.0;
void setup() {
  size(640, 480);
  
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  //表示サイズを半分の320x240にしている
  video = new Capture(this,320, 240, cameras[0]);
  video.start();
}
void draw() {
  //もし、キャプチャーの準備ができていたら、
  if (video.available() == true) {
    video.read();    //カメラからの画像を読み込む
    pushMatrix();
    //画像の基準点を画面中心に移動
    translate(width/2, height/2);
    rotate(rad);  //回転
    image(video, 0, 0);  //正像
    popMatrix();
    rad += 0.01;
    if (rad > TWO_PI) rad = 0.0;
  }
}

4.4.4 1ピクセルずつ抽出する

次に、画像の中のひとつひとつのピクセルの色を抜き出してみます。このことによって、様々なエフェクトをかけることができます。
画面上は【リスト4.4-a】のサンプルと全く同じなのですが、ひとつずつのピクセルの色を抽出している点で、全く違うコードになっています。

リスト4.4-e
import processing.video.*;
Capture video;
void setup() {
  // 640 x 480が大きすぎて遅かったら、320 x 240に変更する
  size(640, 480);
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //画面サイズのキャプチャ画像を生成。
  video = new Capture(this, width, height, cameras[0]);
  loadPixels();  //画面に画像のピクセルを展開
  video.start();
}
void draw() {
  if (video.available()) {  //もしキャプチャができたら、
    video.read(); //ビデオフレームの読み込み
    video.loadPixels(); //ビデオのピクセルを操作できるようにする
    //1ピクセルごとに色を調べる。
    for (int y = 0; y < video.height; y ++) {
      for (int x = 0; x < video.width; x ++) {
        //ビデオのピクセルを抜き出す
        int pixelColor = video.pixels[y*video.width + x];
        //赤、緑、青をそれぞれ抽出する。
        //以下の3行はビットシフトといって、
        //理解するのが結構大変なので、ここでは詳細は説明しない。
        int r = (pixelColor >> 16) & 0xff;
        int g = (pixelColor >> 8 ) & 0xff;
        int b = pixelColor & 0xff;
        //ウィンドウのピクセルの色を決定
        pixels[y*video.width + x] = color(r, g, b);
      }
    }
    updatePixels();    //画像を更新
  }
}

ここで、練習問題として画像の色を変えてみましょう。サンプルコードはグレーに変換するコードだけ機能するようにコメントアウトされているので、色を変換する部分はそれぞれのコメントアウトを外してください。35~53行目以外は前述のコードと同じです。

グレーに変換
2016_10_07_21_01

図4.4-f

色の反転
2016_10_07_21_01

図4.4-g

2色に変換
2016_10_07_21_02

図4.4-h
リスト4.4-f
import processing.video.*;
Capture video;
void setup() {
  // 640 x 480が大きすぎて遅かったら、320 x 240に変更する
  size(640, 480);
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  video = new Capture(this, cameras[0]);
  video.start();
  loadPixels();    //画面に画像のピクセルを展開
}
void draw() {
  background(0);
  if (video.available()) {  //もしキャプチャができたら、
    video.read(); //ビデオフレームの読み込み
    video.loadPixels(); //ビデオのピクセルを操作できるようにする
    //1ピクセルごとに色を調べる。
    for (int y = 0; y < video.height; y ++) {
      for (int x = 0; x < video.width; x ++) {
        //ビデオのピクセルを抜き出す
        int pixelColor = video.pixels[y*video.width + x];
        //赤、緑、青をそれぞれ抽出する。
        //以下の3行はビットシフトといって、
        //理解するのが結構大変なので、ここでは詳細は説明しない。
        int r = (pixelColor >> 16) & 0xff;
        int g = (pixelColor >> 8 ) & 0xff;
        int b = pixelColor & 0xff;
        //グレー
        r = g = b = (r + g + b)/3; //明暗を抽出
        //色の反転
        /*
        r = 255 - r;
         g = 255 - g;
         b = 255 - b;
         */
        //2色に変換
        //もし、ピクセルの明度127以上だったら
        /*
        if((r + g + b)/3 >= 127){
         
         r = 255; g = 255; b = 255;  //白にする
         }else{
         r = 255; g = 0; b = 0;  //それ以外は赤にする        
         }
         */
        //ウィンドウのピクセルの色を決定
        pixels[y*video.width + x] = color(r, g, b);
      }
    }
    updatePixels();    //画像を更新
  }
}

さて、もう少し踏み込んで25行目のコードを理解しておきましょう。

>//ビデオのピクセルを抜き出す
int pixelColor = video.pixels[y*video.width + x];

通常、640×480ピクセル用のメモリが確保される場合には、1ピクセルごとの色を格納する配列は【図4.4-i】のように順番に並んでいます。
image_process_01.gif

図4.4-i

しかし、ピクセルごとの画像処理では、目的のピクセルの色を抜き出すために、そのピクセルが画像の中でどの位置にあるかを指定する必要があります。
ここでは、8 x 6ピクセルの画像があり、目的のピクセルは x = 3、 y = 2 の位置にあるとします。
結果、行ごとにピクセルが順番に並んでいると考えると、yの高さにvideo*widthをかけ、さらにxの値を足すと、配列の先頭から数えて何番目かが分かります。

%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab_001
図4.4-j

4.4.5 時間の遅延を記録する

この原理が分かると、映像が歪んでいくような効果を出すこともできます。次のサンプルは、画面上からスキャナのように 640 x 1ピクセルずつをキャプチャしていきます。その結果、それぞれの行のキャプチャに時間の遅延が起きて、動いた物体が歪んで表示されます。

11_16_15__17_40
図4.4-k
リスト4.4-g
import processing.video.*;
 
Capture video;
int row = 0;  //行番号
 
void setup() {
  // 640 x 480が大きすぎて遅かったら、320 x 240に変更する
  size(640, 480);
 
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  video = new Capture(this, 640, 480, cameras[0]);
  video.start();
  loadPixels();    //画面に画像のピクセルを展開
}
 
void draw() {
  if (video.available()) {  //もしキャプチャができたら、
    video.read(); //ビデオフレームの読み込み
    video.loadPixels(); //ビデオのピクセルを操作できるようにする
 
    //1ピクセルごとに色を調べる。
    for (int x = 0; x < video.width; x ++) {
      //1行のビデオのピクセルを抜き出す
      int pixelColor = video.pixels[row*video.width + x];
 
      //赤、緑、青をそれぞれ抽出する。以下の3行はビットシフトというが、
      //ここでは詳細は説明しない。
      int r = (pixelColor >> 16) & 0xff;
      int g = (pixelColor >> 8 ) & 0xff;
      int b = pixelColor & 0xff;
 
      //ウィンドウのピクセルの色を決定
      pixels[row*video.width + x] = color(r, g, b);
    }
    updatePixels();    //画像を更新
    
    stroke(255, 0, 0);  //基準線
    line(0, row, width, row);
 
    row ++;  //行を下に移動
    if (row >= height) row = 0;  //画面の最後まで行ったら元に戻す。
  }
}

縦方向の遅延にしてみます(うーん、ちょっと怖いですね)。

11_16_15__17_44
図4.4-l
リスト4.4-h
import processing.video.*;
Capture video;
int column = 0;  //列番号
void setup() {
  // 640 x 480が大きすぎて遅かったら、320 x 240に変更する
  size(640, 480);
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  video = new Capture(this, 640, 480, cameras[0]);
  video.start();
  loadPixels();    //画面に画像のピクセルを展開
}
void draw() {
  if (video.available()) {  //もしキャプチャができたら、
    video.read(); //ビデオフレームの読み込み
    video.loadPixels(); //ビデオのピクセルを操作できるようにする
    //1ピクセルごとに色を調べる。
    for (int y = 0; y < video.height; y ++) {
      //1行のビデオのピクセルを抜き出す
      int pixelColor = video.pixels[y*video.width + column];
      //赤、緑、青をそれぞれ抽出する。以下の3行はビットシフトというが、
      //ここでは詳細は説明しない。
      int r = (pixelColor >> 16) & 0xff;
      int g = (pixelColor >> 8 ) & 0xff;
      int b = pixelColor & 0xff;
      //ウィンドウのピクセルに当てはめる
      pixels[y*video.width + (video.width - column - 1)] = color(r, g, b);
    }
    updatePixels();    //画像を更新
    stroke(255, 0, 0);  //基準線
    line(width - column, 0, width - column, height);
    column ++;  //行を下に移動
    if (column >= width) column = 0;  //画面の最後まで行ったら元に戻す。
  }
}

カメラ映像の描画に図形を使う

他にも、カメラ映像の描画に図形を使うとまた違った見え方になります。あとはアイデア次第で様々なバリエーションを作ることができるでしょう。
このサンプルは、カメラ映像を四角形で描画するものです。

2016_10_07_21_13
図4.4-m
リスト4.4-i
import processing.video.*;
 
Capture video;
 
void setup() {
  size(640, 480);
 
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  video = new Capture(this, 80, 60, cameras[0]);
  video.start();
  loadPixels();    //画面に画像のピクセルを展開
  rectMode(CENTER);
  noFill();
}
 
void draw() {
  if (video.available()) {  //もしキャプチャができたら、
    video.read(); //ビデオフレームの読み込み
    video.loadPixels(); //ビデオのピクセルを操作できるようにする
    background(0);  //ver.2.0以上はここにbackgroundが必要
 
    //1ピクセルごとに色を調べる。
    for (int y = 0; y < video.height; y ++) {
      for (int x = 0; x < video.width; x ++) {
        //ビデオのピクセルを抜き出す
        int pixelColor = video.pixels[y*video.width + x];
 
        //赤、緑、青をそれぞれ抽出する。以下の3行はビットシフトというが、
        //ここでは詳細は説明しない。
        int r = (pixelColor >> 16) & 0xff;
        int g = (pixelColor >> 8 ) & 0xff;
        int b = pixelColor & 0xff;
 
        float rectSize = float(r + g + b)/(255*3)*30.0;
        stroke(r, g, b);
        rect(x*8, y*8, rectSize, rectSize);
      }
    }
  }
}

線状のエフェクトをかけるサンプルです。

2016_10_07_21_13
図4.4-n
リスト4.4-j
import processing.video.*;
 
Capture video;
 
void setup() {
  size(640, 480);
 
  //使用できるカメラのリスト
  String[] cameras = Capture.list();
  
  //カメラが認識されないようなら、以下のコードを追加
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  //カメラの番号をcameras[0]のようにインデックス番号で明示する
  video = new Capture(this, 80, 60, cameras[0]);
  video.start();
  loadPixels();    //画面に画像のピクセルを展開
  rectMode(CENTER);
  noFill();
}
 
void draw() {
  if (video.available()) {  //もしキャプチャができたら、
    video.read(); //ビデオフレームの読み込み
    video.loadPixels(); //ビデオのピクセルを操作できるようにする
    background(0);
 
    //1ピクセルごとに色を調べる。
    for (int y = 0; y < video.height; y ++) {
      for (int x = 0; x < video.width; x ++) {
        //ビデオのピクセルを抜き出す
        int pixelColor = video.pixels[y*video.width + x];
 
        //赤、緑、青をそれぞれ抽出する。以下の3行はビットシフトというが、
        //ここでは詳細は説明しない。
        int r = (pixelColor >> 16) & 0xff;
        int g = (pixelColor >> 8 ) & 0xff;
        int b = pixelColor & 0xff;
 
        float lineSize = float(r + g + b)/24.0;
        stroke(r, g, b);
        line(x*8 + random(-3, 3) - lineSize/2, y*8 + random(-3, 3), 
          x*8 + random(-3, 3) + lineSize/2, y*8 + random(-3, 3));
      }
    }
  }
}

アイデア次第で様々なエフェクトを自作することができます。