lecture:design_with_prototyping:ピクセル再構成

ピクセルの再構成

Georges Seurat [Public domain], via Wikimedia Commons

コンピュータにおいて画像の表示は,細かな点を密集させ表示させることで精密な描画を実現しています.このような手法は絵画では点描画と呼ばれ19世紀後半のスーラによるものが有名です.上記画像はGeorges Seuratの代表作である A Sunday on La Grande Jatteです.画像をクリックして拡大してその描画の細かさをよく見てみましょう.一点一点細かな点で描かれているのが確認できます.コンピュータの画像はドットの集まりであることは当然のこととして考えてしまいがちですが,人間の感覚からすると,目の前のものを点で描くということはストロークで線を描くことと比較すると大きく異るものです.

まずは下記静止画像を読み込み,それを表示するだけのプログラムを記述してみます.静止画像を別名保存し, スケッチファイルのdataファイルの中にtest.pngとして保存してください. Processing3以上の場合,size()関数に変数を引き渡すことが禁止されてしまっているので,直接画像サイズを入力してください.今回のtest.pngの画像サイズは628,350となっています.

sample1.pde
// Processing v.3
PImage img;
img = loadImage("test.png");
size(628, 350);
image(img,0,0,img.width, img.height);

たった4行ですが,指定した画像を表示するプログラムが書けました.ではこの画像を再構成する にはどのようなことをすればよいでしょうか?デジタル画像において,全ての画像はピクセルから 構成されます.つまりピクセルデータを扱うことで,同じ画像データから様々な表示に再構成できる はずです.上記プログラムではimage関数から直接PImageの中身を描画していますが,これでは 各ピクセル画像の値を操作することができていません.そこで,次のようなプログラムに変えること で,各ピクセルデータを取得し,点描画と同じやり方で静止画像を描画してみます.

sample2.pde
PImage img;
img = loadImage("test.png");
size(628, 350);
imageMode(CENTER);
noStroke();
background(255);
for( int i = 0; i < img.height; i++ ){
  for( int j = 0; j < img.width; j++ ){
    color c = img.get(j,i);
    stroke(red(c), green(c), blue(c));
    point(j, i);  
  }
}

実行結果はsample1と変わらないことが確認できたと思います.では再構成の簡単な事例として, 各色データにおけるRGBをBGRの順に入れ替えて表示してみましょう.

sample3.pde
PImage img;
img = loadImage("test.png");
size(628, 350);
imageMode(CENTER);
noStroke();
background(255);
for( int i = 0; i < img.height; i++ ){
  for( int j = 0; j < img.width; j++ ){
    color c = img.get(j,i);
    stroke(blue(c), green(c), red(c));
    point(j, i);  
  }
}

実行してみて,一見するとなにも変わっていないように感じるかもしれませんが, 赤色と青色の部分が元画像から入れ替わっているのがわかると思います.では次にグレースケールに変更 してみましょう.RGBに各色を設定していましたが,ここで,RGB画素の平均値を与えることで, グレースケールに変更出来ます.

sample4.pde
PImage img;
img = loadImage("test.png");
size(628, 350);
imageMode(CENTER);
noStroke();
background(255);
for( int i = 0; i < img.height; i++ ){
  for( int j = 0; j < img.width; j++ ){
    color c = img.get(j,i);
    float gray = (red(c)+green(c)+blue(c))/3;
    stroke(gray,gray,gray);
    point(j, i);  
  }
}

ここで,イメージをグレースケールにすることができました.では次に,閾値(しきいち)を設けて, 画像を二値化してみましょう.つまりこの画像の明暗を分ける処理になります.すでにグレースケールの 値は各画素において取得できているので,これと適当な値をくれべることで,画像を白と黒の二値に してみます.

sample5.pde
PImage img;
img = loadImage("test.png");
size(628, 350);
imageMode(CENTER);
noStroke();
background(255);
for( int i = 0; i < img.height; i++ ){
  for( int j = 0; j < img.width; j++ ){
    color c = img.get(j,i);
    float gray = (red(c)+green(c)+blue(c))/3;
    if( gray < 50 ){
      gray = 0;
    }
    else{
      gray = 255;
    }
    stroke(gray,gray,gray);
    point(j, i);  
  }
}

次に二値化情報を元に,grayが255の箇所は色を復元することにすると,次のような結果になります. 結果として比較的明度の高いラインが残るようになりました.このように画像に対して二値化を行い, 処理を行なう対象領域を限定する手法は画像処理(Computer Vision)において非常に一般的な手法です. もちろん静止画や動画像における再構成要素としても利用価値が高いものです.

sample6.pde
PImage img;
img = loadImage("test.png");
size(628, 350);
imageMode(CENTER);
noStroke();
background(255);
for( int i = 0; i < img.height; i++ ){
  for( int j = 0; j < img.width; j++ ){
    color c = img.get(j,i);
    float gray = (red(c)+green(c)+blue(c))/3;
    if( gray < 80 ){
      gray = 0;
    }
    else{
      gray = 255;
    }
 
    if( gray == 255 ){
      stroke(red(c),green(c),blue(c));
    }
    else{
      stroke(255);
    }
    point(j, i);  
  }
}

練習 上記の2値化については,手動でやるのも問題ないですが,実はPImageのメソッドにはfilter()なるものが 用意されているので,それを使えば一発でできます. http://processing.org/reference/PImage_filter_.html を参考にして,同じ事をfilter関数を利用して実装してみましょう.

対象物を表現するには

ここまでのサンプルは抽象的な画像を用いて来ましたが,少し具体的な下記画像に変えてみたいと思います.  サンプルとなる画像

私達が見ているのは,画像のピクセルの集合体です.画面におけるピクセルとは画像を表示するための最小単位になっています. 単位について考えてみます.単位とはなんでしょうか?なにかものを表すための基準となる目盛りのことです.ではその基準は 誰が決めるのか.それはエンジニアであったりデザイナであったりアーティストであったり,研究者であったり.単位はそれを 表す上で実は自由に決めて良いものでもあります.その証拠に,オーム(Ω)やニュートン(N),アンペア(A),テスラ,ボルト, ベクレル,ヘルツなどその単位を定義付けた科学者の名前がそのまま単位になっている事例が少なからず知られています.

上記で見ている画像はニューヨークのセントラルパーク前の横断歩道前で私が撮影した画像です.800×597のピクセル数で構成 されており,画素単位は1ピクセルです.ではここで,100ピクセルを1単位として考えてみます.一つの画素を一つの単位として考えるのではなく,100の画素を一つ単位として考えます.

sample07.pde
PImage img;
img = loadImage("sample.jpg");
size(800, 597);
imageMode(CENTER);
noStroke();
background(255);
for ( int i = 0; i < img.height; i=i+10 ) {
  for ( int j = 0; j < img.width; j=j+10 ) {
    color c = img.get(j, i);
    stroke(red(c), green(c), blue(c));
    fill(red(c),green(c),blue(c));    
    strokeWeight(10);
    point(j, i);
  }
}

10ピクセルごとに丸で描画してみると,目の細かいモザイクのような効果になりました.次は文字'A'を単位にしてみます.

sample08.pde
PImage img;
img = loadImage("sample.jpg");
size(800, 597);
imageMode(CENTER);
noStroke();
background(255);
for ( int i = 0; i < img.height; i=i+10 ) {
  for ( int j = 0; j < img.width; j=j+10 ) {
    color c = img.get(j, i);
    fill(red(c),green(c),blue(c));    
    text("A",j,i);
  }
}

文字の複雑さを濃度として考え、ピクセルを描画する

いわゆるアスキーアートと呼ばれる手法です。ピクセルに対応する濃度を文字種に変換することで、文字だけで絵を表現する独特の手法になります。ここまでですでに各ピクセルの明るさ情報は取得できているので、それを利用してどの文字を利用するのが良いかがわかれば実装ができそうです。Character representation of grey scale imagesという1997年に書かれたサイトには、白と黒の明るさを文字種に対応させた一覧が載せられています。

char[] char_pixel = {'$', '@', 'B', '%', '8', '&', 'W', 'M', '#', '*', 'o', 'a', 'h', 'k', 
'b', 'd', 'p', 'q', 'w', 'm', 'Z', 'O', '0', 'Q', 'L', 'C', 'J', 'U', 'Y', 'X', 'z', 
'c', 'v', 'u', 'n', 'x', 'r', 'j', 'f', 't', '/', '|', '(', ')', '1', '{', '}', '[', ']', 
'?', '-', '_', '+', '~', '<', '>', 'i', '!', 'l', 'I', ';', ':', ',', '<', '^', '`', '.', ' ' };

上記の配列を利用して、真っ暗だと '$' を、真っ白だと ' ' を表示するプログラムを記述します。ポイントとしては

  • 0-255 のピクセルbrightnessを 0 - (char_pixel.length-1)のサイズに変換する
  • brightness(c1)とすることで、輝度情報を取得できる

の2つを追記できれば良いかなと思います。下記画像をよく見ると小さな文字だけで構成されているのがわかるかと思います。

sample08_2.pde
import processing.video.*;
char[] char_pixel = {'$', '@', 'B', '%', '8', '&', 'W', 'M', '#', '*', 'o', 'a', 'h', 'k', 
  'b', 'd', 'p', 'q', 'w', 'm', 'Z', 'O', '0', 'Q', 'L', 'C', 'J', 'U', 'Y', 'X', 'z', 
  'c', 'v', 'u', 'n', 'x', 'r', 'j', 'f', 't', '/', '|', '(', ')', '1', '{', '}', '[', ']', 
  '?', '-', '_', '+', '~', '<', '>', 'i', '!', 'l', 'I', ';', ':', ',', '<', '^', '`', '.', ' ' };
 
PImage img;
size(800, 597);
img = loadImage("sample.jpg");
imageMode(CENTER);
noStroke();
print(char_pixel.length);
textSize(5);
 
noFill();
background(0);  
for ( int i = 0; i < img.height; i=i+5 ) {
  beginShape();
  for ( int j = 0; j < img.width; j=j+5 ) {
    color c1 = img.get(j, i);
    float brightness = map(brightness(c1), 0, 255, 0, char_pixel.length-1);
    text(char_pixel[int(brightness)], j, i);
  }
  endShape();
}

上記はピクセルをベースに対象物を再構成してみましたが,次は線を用いてみます.

sample09.pde
PImage img;
img = loadImage("sample.jpg");
size(800, 597);
imageMode(CENTER);
noFill();
background(255);
for( int i = 0; i < img.height; i++ ){
 
  beginShape();
  for( int j = 0; j < img.width; j++ ){
    color c = img.get(j,i);
    float gray = (red(c)+green(c)+blue(c))/3;
    stroke(0,0,0);
    vertex(j,i-gray/10.0);
  }
  endShape();
}

ここで少し工夫している箇所は vertex 関数において,ただ座標を正しくうつのではなく,その画素の グレースケール値に基づいて上下に位置を動かしていることです.これにより明るめの画素は上方向に座標が 移動し,暗めの画素は下方向に画素が移動します.上記プログラムはそれを画像サイズの縦ピクセル分すべて 描画していますが,これを少し間引いて下記のようなプログラムに変更してみます.これまで i++ としてた ものを i=i+5 と変更したのみです.

sample10.pde
PImage img;
img = loadImage("sample.jpg");
size(800, 597);
imageMode(CENTER);
noFill();
background(255);
for( int i = 0; i < img.height; i=i+5 ){ // 変更箇所
 
  beginShape();
  for( int j = 0; j < img.width; j++ ){
    color c = img.get(j,i);
    float gray = (red(c)+green(c)+blue(c))/3;
    stroke(0,0,0);
    vertex(j,i-gray/10.0);
  }
  endShape();
}

次はプログラムを少し最初に戻して,描く線にアルファ値をもたせてみます.アルファ値とは 透明度のことで,詳細は https://processing.org/tutorials/color/ を参照すると よいでしょう.

この透明度を利用し,これまで真っ黒で書いていた線に対して一定の透明度をつけてみます. 実行結果とプログラムは下記の通りです.面白いことに結果として得られた画像は,デプスマップ(深度情報) を持った画像のように見えますね.

sample12.pde
PImage img;
img = loadImage("sample.jpg");
size(800, 597);
imageMode(CENTER);
noFill();
background(255);
for( int i = 0; i < img.height; i++ ){
 
  beginShape();
  for( int j = 0; j < img.width; j++ ){
    color c = img.get(j,i);
    float gray = (red(c)+green(c)+blue(c))/3;
    stroke(0,0,0,100);
    vertex(j,i-gray/10.0);
  }
  endShape();
}
  • lecture/design_with_prototyping/ピクセル再構成.txt
  • 最終更新: 2019/11/22 00:55
  • by baba