ピクセルの再構成
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 = (red(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>
閑話休題
ここまで1pixelごとに真面目に処理を加えてきましたが、filter()関数を使うとこの辺のことは簡単に実装することもできます。
のリンクにあるように例えば次のようなコードを記述することでぼかし効果を簡単にえることもできます。
- 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); filter(BLUR,3); }
対象物を表現するには
私達が見ているのは,画像のピクセルの集合体です.画面におけるピクセルとは画像を表示するための最小単位になっています.単位について考えてみます.単位とはなんでしょうか?なにかものを表すための基準となる目盛りのことです.ではその基準は誰が決めるのか.それはエンジニアであったりデザイナであったりアーティストであったり,研究者であったり.単位はそれを表す上で実は自由に決めて良いものでもあります.その証拠に,オーム(Ω)やニュートン(N),アンペア(A),テスラ,ボルト,ベクレル,ヘルツなどその単位を定義付けた科学者の名前がそのまま単位になっている事例が少なからず知られています.
単位とは量を把握するための単なる仕組みであり、私達はこれを客観的な手法からときには超主観的な場合にもこの単位を利用して世界にアクセスしています。単位に関して個人的な短い記事を書いておいたので以下をお読みください。
さて、画素単位は1ピクセルです.ではここで,10×10ピクセルを1単位として考えてみます.一つの画素を一つの単位として考えるのではなく,10の画素を一つ単位として考えます.10ピクセルごとに丸で描画してみると,目の細かいモザイクのような効果になりました.
- sketch.js
var sample_image; function preload() { sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500, 336); noLoop(); } function draw() { background(0); for (let i = 0; i < sample_image.height; i+=10) { for (let j = 0; j < sample_image.width; j+=10) { let c = sample_image.get(j, i); stroke(c); strokeWeight(10); point(j, i); } } }
文字を単位にしてみる
この10ピクセル分の塊を別の単位に置き換えてみましょう. 文字'A'を単位にしてみます.
- sketch.js
var sample_image; function preload() { sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500, 336); noLoop(); } function draw() { background(255); for (let i = 0; i < sample_image.height; i+=10) { for (let j = 0; j < sample_image.width; j+=10) { let c = sample_image.get(j, i); fill(c); strokeWeight(0); textSize(10); text('A',j,i); } } }
let char_typed; function keyPressed(){ char_typed = key; }
というコードをsketch.jsに追記してあげると入力した文字を char_typed というグローバル変数に保存しておくことができます。これを利用してA以外の文字でピクセル表示に切り替えられるようにプログラムを書き換えて見ましょう。
文字の複雑さを濃度として考え、ピクセルを描画する
いわゆるアスキーアートと呼ばれる手法です。ピクセルに対応する濃度を文字種に変換することで、文字だけで絵を表現する独特の手法になります。ここまでですでに各ピクセルの明るさ情報は取得できているので、それを利用してどの文字を利用するのが良いかがわかれば実装ができそうです。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(c)とすることで、輝度情報を取得できる
の2つを追記できれば良いかなと思います。下記画像をよく見ると小さな文字だけで構成されているのがわかるかと思います。
- sketch.js
var sample_image; const 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', ';', ':', ',', '<', '^', '`', '.', ' ' ]; function preload() { sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500, 336); noLoop(); } function draw() { background(0); for (let i = 0; i < sample_image.height; i+=2) { for (let j = 0; j < sample_image.width; j+=2) { let c = sample_image.get(j, i); let pos = parseInt(map(brightness(c), 0,255, 0,char_pixel.length-1)); fill(255); textSize(2); text(char_pixel[pos],j,i); } } }
線で表現する
上記はピクセルをベースに対象物を再構成してみましたが,次は線を用いてみます.テレビの走査線のように左から右へ向けて黒い線を引きます。ただしただ真っ直ぐな線では真っ黒なキャンバスができあがるだけですので、画素の明るさ(brightness)に応じて線を少し上方向にずらして描画してみると、以下のような出力結果を得ることができます。すこし不思議な画像になりましたね。このサンプルではパラメータを調整できるように,index.htmlファイルも編集しているので、そちらの修正も同時に行ってください。
- sketch.js
var sample_image; let x_step, y_step, y_max; function preload() { sample_image = loadImage("seurat.png"); } function setup() { createCanvas(500, 336); select('#x_step').changed(xChanged); select('#y_step').changed(yChanged); select('#y_max').changed(maxChanged); x_step = select('#x_step').value(); y_step = select('#y_step').value(); y_max = select('#y_max').value(); noLoop(); } function draw() { background(255); stroke(0); for (let i = 0; i < sample_image.height; i += y_step) { beginShape(); for (let j = 0; j < sample_image.width; j += x_step) { let c = sample_image.get(j, i); let b = brightness(c); b = map(b, 0, 255, 0, y_max); vertex(j, i - b); } endShape(); } } function xChanged() { x_step = this.value(); draw(); } function yChanged() { y_step = this.value(); draw(); } function maxChanged() { y_max = this.value(); 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> x_step:<input type="range" id="x_step" value="1", min="1", max="10"><br> y_step:<input type="range" id="y_step" value="1", min="1", max="10"><br> y_max:<input type="range" id="y_max" value="5" min="0" max="50"><br> </body> </html>
ここで少し工夫している箇所は vertex 関数において,ただ座標を正しくうつのではなく,その画素の グレースケール値に基づいて上下に位置を動かしていることです.これにより明るめの画素は上方向に座標が 移動し,暗めの画素は下方向に画素が移動します.
線の本数や頂点座標のずらし方を変更してみる
次はプログラムを少し最初に戻して,描く線にアルファ値をもたせてみます.アルファ値とは 透明度のことで,詳細は https://p5js.org/reference/#/p5/alpha を参照すると よいでしょう.
この透明度を利用し,これまで真っ黒で書いていた線に対して一定の透明度(stroke(0,0,0,100);)をつけてみます. 実行結果とプログラムは下記の通りです.面白いことに結果として得られた画像は,凹凸がついた銀盤のように見えますね。なお下記画像のパラメータは
- x_step = 1;
- y_step = 1;
- y_max = 5;
としています。
ここまで学習した内容から、単位を別のなにかに置き換えることでスーラの絵画を再構成してください。
- Hint
- 文字をピクセルにしたように、様々な文字(漢字やカタカナ等)も単位として表現しても良いかもしれません
- beginShape()にLINESやTRIANGLE_FAN等を使って表示した場合どのようになるか試してみると良いかもしれません。
- 出力した結果がもとの絵からかけ離れてしまっても面白いかもしれません。