画像処理

このページでは、Processingでカメラ映像を使った画像処理を学びます。画像処理というと難しく聞こえますが、基本は「映像を読み込む」「色を変える」「1ピクセルずつ調べる」「別の図形で描き直す」という流れです。

p5.jsの実行欄では、ブラウザのカメラを使ってProcessingのサンプルに近い動きを再生します。最初にカメラ開始ボタンを押し、ブラウザのカメラ使用を許可してください。

カメラはまだ開始されていません。

1. カメラ映像の表示

まずは、カメラからの映像をProcessing上で表示します。Processingではprocessing.videoライブラリのCaptureを使います。

リスト1
import processing.video.*;

Capture video;

void setup() {
  size(400, 400);

  String[] cameras = Capture.list();

  // カメラが認識されるまで待つ
  while (cameras.length == 0) {
    cameras = Capture.list();
  }

  // 最初に見つかったカメラを使う
  video = new Capture(this, 400, 400, cameras[0]);
  video.start();
}

void draw() {
  // 新しい映像が来ていたら読み込む
  if (video.available()) {
    video.read();
  }

  image(video, 0, 0, width, height);
}
実行結果:カメラ映像をそのまま表示する

video.read()でカメラの映像を読み込み、image(video, 0, 0)で画面に表示しています。

2. tint()を使った効果

tint()を使うと、画像や映像に色を重ねることができます。tint(r, g, b, alpha)のように書くと、不透明度も指定できます。

リスト2
import processing.video.*;

Capture video;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, 400, 400, cameras[0]);
  video.start();
}

void draw() {
  if (video.available()) {
    video.read();
  }

  // 映像に赤っぽい色を重ねる
  tint(255, 100, 100);
  image(video, 0, 0, width, height);
}
実行結果:カメラ映像に赤い色を重ねる

3. 座標変換を使った反転

scale()を使うと、カメラ映像を左右反転・上下反転できます。左右反転ではscale(-1, 1)を使います。ただしx方向が反転するので、表示位置を-widthにする必要があります。

リスト3
import processing.video.*;

Capture video;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, 200, 200, cameras[0]);
  video.start();
}

void draw() {
  if (video.available()) {
    video.read();
  }

  background(0);

  image(video, 0, 0, 200, 200);  // 左上:正像

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

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

  pushMatrix();
  scale(-1, -1);                 // 上下左右反転
  image(video, -400, -400, 200, 200);
  popMatrix();
}
実行結果:正像・左右反転・上下反転・上下左右反転

4. 回転するカメラ映像

translate()rotate()を使うと、カメラ映像を回転させることもできます。画像の基準点を画面中央に移動してから回転させるのがポイントです。

リスト4
import processing.video.*;

Capture video;
float angle = 0;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, 240, 180, cameras[0]);
  video.start();
  imageMode(CENTER);
}

void draw() {
  if (video.available()) {
    video.read();
  }

  background(0);

  translate(width/2, height/2);  // 回転の中心を画面中央にする
  rotate(angle);
  image(video, 0, 0);

  angle += 0.02;
}
実行結果:カメラ映像が画面中央で回転する

5. 1ピクセルずつ色を取り出す

画像処理では、1ピクセルずつ色を調べることが重要です。Processingでは、loadPixels()でピクセル配列を使えるようにし、pixels[y * width + x]で目的のピクセルを指定します。

x,yindex = y * width + x横に並んだピクセルを1本の配列として数える例:x=3, y=2 のピクセルを取り出す
図:2次元の座標を、1次元の配列番号に変換する考え方
リスト5
import processing.video.*;

Capture video;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, width, height, cameras[0]);
  video.start();
  loadPixels();
}

void draw() {
  if (video.available()) {
    video.read();
  }

  video.loadPixels();
  loadPixels();

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      // x,y座標を配列番号に変換する
      int index = y * width + x;
      pixels[index] = video.pixels[index];
    }
  }

  updatePixels();
}
実行結果:1ピクセルずつ読み取って同じ映像を描く

6. 色を変換する

ピクセルの赤・緑・青の値を取り出すと、グレー変換、色の反転、2色化などができます。

処理考え方
グレー変換赤・緑・青の平均を使う
色の反転255 - 色にする
2色化明るさがしきい値以上なら白、未満なら別の色にする
リスト6
import processing.video.*;

Capture video;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, width, height, cameras[0]);
  video.start();
  loadPixels();
}

void draw() {
  if (video.available()) {
    video.read();
  }

  video.loadPixels();
  loadPixels();

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      int index = y * width + x;
      color c = video.pixels[index];

      float r = red(c);
      float g = green(c);
      float b = blue(c);

      // グレー変換:RGBの平均を明るさにする
      float gray = (r + g + b) / 3;
      pixels[index] = color(gray);
    }
  }

  updatePixels();
}
実行結果:グレー・反転・2色化を切り替える

7. 時間の遅延を記録する

1行ずつ、または1列ずつ映像を記録していくと、時間のずれが画面に残ります。動いているものが歪んで表示されるのは、行や列ごとに撮影された時間が違うからです。

リスト7
import processing.video.*;

Capture video;
int row = 0;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, width, height, cameras[0]);
  video.start();
  loadPixels();
}

void draw() {
  if (video.available()) {
    video.read();
  }

  video.loadPixels();
  loadPixels();

  // 1行分だけ現在の映像からコピーする
  for (int x = 0; x < width; x++) {
    int index = row * width + x;
    pixels[index] = video.pixels[index];
  }

  updatePixels();

  stroke(255, 0, 0);
  line(0, row, width, row);

  row++;
  if (row >= height) {
    row = 0;
  }
}
実行結果:横方向のスキャンで時間差を記録する
リスト8
import processing.video.*;

Capture video;
int column = 0;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, width, height, cameras[0]);
  video.start();
  loadPixels();
}

void draw() {
  if (video.available()) {
    video.read();
  }

  video.loadPixels();
  loadPixels();

  // 1列分だけ現在の映像からコピーする
  for (int y = 0; y < height; y++) {
    int index = y * width + column;
    pixels[index] = video.pixels[index];
  }

  updatePixels();

  stroke(255, 0, 0);
  line(column, 0, column, height);

  column++;
  if (column >= width) {
    column = 0;
  }
}
実行結果:縦方向のスキャンで時間差を記録する

8. カメラ映像を図形で描く

カメラ映像をそのまま表示するのではなく、ピクセルの明るさや色を使って図形を描くと、モザイクや線画のような表現ができます。

リスト9
import processing.video.*;

Capture video;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, 80, 80, cameras[0]);
  video.start();
  rectMode(CENTER);
  noFill();
}

void draw() {
  if (video.available()) {
    video.read();
  }

  video.loadPixels();
  background(0);

  for (int y = 0; y < video.height; y++) {
    for (int x = 0; x < video.width; x++) {
      int index = y * video.width + x;
      color c = video.pixels[index];
      float brightness = (red(c) + green(c) + blue(c)) / 3;

      float rectSize = brightness / 255 * 20;
      stroke(c);
      rect(x * 5, y * 5, rectSize, rectSize);
    }
  }
}
実行結果:明るさに応じた四角形で映像を描く
リスト10
import processing.video.*;

Capture video;

void setup() {
  size(400, 400);
  String[] cameras = Capture.list();
  while (cameras.length == 0) {
    cameras = Capture.list();
  }
  video = new Capture(this, 80, 80, cameras[0]);
  video.start();
}

void draw() {
  if (video.available()) {
    video.read();
  }

  video.loadPixels();
  background(0);

  for (int y = 0; y < video.height; y++) {
    for (int x = 0; x < video.width; x++) {
      int index = y * video.width + x;
      color c = video.pixels[index];
      float brightness = (red(c) + green(c) + blue(c)) / 3;

      float lineSize = brightness / 255 * 30;
      stroke(c);
      line(x * 5 - lineSize/2, y * 5,
           x * 5 + lineSize/2, y * 5);
    }
  }
}
実行結果:明るさに応じた線で映像を描く

まとめ

命令・考え方意味
Captureカメラ映像を取得する
image()画像や映像を画面に表示する
tint()画像や映像に色を重ねる
scale(-1, 1)左右反転する
loadPixels()ピクセル配列を操作できるようにする
pixels[y * width + x]x,y座標のピクセルを配列番号で指定する
グレー変換RGBの平均を明るさとして使う
時間の遅延行や列ごとに違う時間の映像を記録する

今日の重要ポイント:画像処理の基本は、映像をそのまま表示することではなく、ピクセルの色を取り出して、別の色や別の図形に変換して描き直すことです。