0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像処理1

Last updated at Posted at 2024-10-28

Processing 3.5.3

Mac環境でProcessingからカメラアクセスをする場合、結構条件が厳しい。確実に起動できる環境Processing 3.5.3を起動する。
image.png
黒いアイコンが3.5.3
白いアイコンが4.x.x系

(自分のMacで動かすのならば、ここから入手する。3.5.4ではダメ)
https://github.com/processing/processing/releases

Webカメラを使うためのライブラリ

次の作業を行って、カメラを使うプログラムを動かす準備をする。
スケッチ > ライブラリをインポート > ライブラリを追加
検索ボックスにvideoと入力
Video|Gstreamer ~を選択し、Install
image.png

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は画像と同じように扱えるので、以下のように、拡大や縮小して使用することが可能である。

camの拡大縮小表示
image(cam, 0, 0,width, height); 
WEBカメラ動作のテスト
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になるようにしている。

slitscan
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の値と結果を確認できる。
およそ色相環になっていることがわかる。
image.png

二値化

グレースケール化したデータをしきい値で白と黒に分ける。
ここでは、中央値の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(); 
}

スクリーンショット 2024-10-28 14.37.44.png

モノの色をマウスの代わりに②   

先のプログラムで、赤いピクセルがどれであるかは判明した。
これを改良し、赤いピクセルの中央値(座標)を求める。
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(); 
}

スクリーンショット 2024-10-28 14.36.55.png

残像と差分(差の絶対値)

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(); 
} 
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?