[Go to “teaching” in Morimoto Lab]

Game by ball 8

①画素へのアクセス(画像処理)
②メタボール

①は画像処理の基本で、OpenCVのようなライブラリに任せてしまうのではなく、そのような画像処理の中身を理解し、自分で1から実装できるようにしておくのに不可欠です。

②はCGの手法の一つで、滑らかな境界を作るために用いられます。
今回は簡易版です。

※この回は講義2コマ分になるかも。


1. 画素の操作による画像処理

画像処理の便利な関数はいくつかProcessingに用意されているが、自分でそのような画像処理を実装する場合を見て行く。
Images and Pixelsという公式チュートリアルの “Pixels, pixels, and more pixels”の一部分を以下で説明する。


↑画素の実際の並びは上の図のようになっていますが、
画素は1次元配列で下の図のように格納されています。

↓Processingの実行画面の画素を扱う最もシンプルな例
http://learningprocessing.com/examples/chp15/example-15-05-PixelArray

size(200, 200);
// ↓画素を扱う前に必ず記述しましょう
loadPixels();
// 画素配列のlengthなので、画素の全てを見て行きます
for (int i = 0; i < pixels.length; i++) {
// 0から255の乱数
float rand = random(255);
// 乱数でグレイスケールの色を決めます。color型については調べてください。
color c = color(rand);
// ↓画素の色を設定
pixels[i] = c;
}
// ↓画素の処理を終わるときに必ず記述する
updatePixels();
  1. loadPixels() allows us to capture the window.

  2. pixels[width * height] allows us to change pixel colors one by one.

  3. updataPixels() allows us to reflect the change. Put this when finishing dealing with pixels.


↓任意の位置の画素にアクセス、および、RGBを扱う例

loadPixels();
for (int x= 0; x<width; x++) {
for (int y= 0; y<height; y++) {
int pixCol = pixels[x + y * width];
float r = red(pixCol); //(pixCol >> 16) & 0xff;
float g = green(pixCol); //(pixCol >> 8 ) & 0xff;
float b = blue(pixCol); //(pixCol & 0xff);
//r=0;//値を変えたい場合
//ウィンドウのピクセルに当てはめる
pixels[y * width + x] = color(r, g, b);
}
}
updatePixels();
  1. red(), green(), blue() これらの関数によってRGB成分を個別に取り出せる。
    このほか、hue(), saturation(), brightness(), alpha()などもある

  2. x+y*width番目のpixels[]の要素が(x,y)の画素です。
    この場合、実行画面が画像なので、ウィンドウの横幅であるwidthを使っていますが、もしPImageで定義した画像を使う場合は、画像の横幅をwidthに入れてください。


余談ですが、for (int i = 0; i < pixels.length; i++){ }
のとき、iからx,yを求めることもできます。
x=i%width, y=i/widthですね。


Note:ビットシフトでRGBを取り出す
上記サンプル内のコメントアウト部分は、これらの関数と同じ働きをする別のやり方。
ビットシフトという演算を使っている。
intは4byte(=32bit)なので、1byteずつ各色の成分を割り当てられる。
色を表す場合の順番は(A)RGBとなっており、1画素を1つのintで表すと以下のような構造になっている。
00000000 00000000 00000000 00000000
↑透明   ↑赤   ↑緑    ↑青
ビットシフトで取り出したい色を右側8桁に移動し、0xffつまり、11111111でその部分だけを取り出す(ビットANDの演算)と、該当する色の値を取り出すことができる。[Processingのright shift参照]


Ex A: 以下の二つのサンプルコードを一部変更して、色をつけなさい。

Random code.
Stripe code.

random noise image stripe image

Ex B: Make the following images by using pixels() and so on.

point()などで描画するのではなく、pixels配列を用いて画像を生成してください。
色を付けても面白いですよ。

Alt text Alt text

Hint:

  1. 左図を作るには、画面中央からの各画素の距離を計算し、ある一定の距離より小さい場合は白、大きい場合は黒、とするとできる。

  2. 右図を作るには、まず画面中央からの距離を画素に置き換えてみるとよい。
    その後、画素値の反転や自乗、適当な係数をかけるなどして、調整してみよう。
    また、自乗やべき乗 sq(), pow()を使ってみましょう。
    sqやpowはなくても光っているようになるが、光の広がり方を調整する場合に有効です。

Ex C: PImageで読み込んだ画像の画素を使って、画像処理フィルタを実装しなさい。

簡単なものなら、グレイスケール化などをしてみてください。

できそうな人は、8近傍の平均化フィルタなどをやってみましょう。

Note : PImageによる画素の処理
PImage も実行画面の処理の時に使った以下の関数と配列を持っています。
loadPixels()
pixels[]
updatePixels()
PImageを使った画素の処理の例はこちらを見てください

余裕がある人へ: Images and Pixelsの残りの部分を上から順番に実行してみましょう。

大事な豆知識:オーバーフロー
↓以下は2022年度の自由課題としてHiragaさんが提出してくれたものです。
Alt text
このようなグラデーションは0-255まで使用する色の情報のところにそれ以上の値を入れることで起こっていると思われます。
これはオーバーフローという挙動に関係しています。
以下のYouTube動画の5分30秒あたりでその解説を話しています。
よかったら見てみてください。
https://www.youtube.com/watch?v=z981JGWkYeg


2. 簡易メタボール

メタボールとは?
滑らかな線・面を作るためのCGの手法。
点を空間に配置し、その点からの距離が離れると濃度が低くなる空間を作る。
点が複数ある場合、濃度は加算され、濃度の等高線は滑らかなものになる。
この等高線を得るために、濃度の同じピクセルをmarching cube法で面を張る。
(2Dの場合はmarching square)

metaball

以上がメタボールだが、ここでは最後の面をはったり、境界線を作るmarching cube / squareを省略し、簡易的にメタボールを作成する。

メタボールを実装するためには、ピクセル毎に濃度を計算する必要があるため、実行画面の画素を取り出し、書き換えを行う必要がある。
つまり、画像処理を行う必要がある。
※loadPixels(), updatePixels(), pixels[]の使用例は前半のテキストを見てください。

簡易メタボールのアルゴリズム

  1. ボールの中心となる複数の点を配置する(最初は2つで実装する)

  2. 画素毎にすべての点に対する距離を計算し、足して持っておく

  3. 足した値は適当に調整する

  4. 値を2値化すれば境界ができるので、簡易メタボールの完成

今回は上記に加えて、N値化とアニメーションを行い、面白い絵を作りましょう。

メタボールのための最低限のプログラム1:ボールを作る

ArrayList<Metaball> metaballs;
void setup(){
size(500,500);
metaballs = new ArrayList<Metaball>();
metaballs.add(new Metaball(new PVector(150,200), 200f));
metaballs.add(new Metaball(new PVector(300,200), 200f));
}
void draw(){
background(0);
for(int i=0 ; i<metaballs.size(); i++){
Metaball b = metaballs.get(i);
ellipse(b.pos.x, b.pos.y, b.radi, b.radi);
}
}
class Metaball
{
PVector pos;
float radi;
Metaball(PVector pos0, float radi0){
pos = pos0;
radi = radi0;
}
}

Alt text

メタボールのための最低限のプログラム2:単純な距離の可視化

上記プログラムのdraw()のところを、以下のように変更する。
コード内のコメントアウトに説明があります。

void draw(){
background(0);
loadPixels();
//↓全ての画素を見るfor分
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
int index = x + y * width;
float sum = 0;
//↓一つの画素に対し、すべてのmetaballからの距離を積算している。
//for (Blob b : blobs) {//←蛇足:以下2行の代わりにこのようにも書ける
for (int i=0; i < metaballs.size(); i++) {
Metaball b = metaballs.get(i);
//↓画素からボール中心への距離
float d = dist(x, y, b.pos.x, b.pos.y);
sum += d;//1.
}
//2.
pixels[index] = color(sum);
//pixels[index] = color(sum, 255, 255);
}
}
updatePixels();
}

Alt text

Ex D. sumの計算で、単純に距離を足した上記のプログラムに対し、きれいに見えるように値を調整してみよう。(上記プログラムの1の部分)

以下は一例:Tweakでここの値を変えてみるとよい。

sum += 50 * b.radi / d;

Ex E. sumの値によって結果画像を4値化してみよう(上記プログラムの2の部分に記載する)

ifで場合分けをする方法が単純である。

ちなみに:
Processingではcolorへの値の指定時、0未満や256以上の値は勝手に0に切り上げたり、255に切り捨てられる。

しかし、他の言語ではそうとは限らないので、曖昧なプログラムにならないようにif文などで切り上げや切り捨ての処理を自分で行う。
こんなところにバグが潜んでいる場合もある。

Alt text
↑4値化した結果。

Ex F. (上記プログラムの2の部分)sumの値によって結果画像をN値化してみよう

N値化というのは、
int N;
を用意し、このNにどんな値を入れてもN値化されるようにする、というものです。
少し難しいかもしれません。
まずは2値化、3値化の場合で考えてみて、次にN値化について考えてみるとよいでしょう。

Hint 1:
forや、breakを使って各画素がどの値域に入っているか確認していく方法がストレートな考え方ですが少し実装は面倒です。

一方で、少しトリッキーな方法も紹介します。
一旦決まった画素値を正規化(ここでは255で割って0-1に直すこと)した上で、N倍し、int型に変換(キャストという)した上で255/Nを乗算するという方法もあり、こちらは実装上は非常に楽です。

Hint 2:
グレイスケールで実装したとき、ちゃんと黒(0)と白(255)が出ていますか?

余裕のある人へ. ボールを増やしたり、動かしたりして複雑な結果を作ろう。

参考リンク

  1. Coding Challenge #28: Metaballs
    ProcessingをYoutubeで学ぶ内容のうちの一つ。
    今回のメタボールの内容を書くのに参考にしました。
    N値化は課題なのでしてほしいですが、グラデーションのままでもきれいだと思います。

  2. Marching Squaresアルゴリズム
    3次元のメタボールならMarching cubesというのが有名ですが、その2次元版になります。
    わざわざ等高線を作成するのはひと手間かかります。

  3. 本学出身の高山先生の作品
    ここにある作品のいくつかはメタボールを使って作っているようです。
    みなさんも工夫してきれいなアニメーションを作ってみましょう!?