Processingでデッサン風の画像を作成する

ゴールデンウィークも残りあと数日。仕事に復帰できる気が全然しません。

子供が美術の宿題で自分の手のデッサンをしていて、陰影のつけ方が微妙に間違っていたので指摘したのですが、自分でも違っているのはわかるが、どこがどう違うかよくわからないとの返事が返ってきました。

よろしい、ならば実装(Processing)だ。ということで、Processingでお手本のデッサン画像を自動作成するスケッチを作ってみることにします。

デッサン風画像の作成方法

デッサン風画像の作成は、以下の3つのステップで行います。

  1. 参考画像を取得
  2. 画像のピクセル情報を参照
  3. 各ピクセルを鉛筆風に描画

各ステップの詳細を順に説明します。

参考画像を取得

デッサンの基になる画像はWebカメラでキャプチャしたものを使うことにします。Processing 2.xでは以下のコードでカメラのキャプチャを開始できます。

import processing.video.*;

Capture capture;

void setup() {
  String[] cameras = Capture.list(); // 利用可能なカメラのリストを取得する
  capture = new Capture(this, cameras[0]); // 0番のカメラを初期化
  capture.start(); // キャプチャを開始する
  // 以下略
}

画像のピクセル情報を参照

続いてキャプチャされた画像から、個々のピクセル情報を取得します。

以下のコードでは、次のステップ用にピクセルの明度を計算する処理も含めてあります。

void draw() {
  if (capture.available()) { // カメラが更新された
    capture.read(); // キャプチャされた画像を読み込み
    capture.loadPixels(); // capture.pixelsにピクセル情報をコピー

     // 事前にx, yに参照したいピクセルの座標を代入する(実際のコードは省略)
     color c = capture.pixels[capture.width * y + x]; // 指定座標のカラーを取得
     int b = (((c >> 16) & 0xff) + ((c >> 8) & 0xff) + (c & 0xff)) / 3; // (R+G+B)/3で明度を計算
  }
  // 以下略
}

明度の計算は、本当はY = ( 0.298912 * R + 0.586611 * G + 0.114478 * B )で計算するのが望ましいのですが、デッサン画像の参考に使うだけなので、3で割った単純な平均を使っています。

各ピクセルを鉛筆風に描画

最後に、各ピクセルを鉛筆で描いたようなデッサン調で描画します。

ここではピクセルを一定間隔で参照(サンプリング)し、ピクセルの明度に応じて半透明の黒い線を描画します。具体的には、対象ピクセルの座標付近に、暗いピクセルならば濃い黒の線を何本も重ね描きし、明るいピクセルの時は薄い黒の線を数本だけ描画します。

final int GRID = 4; // ピクセルをサンプルリングする間隔

void drawLines(int x, int y, int brightness) {
  stroke(0, 64 - (brightness / 5)); // 色は黒、明るいほど透明に(=薄く)する

  float shade = (255.0f - brightness) / 60.0f; // 暗いほど線の本数を増やす
  shade = pow(shade, 3.0f) - 2.0f; // 3乗して明暗の差を極端にした後、-2して意図的に白飛びを起こす

  for (int i = 0; i < shade; i++) { // shadeの値分だけ、指定座標付近にランダムの短い線を描画する
    line(x + random(GRID), y + random(GRID), x + GRID + random(GRID), y + GRID + random(GRID));
  }
}

正確な描画だと単なる白黒写真になってしまうので、線の本数を計算する式にいろいろ細工してデッサンっぽくなるようにしています。

実際に動かしてみある

デッサン風の画像を自動作成するスケッチのコード全文は以下になります。

import processing.video.*;

final int GRID = 4;
Capture capture;

void setup() {
  size(640, 480);

  String[] cameras = Capture.list();
  capture = new Capture(this, cameras[0]);
  capture.start();
}

void draw() {  
  if (capture.available()) {
    noFill();
    strokeWeight(1);
    background(255);

    capture.read();
    capture.loadPixels();

    for (int i = 0; i < capture.height && i < height; i += GRID) {
      for (int j = 0; j < capture.width && j < width; j += GRID) {
        color c = capture.pixels[capture.width * i + j];
        int b = (((c >> 16) & 0xff) + ((c >> 8) & 0xff) + (c & 0xff)) / 3;
        drawLines(width - j, i, b);
      }
    }
  }
}

void drawLines(int x, int y, int brightness) {
  stroke(0, 64 - (brightness / 5));

  float shade = (255.0f - brightness) / 60.0f;
  shade = pow(shade, 3.0f) - 2.0f;

  for (int i = 0; i < shade; i++) {
    line(x + random(GRID), y + random(GRID), x + GRID + random(GRID), y + GRID + random(GRID));
  }
}

実際にコードを動かして作成した画像がこちらです。

①美術の課題でよく出る手のデッサン風画像

f:id:tkitao:20140504154220p:plain

②同じく課題によく出るガラスのコップのデッサン風画像

f:id:tkitao:20140504154227p:plain

終わりに

今回コードを書いてみて、改めてProcessingはこういった試行錯誤の必要な画像処理が書きやすいと感じました。ただ、パフォーマンス度外視で書いてしまったので非常に重いです。若干手間は増えますが、リアルタイム性を求めるならopenFrameworksでの実装を視野に入れてもいいかもしれません。