Processingを利用して魚群再現をする(1)からの続きです。
Processingはカメラを利用することによって、鑑賞者の動作によりイメージを変化させていくインタラクティブデザインを作ることが可能になっています。
#webカメラの利用
せっかくProcessingを利用しているので、カメラを利用してインタラクティブデザインとして発展させていきます。人によってはカメラの画像を利用して、オプティカルフローの実装を行っていますが、それはまたの機会にやります。
Processingでは画像イメージをPImage型として利用します。
PImage型の詳細なテキストはリファレンス に載っています。
PImageの配列の値を利用して、画像に対して魚の動きが変わっていくように実装していきます。
##Webカメラの起動
それではWebカメラの起動から行ってきます。
今回は画像処理ライブラリのOpenCVを利用しますので、同時にOpenCVのimportも行ってください。
プログラムの先頭に以下の構文を挿入します。
import gab.opencv.*;
import processing.video.*;
Capture cam;
OpenCV opencv;
PImage nr, th;
以下の関数を定義します。
動作としてはカメラが画像イメージを読み取った場合に、読み込みを行います。
void captureEvent(Capture cam) {
cam.read();
}
また、前回利用したプログラム本体を以下のように書き変えます。
void setup() {
cam=new Capture(this, Width, Height);
opencv=new OpenCV(this, Width, Height);
cam.start();
background(0, 0, 0);
fill(255, 255, 255);
noStroke();
for (int i=0; i<FishNum; i++) {
fishes[i]=new Fish(random(30, Width-30), random(30, Height-30), random(3), random(3), 0, 0, 5);
}
//場の設定
for (int i=0; i<Width; i++) {
Field[i]=new int[Height];
for (int j=0; j<Height; j++) {
if (i<30 || Width-30<i) {
Field[i][j]=1;
} else if (j<30 || Height<j) {
Field[i][j]=1;
} else {
Field[i][j]=0;
}
}
}
setField(350, 400, 250, 200);
}
void draw() {
background(0, 0, 0);
fill(0, 0, 0);
//カメライメージの読み込み
opencv.loadImage(cam);
//カメライメージの取得(1)
nr=opencv.getOutput();
//画面のモノクロ化
opencv.gray();
//画面の二値化
opencv.threshold(140);
//カメライメージの取得(2)
th=opencv.getOutput();
image(th,0,0)
//Field
fill(255, 0, 0);
rect(350, 400, 250, 200);
rect(0, 0, 30, Height);
rect(0, 0, Width, 30);
rect(Width-30, 0, 30, Height);
rect(0, Height-30, Width, 30);
fill(255, 255, 255);
for (int i=0; i<FishNum; i++) {
fishes[i].ShowFish();
}
for (int i=0; i<FishNum; i++) {
fishes[i].update(fishes,th);
fishes[i].RandomActive();
}
//フレームごとの画像を保存
//saveFrame("frames/######.png");
}
##画像の二値化
以上に示したdraw関数内部で画像の二値化を行っています。
画像イメージとは基本的に1ピクセル(画像を処理する最小のデータ範囲)ごとに数bitのデータをモノクロであれば1種類、カラーであれば3種類持っています(データの種類によっては透過度を示すα値もあります)。
以下の解説では1ピクセルに8bitのデータを持つ画像データを考えます。8bitのピクセルデータの場合、持つ値の範囲は0から255までの範囲になります。
画像処理の世界では、モノクロ画像についてある一定の閾値を持つピクセルの値を255にし、それ以下のデータを0にする処理を行うことによって処理をしやすくすることがあります。これを二値化と呼びます。
以下の図では、左上が元の画像であり、右上がグレースケール(白黒)画像、左下、右下がそれぞれ閾値100、200で二値化した画像になっています。
Rule0に画像の二値化により255(または、0)を持つピクセルを避けるようにプログラムするのが今回の目的になります。
##PImageのカラーコード
PImageのカラーコードは基本的にRGBの3種類と透過度を表すα値から構成されています。
試しにPImageのピクセルのカラーコードを表示してみます。
print(th.pixels[j*Width+i]);
>> -1
//color(Red,Green,Blue)
print(color(255,255,255));
>> -1
上は二値化した画像thのあるピクセルのカラーコード(白)を表示しており、下が白を明示的に指定したときのカラーコードになっています。どちらも値が-1になっていることがわかります。
白のカラーコードは#FFFFFFFFの32bitのデータであり、内訳はα値(0~255:8bit)、Red(0~255:8bit)、Green(0~255:8bit)、Blue(0~255:8bit)となっています。
したがって、二の補数を考慮した場合、白のカラーコードは0xFFFFFFFFであることから、color(255,255,255)=-1が成り立つことがわかります。
余談ですが、colorMode関数を利用すればHSB(Hue:色相,Saturation:彩度,Brightness:明度)色空間で色を指定することも可能になります。
##Rule0の背景に対する変更
上のプログラムではカメラの画像を二値化したPImage型thを利用して、白のピクセルを魚オブジェクトが避けるように変更します。
private PVector Rule0(int[][] Field, PImage th, float radius) {
PVector direction=new PVector(0, 0);
for (int i=int(pos.x-radius); i<int(pos.x+radius); i++) {
for (int j=int(pos.y-radius); j<int(pos.y+radius); j++) {
//半径範囲内である場合
if (sqrt(pow(pos.x-i, 2)+pow(pos.y-j, 2))<radius) {
//境界範囲外の場合
if ((i<0)||(Width<=i) || (j<0 || Height<=j)) {
PVector diff=new PVector(pos.x-i, pos.y-j);
direction=PVector.add(direction, PVector.div(diff, pow(diff.mag(), 2)));
}
//壁の存在を感知した場合
else if (Field[i][j]==1) {
PVector diff=new PVector(pos.x-i, pos.y-j);
direction=PVector.add(direction, PVector.div(diff, pow(diff.mag(), 2)));
}
//背景の白ピクセルの感知
else if (th.pixels[j*Width+i]==color(255, 255, 255)) {
PVector diff=new PVector(pos.x-i, pos.y-j);
direction=PVector.add(direction, PVector.div(diff, pow(diff.mag(), 2)));
}
}
}
}
return direction;
}
コマ送りのような動作になっているのは、プログラムが重いため1フレームごとの間隔があいてしまっているためです。
また、気づかれた方もいるかもしれませんが、背景が動くことによって魚オブジェクトが赤色の範囲を超え、画面外に逃げてしまっていることがわかります。本実装において、完全に画面外に出て行ってしまったオブジェクトはもう戻ってくることはできません。背景は静物を利用する予定なので、問題はないのですが、解決策があれば知りたいです。
##追記:クロージング処理について
上記の動画を見てもらうとわかる通り、白の縁がギザギザになっていることがわかります。
本来であれば、このような場合には画像の膨張、収縮処理を使って、クロージング処理を行います(クロージング処理はノイズの処理などに用います)。
但し、処理にかかる時間はキャンバスの一片の長さをN、魚オブジェクトの数をnとした時、
WithClosing-O(N^2) NoClosing-O(n)
となり、処理にかかる時間が増大するため、動画がカクカクします。
下に、例として2回膨張後、2回収縮したクロージング処理を行った場合の動画と行わない場合の動画を載せます。左上にはフレームレート(fps)を載せています。
クロージング処理を行うことによって、大体3倍程度処理速度が悪化していると言えます。
そのため、クロージング処理をこれからは行いませんが、
処理内容としては以下に示す通りです。
PImage Erosion(PImage orig_img) {
PImage img;
img=orig_img.get();
for (int i=0; i<Height; i++) {
for (int j=0; j<Width; j++) {
for (int x=-1; x<2; x++) {
for (int y=-1; y<2; y++) {
if ((i+y)<0 || Height<=(i+y)) {
continue;
} else if (((j%Width)+x)<0 || Width<=(j%Width)+x) {
continue;
} else if (orig_img.pixels[(i+y)*Width+(j+x)]==color(255, 255, 255)) {
img.pixels[i*Width+j]=color(255, 255, 255);
}
}
}
}
}
img.updatePixels();
return img;
}
PImage Shrink(PImage orig_img) {
PImage img;
img=orig_img.get();
for (int i=0; i<Height; i++) {
for (int j=0; j<Width; j++) {
for (int x=-1; x<2; x++) {
for (int y=-1; y<2; y++) {
if ((i+y)<0 || Height<=(i+y)) {
continue;
} else if (((j%Width)+x)<0 || Width<=(j%Width)+x) {
continue;
} else if (orig_img.pixels[(i+y)*Width+(j+x)]==color(0, 0, 0)) {
img.pixels[i*Width+j]=color(0, 0, 0);
}
}
}
}
}
img.updatePixels();
return img;
}
PImage Closing2(PImage orig_img) {
PImage img;
img=orig_img.get();
img=Erosion(Erosion(img));
img=Shrink(Shrink(img));
return img;
}
#これからの実装について
とりあえずカメラを使用した処理が完成したので、魚群のアニメーションができるか試してみたいと考えています。
また、さらに魚群らしく見えるパラメータの調整と背景画像の選定によりリアルな魚群を目指していきます。
- 魚群のアニメーション作成
- パラメータの調整
- 動作速度の向上
(1)の記事についてはこちら。
(3)の記事についてはこちら(多忙のため完成時期未定)。
#注意
開発を進めながら投稿しているため、特定時点のプログラムを再現したものを掲載しています。そのため、そのままコピペしても動作しない可能性があります。その場合、都度修正して実行してみてください。