画像処理１

# Processing 3.5.3
Mac環境でProcessingからカメラアクセスをする場合、結構条件が厳しい。確実に起動できる環境Processing 3.5.3を起動する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/6c8c16a7-0423-739a-1c8a-bda34932bb8a.png)
黒いアイコンが3.5.3
白いアイコンが4.x.x系

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

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

次の作業を行って、カメラを使うプログラムを動かす準備をする。 
スケッチ　＞　ライブラリをインポート　＞　ライブラリを追加 
検索ボックスにvideoと入力 
Video|Gstreamer ~を選択し、Install
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/5b9e857a-db6d-8ba4-9f7e-15c5eb83477d.png)

# WEBカメラのキャプチャテスト
iMacについているカメラから映像を取得する。 
Caputure型のcamという変数をつくり、解像度を指定し、カメラを認識させる。
※カメラがみつからない場合などのエラー処理を入れるべきだが省略。
```java
cam = new Capture(this,640,480); 
```

解像度は、カメラの種類によって指定できる数値がおよそ決まっている。 

16:9なら(1920,1080)
　　　　(1280,720)
　　　　(960,540)

4:3なら(800,600)
　　　　(640,480)
　　　　(320,240)

解像度が高いほど、処理は重くなる。

camは画像と同じように扱えるので、以下のように、拡大や縮小して使用することが可能である。 
```java:camの拡大縮小表示
image(cam, 0, 0,width, height); 
```

```java: 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は現在スキャンしている縦位置で、１ずつ増加し、480になると、%演算子を用いて0になるようにしている。

```java: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();
}
```
上のプログラムでは、１フレームにつき、１行の描画を行っているので、60frame/secで動いている場合、1秒で60行、最下行まで更新するのに８秒かかる。

そこで、１回に、複数行を更新する例を以下に示す。
```java
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
わかりやすく、行を分けて書いた。

```java
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と、右に２つずれる。結果、赤緑になる。さらに、
0x00FFFF00>>8とすると
0x0000FFFFと、右に２つずれる。結果、緑青になる。
これをシフト演算という。
空いた部分は、00で埋められる。

次に、&演算子はどういう意味か。
0x12345678
&	0x00FF00FFこれを計算すると
0x00340078となる 
つまり、欲しい部分だけ残して、後を消すことが出来る。
赤成分だけほしいといった時に、使える。

まとめると、以下のプログラムは、
「色情報を右にずらして、赤情報を右端に持ってくる」「右端の情報だけ取り出す」
「色情報を右にずらして、緑情報を右端に持ってくる」「右端の情報だけ取り出す」
「右端の情報だけ取り出す」
```
float r = c >> 16 & 0xFF;
float g = c >>  8 & 0xFF;
float b = c       & 0xFF;
```
以上。 

ここで、「>> 1」とすると、1ビットだけ右にずれる。これは2で割ることと、同値である。
コンピュータは2進数で計算しているので、割り算の中でも、２，４，８などの数字の掛け算、割り算は、ビット演算に置き換えることができる。ビット演算は掛け算割り算に比べて、はるかに速い。そして割り算は特に遅い。割り算は少なくとも、掛け算に置き換えた方が効率がいい。ただしビット演算は読みにくい。


# RGB成分の取り出し
RGB成分のを分けることを考える。各ピクセルが、赤緑青で構成されていることを理解すること。

```java
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 

```java
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に変換しなおして、色を決定する。 
```java
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(); 
}
```

```java
float U = 320-mouseX; 
float V = 240-mouseY; 
```
このように書き換えることで、UVの値と結果を確認できる。 
およそ色相環になっていることがわかる。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/27856a01-3e0e-82d4-7cee-cbf63fba8859.png)

# 二値化

グレースケール化したデータをしきい値で白と黒に分ける。 
ここでは、中央値の128をもとに、if文で０と255に分けている。 
128をmouseXと置き換えれば、自由にしきい値を変更できる。 
```java
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倍すると、結果として、１の位を切り捨てできる。 
例えば、(x, y) = (37, 54)という座標は、(30, 50)になる。 
 
※次式でもいいかもしれない 
x-x%10 
y-y%10 

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

# 画像を●で置き換える
カメラ画像を表示せずに、カメラ画像から色を取得し、その色で●を描く
```java
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(); 
} 
```

# 画像の色でランダムな線を引く
カメラ画像を表示せずに、カメラ画像から色を取得し、その色でランダムな線を描く 

```java
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で行っている点に注意。
```java
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/f37b0373-8318-3b79-a214-1cb973366265.png)

 
 
 
# モノの色をマウスの代わりに②　　　 
先のプログラムで、赤いピクセルがどれであるかは判明した。 
これを改良し、赤いピクセルの中央値（座標）を求める。 
Ifで赤いと判定した際には、その座標値を積算していく。なおxとy別々に積算するため、 
変数sumXとsumYを用意する。 
また、該当するピクセル数を保存するために、sumCountをよういする。 
※0除算を避けるため初期値を1としている。 
 
全てのカメラピクセルについて、積算を終えたのち、 
(suX/sumCount, sumY/sumCount)が、中央値（座標）となるので、適当な矩形を描く。 
全体がちらつくような場合のノイズには弱い。
```java
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/00dc5bac-638d-b2be-1ab3-5741d8128e42.png)

 
 
# 残像と差分（差の絶対値）
Processingにはfilter機能があり、簡単な画像処理は、１行で記述できる。 
例えば、グレースケールに変換するには、下記のようにする。 
他のfilterについては、helpからreferenceを参照するとよい。
```java
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の状態で差分を計算し、二値化した。 
グレースケールでもいいが、見た目がいまいちであった。 

```java
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について、ピクセル操作するので、
```java
  diff.loadPixels(); 
  diff.updatePixels(); 
```
で挟んでいる。 
前に何も書かなloadPixelsとupdatePixelsはウィンドウに表示されているピクセルを操作している。 
 
diffの表示後(iff.updatePixels()の後)、ウィンドウ全体を半透明の黒い矩形で塗りつぶしている。

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