Processing 3.5.3
Mac環境でProcessingからカメラアクセスをする場合、結構条件が厳しい。確実に起動できる環境Processing 3.5.3を起動する。
黒いアイコンが3.5.3
白いアイコンが4.x.x系
(自分のMacで動かすのならば、ここから入手する。3.5.4ではダメ)
https://github.com/processing/processing/releases
Webカメラを使うためのライブラリ
次の作業を行って、カメラを使うプログラムを動かす準備をする。
スケッチ > ライブラリをインポート > ライブラリを追加
検索ボックスにvideoと入力
Video|Gstreamer ~を選択し、Install
WEBカメラのキャプチャテスト
iMacについているカメラから映像を取得する。
Caputure型のcamという変数をつくり、解像度を指定し、カメラを認識させる。
※カメラがみつからない場合などのエラー処理を入れるべきだが省略。
cam = new Capture(this,640,480);
解像度は、カメラの種類によって指定できる数値がおよそ決まっている。
16:9なら(1920,1080)
(1280,720)
(960,540)
4:3なら(800,600)
(640,480)
(320,240)
解像度が高いほど、処理は重くなる。
camは画像と同じように扱えるので、以下のように、拡大や縮小して使用することが可能である。
image(cam, 0, 0,width, height);
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this,640,480);
cam.start();
}
void draw() {
image(cam, 0, 0);
}
void captureEvent(Capture c){
cam.read();
}
SlitScan
Slit scanでは、横一ラインor縦一ラインの色情報を取得し、時間差を与えながら、逐一取得する。
結果、移動する物体では、ズレが生じる。アナログカメラの時代から試みられている技法であり、作品としては岩井俊雄氏の「マシュマロスコープ」が有名である。
以下に、サンプルを示す。scanYは現在スキャンしている縦位置で、1ずつ増加し、480になると、%演算子を用いて0になるようにしている。
import processing.video.*;
Capture cam;
int scanY = 0;
color col;
void setup() {
size(640, 480);
cam = new Capture(this,640,480);
cam.start();
}
void draw() {
for(int x=0; x<640; x++){
col = cam.get(x,scanY);
stroke(col);
point(x, scanY);
}
scanY = (scanY + 1)%480;
}
void captureEvent(Capture c){
cam.read();
}
上のプログラムでは、1フレームにつき、1行の描画を行っているので、60frame/secで動いている場合、1秒で60行、最下行まで更新するのに8秒かかる。
そこで、1回に、複数行を更新する例を以下に示す。
import processing.video.*;
Capture cam;
int scanY = 0;
int scanlines = 1;
color col;
void setup() {
size(640, 480);
cam = new Capture(this,640,480);
cam.start();
}
void draw() {
for(int n=0; n<scanlines; n++){
for(int x=0; x<640; x++){
col = cam.get(x,scanY+n);
stroke(col);
point(x, scanY+n);
}
}
scanY = (scanY + scanlines)%480;
}
void captureEvent(Capture c){
cam.read();
}
get命令
color型の変数cを用意する。cは1ピクセルの色情報を保持している。
get命令はスクリーン上のピクセルの色情報を取得する。
color c;
c = get(x,y);
また、表示されていない画像の色情報は、次のように、取得できる。
img.get(x,y);
ネガポジ反転
カメラ画像を取得し、R,G,Bそれぞれ、反転させる。
式は次の通り
R’=255-R
G’=255-G
B’=255-B
わかりやすく、行を分けて書いた。
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
}
void draw() {
image(cam, 0, 0);
loadPixels();
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
color c = pixels[y*width+x];
float r = red(c);
float g = green(c);
float b = blue(c);
r = 255-r;
g = 255-g;
b = 255-b;
pixels[y*width+x] = color(r,g,b);
}
}
updatePixels();
}
void captureEvent(Capture c){
cam.read();
}
ここで、リファレンスからred()を見てみる。Color型から各RGB情報を取得する命令。
すると、>>とか&とか耳慣れない文字が出てくる。
これをシフト演算ビット演算と呼び、画像処理を高速に行うテクニックである。
0xは16進数を示す、接頭語である。
よって0xFFは255を指す。
色は0xAARRGGBBとなっていて、AAは透明度を指す。FFなら不透明。
0xFFFF0000は不透明の赤を指す。
ここで、
0xFFFF0000>>8とすると
0x00FFFF00と、右に2つずれる。結果、赤緑になる。さらに、
0x00FFFF00>>8とすると
0x0000FFFFと、右に2つずれる。結果、緑青になる。
これをシフト演算という。
空いた部分は、00で埋められる。
次に、&演算子はどういう意味か。
0x12345678
& 0x00FF00FFこれを計算すると
0x00340078となる
つまり、欲しい部分だけ残して、後を消すことが出来る。
赤成分だけほしいといった時に、使える。
まとめると、以下のプログラムは、
「色情報を右にずらして、赤情報を右端に持ってくる」「右端の情報だけ取り出す」
「色情報を右にずらして、緑情報を右端に持ってくる」「右端の情報だけ取り出す」
「右端の情報だけ取り出す」
float r = c >> 16 & 0xFF;
float g = c >> 8 & 0xFF;
float b = c & 0xFF;
以上。
ここで、「>> 1」とすると、1ビットだけ右にずれる。これは2で割ることと、同値である。
コンピュータは2進数で計算しているので、割り算の中でも、2,4,8などの数字の掛け算、割り算は、ビット演算に置き換えることができる。ビット演算は掛け算割り算に比べて、はるかに速い。そして割り算は特に遅い。割り算は少なくとも、掛け算に置き換えた方が効率がいい。ただしビット演算は読みにくい。
RGB成分の取り出し
RGB成分のを分けることを考える。各ピクセルが、赤緑青で構成されていることを理解すること。
import processing.video.*;
Capture cam;
void setup() {
size(1280, 960);
cam = new Capture(this,640,480);
cam.start();
}
void draw() {
image(cam, 0, 0);
loadPixels();
for(int y=0; y<480; y++){
for(int x=0; x<640; x++){
color c = pixels[y*1280+x];
pixels[ y *1280+(x+640)] = color(red(c),0,0);
pixels[(y+480)*1280+ x ] = color(0,green(c),0);
pixels[(y+480)*1280+(x+640)] = color(0,0,blue(c));
}
}
updatePixels();
}
void captureEvent(Capture c){
cam.read();
}
グレースケール化
グレースケール化を考える。
単純に(R+G+B)/3でも成り立つが、TV放送の電波の信号は、白黒情報と色情報に分かれている。
初期のTVは白黒であり、後から色情報を載せた経緯がある。
TV進化や互換性の問題から、このようになっている。
TVの信号をYUVといい、Yが白黒情報を持っており、UVが色情報を持っている。
RGBからの変換式は次の通り。
Y = R * 0.3 + G * 0.59 + B * 0.11
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
}
void draw() {
image(cam, 0, 0);
loadPixels();
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
color c = pixels[y*width+x];
float r = red(c);
float g = green(c);
float b = blue(c);
pixels[y*width+x] = color(r*0.3+g*0.59+b*0.11);
}
}
updatePixels();
}
void captureEvent(Capture c){
cam.read();
}
セピア調
モノクロY(UV)画像にUV情報を付加することで、色を載せることが出来る。
YUVの情報をRGBに変換しなおして、色を決定する。
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
}
void draw() {
image(cam, 0, 0);
loadPixels();
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
color c = pixels[y*width+x];
float r = red(c);
float g = green(c);
float b = blue(c);
float Y = r*0.3+g*0.59+b*0.11;
float U = -30;
float V = 15;
r = Y + 1.40*V;
g = Y - 0.34*U - 0.71*V;
b = Y + 1.78*U ;
pixels[y*width+x] = color(r,g,b);
}
}
updatePixels();
}
void captureEvent(Capture c){
cam.read();
}
float U = 320-mouseX;
float V = 240-mouseY;
このように書き換えることで、UVの値と結果を確認できる。
およそ色相環になっていることがわかる。
二値化
グレースケール化したデータをしきい値で白と黒に分ける。
ここでは、中央値の128をもとに、if文で0と255に分けている。
128をmouseXと置き換えれば、自由にしきい値を変更できる。
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
}
void draw() {
image(cam, 0, 0);
loadPixels();
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
color c = pixels[y*width+x];
float r = red(c);
float g = green(c);
float b = blue(c);
float BH = r*0.3+g*0.59+b*0.11;
if(BH<128){
BH=0;
}else{
BH=255;
}
pixels[y*width+x] = color(BH);
}
}
updatePixels();
}
void captureEvent(Capture c){
cam.read();
}
モザイク
カメラの色情報を取得後、適当な変換式によって、色情報を参照する座標の変換を行う。
x座標を整数10で割ると、java言語のルールから、答えも整数になる。
答えを10倍すると、結果として、1の位を切り捨てできる。
例えば、(x, y) = (37, 54)という座標は、(30, 50)になる。
※次式でもいいかもしれない
x-x%10
y-y%10
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
}
void draw() {
image(cam, 0, 0);
loadPixels();
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
color c = pixels[(y/10*10)*width+(x/10*10)];
pixels[y*width+x] = c;
}
}
updatePixels();
}
void captureEvent(Capture c){
cam.read();
}
画像を●で置き換える
カメラ画像を表示せずに、カメラ画像から色を取得し、その色で●を描く
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this, width, height);
cam.start();
}
void draw() {
//image(cam, 0, 0);
cam.loadPixels();
for (int y=0; y<height; y+=20) {
for (int x=0; x<width; x+=20) {
color c = cam.pixels[y*width+x];
noStroke();
fill(c);
ellipse(x, y, 10, 10);
}
}
cam.updatePixels();
}
void captureEvent(Capture c) {
cam.read();
}
画像の色でランダムな線を引く
カメラ画像を表示せずに、カメラ画像から色を取得し、その色でランダムな線を描く
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this, width, height);
cam.start();
}
void draw() {
background(0);
cam.loadPixels();
for (int y=0; y<height; y+=5) {
for (int x=0; x<width; x+=5) {
color c = cam.pixels[y*width+x];
stroke(c,128);
line(x,y,x+random(-10,10),y+random(-10,10));
}
}
cam.updatePixels();
}
void captureEvent(Capture c) {
cam.read();
}
モノの色をマウスの代わりに①
色のはっきりしたボールなどの位置座標を、マウスのように取得することを考える。
RGBの色空間で判定するよりも、HSBの色空間で判定する方が楽である。
RGB色情報から、色相、彩度、明度をそれぞれ、取得する命令がprocessingにはある。
ここでは、それぞれを、0~255で指定するように設定し、明るめで、彩度が高い「赤」をif条件文の組み合わせで指定した。
|| は or条件で、&& は and条件である。
赤色のみ色相環の0の前後であるため、範囲指定に注意が必要である。
条件に合致するピクセルは「白」、それ以外は「黒」で塗る。
※色指定はHSBで行っている点に注意。
import processing.video.*;
Capture cam;
void setup() {
colorMode(HSB, 360, 100, 100);
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
}
void draw() {
image(cam, 0, 0);
loadPixels();
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
color c = pixels[y*width+x];
float h = hue(c);
float s = saturation(c);
float b = brightness(c);
//if((h>245 || h<10) && s>200 && b>100){
if(h>350 && (s>50 && s<80) && (b>10 && b<90)){
pixels[y*width+x] = color(h,0,255);
}else{
pixels[y*width+x] = color(h,0,0);
}
}
}
updatePixels();
}
void captureEvent(Capture c){
cam.read();
}
モノの色をマウスの代わりに②
先のプログラムで、赤いピクセルがどれであるかは判明した。
これを改良し、赤いピクセルの中央値(座標)を求める。
Ifで赤いと判定した際には、その座標値を積算していく。なおxとy別々に積算するため、
変数sumXとsumYを用意する。
また、該当するピクセル数を保存するために、sumCountをよういする。
※0除算を避けるため初期値を1としている。
全てのカメラピクセルについて、積算を終えたのち、
(suX/sumCount, sumY/sumCount)が、中央値(座標)となるので、適当な矩形を描く。
全体がちらつくような場合のノイズには弱い。
import processing.video.*;
Capture cam;
void setup() {
colorMode(HSB, 360, 100, 100);
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
}
void draw() {
image(cam, 0, 0);
int sumX=0;
int sumY=0;
int sumCount=1;
loadPixels();
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
color c = pixels[y*width+x];
float h = hue(c);
float s = saturation(c);
float b = brightness(c);
//if((h>245 || h<10) && s>200 && b>100){
if(h>350 && (s>50 && s<80) && (b>10 && b<90)){
pixels[y*width+x] = color(h,0,255);
sumX+=x;
sumY+=y;
sumCount++;
}else{
pixels[y*width+x] = color(h,0,0);
}
}
}
updatePixels();
fill(0,255,255);//red
rect(sumX/sumCount, sumY/sumCount, 10,10);
}
void captureEvent(Capture c){
cam.read();
}
残像と差分(差の絶対値)
Processingにはfilter機能があり、簡単な画像処理は、1行で記述できる。
例えば、グレースケールに変換するには、下記のようにする。
他のfilterについては、helpからreferenceを参照するとよい。
import processing.video.*;
Capture cam;
void setup() {
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
}
void draw() {
cam.filter(GRAY);
image(cam, 0, 0);
}
void captureEvent(Capture c){
cam.read();
}
画像の分を計算する。動画において連続的に差分を取り続けると、動いた部分のみが浮き出る。
今回はRGBの状態で差分を計算し、二値化した。
グレースケールでもいいが、見た目がいまいちであった。
import processing.video.*;
Capture cam;
PImage prev; //ひとつ前のカメラ画像の保存用
PImage diff; //差分の保存用
void setup() {
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
prev = createImage(width, height, RGB); //用意
diff = createImage(width, height, RGB); //用意
}
void draw() {
diff = cam.copy(); //camからdiffにデータをコピー
diff.blend(prev, //diffとprevの差分計算し、diffに保存
0,0,width,height,
0,0,width,height,
DIFFERENCE);
diff.filter(THRESHOLD, 0.2); //diffを0.2の閾値で二値化する
image(diff, 0, 0); //diffを表示する
prev = cam.copy(); //次回のため、camをprevに保存しておく
}
void captureEvent(Capture c){
cam.read();
}
差分を表示する部分をいったん消す。
代わりに、差分を上書きの方法を少し変えて描画する。
白い部分は、白で。
黒い部分は、描画しない。(過去の描画が残る)
diffについて、ピクセル操作するので、
diff.loadPixels();
diff.updatePixels();
で挟んでいる。
前に何も書かなloadPixelsとupdatePixelsはウィンドウに表示されているピクセルを操作している。
diffの表示後(iff.updatePixels()の後)、ウィンドウ全体を半透明の黒い矩形で塗りつぶしている。
import processing.video.*;
Capture cam;
PImage prev;
PImage diff;
void setup() {
size(640, 480);
cam = new Capture(this,width,height);
cam.start();
prev = createImage(width, height, RGB);
diff = createImage(width, height, RGB);
}
void draw() {
diff = cam.copy();
diff.blend(prev,
0,0,width,height,
0,0,width,height,
DIFFERENCE);
diff.filter(THRESHOLD, 0.2);
diff.loadPixels();
loadPixels();
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
color c = diff.pixels[y*width+x];
if(c == color(255)){
pixels[y*width+x] = color(255);
}
}
}
updatePixels();
diff.updatePixels();
fill(0,0,0,5); //darker screen
rect(0,0,width,height);
prev = cam.copy();
}
void captureEvent(Capture c){
cam.read();
}