文書の過去の版を表示しています。
ピクセルの再構成
Georges Seurat [Public domain], via Wikimedia Commons
コンピュータにおいて画像の表示は,細かな点を密集させ表示させることで精密な描画を実現しています.このような手法は絵画では点描画と呼ばれ19世紀後半のスーラによるものが有名です.上記画像はGeorges Seuratの代表作である A Sunday on La Grande Jatteです.画像をクリックして拡大してその描画の細かさをよく見てみましょう.一点一点細かな点で描かれているのが確認できます.コンピュータの画像はドットの集まりであることは当然のこととして考えてしまいがちですが,人間の感覚からすると,目の前のものを点で描くということはストロークで線を描くことと比較すると大きく異るものです.
まずはスーラのサンプル画像を読み込み,それを表示するだけのプログラムを記述してみます.すでにテンプレートを用意しているので、以下のプロジェクトをデュプリケートして実行結果を確認しましょう。
Sample01
- sketch.js
var sample_image; function preload(){ sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500,336); noLoop(); } function draw() { background(0); image(sample_image,0,0); }
p5.jsではファイルの読み込みにはpreload()関数を利用する必要があります。これはp5.jsがhtmlファイルをベースとしていることに起因しています。一般的にウェブアプリケーションはファイル読み込みもネットワーク越しに行われるため、ファイル読み込みに時間がかかる場合があります。これによって、プログラム上で画像ファイルを表示しようとするときにはまだ読み込みが終わっておらず描画に失敗してしまうからです。例えば以下のようなプログラムの場合は画像の読み込みが間に合わず、画像が表示されなくなります。noLoop()をコメントアウトすることで、draw()関数がループするので、キャンバスが表示されたあと画像が送れて表示される様子を確認してみてください。ネットワーク環境がよければ一瞬で画像表示されるのでほとんど気づかないかもしれませんが。
var sample_image; function setup() { sample_image = loadImage("seurat.png"); createCanvas(500, 336); noLoop(); } function draw() { background(0); image(sample_image, 0, 0); }
上記プログラムではimage関数から画像描画していますが,これでは 各ピクセル画像の値を操作することができていません.そこで,次のようなプログラムに変えること で,各ピクセルデータを取得し,点描画と同じやり方で静止画像を描画してみます.
Sample02
- sketch.js
var sample_image; function preload() { sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500, 336); noLoop(); } function draw() { background(0); image(sample_image, 0, 0); for (let i = 0; i < sample_image.height; i++) { for (let j = 0; j < sample_image.width; j++) { let c = sample_image.get(j, i); stroke(c); point(j, i); } } }
実行結果は先程と変わらないことが確認できたと思います.ただし中味は一つひとつのピクセルデータの色を読み込んで、その色で点描画(point()関数)しています。では再構成の簡単な事例として, 各色データにおけるRGBをBGRの順に入れ替えて表示してみましょう.
Sample03
- sketch.js
var sample_image; function preload() { sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500, 336); noLoop(); } function draw() { background(0); image(sample_image, 0, 0); for (let i = 0; i < sample_image.height; i++) { for (let j = 0; j < sample_image.width; j++) { let c = sample_image.get(j, i); stroke(blue(c), green(c), blue(c)); point(j, i); } } }
RとBの画素情報を入れ替えた結果ですね。赤色と青色の部分が元画像から入れ替わっているのがわかると思います.では次にグレースケールに変更してみましょう.RGBに各色を設定していましたが,ここで,RGB画素の平均値を与えることで, グレースケールに変更出来ます.
Sample04
- sketch.js
var sample_image; function preload() { sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500, 336); noLoop(); } function draw() { background(0); image(sample_image, 0, 0); for (let i = 0; i < sample_image.height; i++) { for (let j = 0; j < sample_image.width; j++) { let c = sample_image.get(j, i); let gray = (blue(c)+green(c)+blue(c))/3; stroke(gray); point(j, i); } } }
ここで,イメージをグレースケールにすることができました.では次に,閾値(しきいち)を設けて,画像を二値化してみましょう.つまりこの画像の明暗を分ける処理になります.すでにグレースケールの値は各画素において取得できているので,これと適当な値を比べることで,画像を白と黒の二値にしてみます.折角なので、2値化のしきい値はhtml上で実装して、スライダーを動かすことでしきい値の値を調整できるようにもしてみましょう。
Sample05
htmlのUIを追加するので、index.htmlも修正するのを忘れずに。
- sketch.js
var sample_image; function preload() { sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500, 336); select('#threshold').changed(changedThreshold); noLoop(); } function draw() { background(0); let threshold = select('#threshold').value(); for (let i = 0; i < sample_image.height; i++) { for (let j = 0; j < sample_image.width; j++) { let c = sample_image.get(j, i); let gray = (blue(c) + green(c) + blue(c)) / 3; if( gray < threshold ){ stroke(0); } else{ stroke(255); } point(j, i); } } } function changedThreshold() { draw(); }
- index.html
<!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/addons/p5.sound.min.js"></script> <link rel="stylesheet" type="text/css" href="style.css"> <meta charset="utf-8" /> </head> <body> <script src="sketch.js"></script> <input type="range" id="threshold" value="127", min="0", max="255"> </body> </html>
次に二値化情報を元に,grayが255の箇所は色を復元することにすると,次のような結果になります. 結果として比較的明度の高いラインが残るようになりました.このように画像に対して二値化を行い, 処理を行なう対象領域を限定する手法は画像処理(Computer Vision)において非常に一般的な手法です. もちろん静止画や動画像における再構成要素としても利用価値が高いものです.
Sample05
- sketch.js
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値化については,手動でやるのも問題ないですが,実は画像を扱うfilter()なるものが 用意されているので,それを使えば一発でできます.ここまで真面目に1ピクセルごとに処理していましたが、これらの画像処理用の関数も使ってみましょう。たとえばBLURを利用すると、画像のボケ味を変更することができます。
対象物を表現するには
ここまでのサンプルは抽象的な画像を用いて来ましたが,少し具体的な下記画像に変えてみたいと思います.
私達が見ているのは,画像のピクセルの集合体です.画面におけるピクセルとは画像を表示するための最小単位になっています. 単位について考えてみます.単位とはなんでしょうか?なにかものを表すための基準となる目盛りのことです.ではその基準は 誰が決めるのか.それはエンジニアであったりデザイナであったりアーティストであったり,研究者であったり.単位はそれを 表す上で実は自由に決めて良いものでもあります.その証拠に,オーム(Ω)やニュートン(N),アンペア(A),テスラ,ボルト, ベクレル,ヘルツなどその単位を定義付けた科学者の名前がそのまま単位になっている事例が少なからず知られています.
上記で見ている画像はニューヨークのセントラルパーク前の横断歩道前で私が撮影した画像です.800×597のピクセル数で構成 されており,画素単位は1ピクセルです.ではここで,10ピクセルを1単位として考えてみます.一つの画素を一つの単位として考えるのではなく,10の画素を一つ単位として考えます.
- 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); } }
上記のプログラムを setup(), draw()で下記直し,tweakモードで実行してみましょう.それぞれforループにあるi, jの増加数を変化させることで画像がどのように変わるかを観察してみよう.
10ピクセルごとに丸で描画してみると,目の細かいモザイクのような効果になりました.ではこの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(); }
線で表現する
以下のサンプルプログラムはsetup(), draw()の内プログラムになっています.自分でvoid setup(){} void draw(){}に書き直して tweakモード実行しつつ,プログラムの振る舞いを観察してみましょう.
上記はピクセルをベースに対象物を再構成してみましたが,次は線を用いてみます.
- 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(); }