ProcessingのOpenCV(Computer Vision)

https://qiita.com/hextomino/items/bbd77e1a03b5c208c28d

から続きます。

# ライブラリの準備
スケッチ＞ライブラリをインポート＞の中に、
Video Library for Processing
OpenCV for Processing
がなければ「ライブラリを追加」から、
検索ボックスでvideoもしくはopencvと入力しインストールする

# 動作確認
ファイル＞サンプルを開く
Contributed Libraries＞OpenCV for Processing＞FaceDetectionをひらき、実行できればOK

# コンピュータによる画像認識
OpenCVはintel開発の、コンピューター・ビジョン・ライブラリ。
Computer Visionとは、画像や映像を認識するための分野。

「カメラが眼」なら
「Computer Visionは脳」。

マルチプラットフォーム対応でprocessingからも限定的だが使用可能。

※インテルは2018年9月よりOpenVINOを提供しており、AIによる、より進んだ認識技術が盛り込まれている。例えば、顔認識は斜め顔でもいけるし、顔の向きが推定できるようになった。

参考
http://jellyware.jp/kurage/openvino/c01_overview.html


OpenCVを使うと、以下の処理が可能となる。
フィルター、行列計算、領域分割、オブジェクト追跡、
特徴点抽出、物体認識、機械学習、パノラマ合成、GUI、
カメラキャリブレーション（樽型ゆがみ補正）
コンピュテーショナルフォトグラフィ、「顔認識」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510257/bd5925b4-ee5d-a984-4856-cd619ec0746d.png)

ライブラリの指定では、
OpenCV
Webカメラ
に加え、Rectangle型を使用するために、もう一行追加する。
Rectangle型は、顔の矩形領域を受け取るための型として必要。
```java
import gab.opencv.*;
import processing.video.*;
import java.awt.*;
```

# Rectangle型
Rectangle型では、矩形領域の
左上の座標(x,y)と幅と、高さ(width,height)を一度に受け取る。
受け取ったデータを読み取るには、
```Rectangle r;```
と宣言した場合、
```rect( r.x, r.y, r.width, r.height);```
のように、ドットの後ろに読み込みたいデータの文字を書く。
これは「クラス」の「メンバ変数」にアクセスする方法である。

### Rectangleの定義
Rectangle(int x, int y, int width, int height)
左上隅が (x,y) として指定され、幅と高さが width 引数および height 引数で指定される新しい Rectangle を構築します。
http://e-class.center.yuge.ac.jp/jdk_docs/ja/api/java/awt/Rectangle.html
より

例えば、次のように使う。

```java
import java.awt.*;

void setup() {
  size(640, 480);
}

void draw() {
  Rectangle r = new Rectangle(50,50,200,200);
  rect(r.x, r.y, r.width, r.height);
}
```

# 顔認識のコード　                                     
OPENCVでは機械学習済みの顔認識の学習データが備わっている。
本来機械学習は、多くの顔画像を入力し、学習させる必要があるが、顔に関しては、すでに用意されている。
```opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);```
と指定するだけで済む。

認識した顔領域は次の一行で読み込む。
```Rectangle[] faces = opencv.detect();```
配列となっているのは、顔を複数認識した場合のためである。
顔が１つであれば、配列の大きさは１、顔が３つであれば、配列の大きさは３となる。
顔の数（配列の大きさ）はfaces.lengthで知ることが出来、for文で使う。

「顔」認識以外には以下が用意されている（※使い物にするには、工夫が必要）
OpenCV.CASCADE_FRONTALFACE「顔」

OpenCV.CASCADE_CLOCK
OpenCV.CASCADE_EYE
OpenCV.CASCADE_FRONTALFACE
OpenCV.CASCADE_FULLBODY
OpenCV.CASCADE_LOWERBODY
OpenCV.CASCADE_MOUTH
OpenCV.CASCADE_NOSE
OpenCV.CASCADE_PEDESTRIAN
OpenCV.CASCADE_PEDESTRIANS
OpenCV.CASCADE_PROFILEFACE
OpenCV.CASCADE_RIGHT_EAR
OpenCV.CASCADE_UPPERBODY

 
ベースとなる顔認識のためのプログラム

```java
import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture video;
OpenCV opencv;
Rectangle[] faces;

void setup() {
  size(640, 480);
  video = new Capture(this, width, height);
  opencv = new OpenCV(this, width, height);

  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);

  video.start();
}

void draw() {
  opencv.loadImage(video);
  image(video, 0, 0);

  noFill();
  stroke(0, 255, 0);
  strokeWeight(3);
  Rectangle[] faces = opencv.detect();

  for (int i = 0; i < faces.length; i++) {
    rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height);
    //copy(faces[i].x, faces[i].y, faces[i].width, faces[i].height, 0,0,50,50);
  }
}

void captureEvent(Capture c) {
  c.read();
}
```


# 目の検出
```java
import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture video;
OpenCV opencv;
Rectangle[] eyes;

void setup() {
  size(320, 240);
  video = new Capture(this, 320, 240);
  opencv = new OpenCV(this, 320, 240);

  opencv.loadCascade(OpenCV.CASCADE_EYE);

  video.start();
  eyes = opencv.detect();

}

void draw() {
  opencv.loadImage(video);
  image(video, 0, 0);

  noFill();
  stroke(0, 255, 0);
  strokeWeight(3);
  Rectangle[] eyes = opencv.detect();

  for (int i = 0; i < eyes.length; i++) {
    rect(eyes[i].x, eyes[i].y, eyes[i].width, eyes[i].height);
    rect(eyes[i].x+eyes[i].width/2, eyes[i].y+eyes[i].height/2,5,5);
  }

}

void captureEvent(Capture c) {
  c.read();
}

//import java.awt.Rectangle;
```

# 目の検出精度を高める（誤認識を減らす）
```java
import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture video;
OpenCV opencv;

void setup() {
  size(320, 320);
  video = new Capture(this, 320, 320);
  opencv = new OpenCV(this, 320, 320);

  video.start();

  noFill();
  strokeWeight(3);
}

PVector faceCenter = new PVector(0, 0);//顔の中心座標
Rectangle faceUpL;//顔の向かって左上
Rectangle faceUpR;//顔の向かって右上

void draw() {
  opencv.loadImage(video);
  image(video, 0, 0);

  stroke(255, 0, 0);

  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
  Rectangle[] faces = opencv.detect();
  if (faces.length == 1) {// 顔検出が１つのみ想定
    rect(faces[0].x, faces[0].y, faces[0].width, faces[0].height);
    faceCenter.y = faces[0].x + faces[0].width/2;
    faceCenter.y = faces[0].y + faces[0].height/2;
    //line(0,faceCenter.y,width,faceCenter.y);//確認表示
    faceUpL = new Rectangle(faces[0].x, faces[0].y, faces[0].width/2, faces[0].height/2);
    //rect(faceUpL.x, faceUpL.y, faceUpL.width, faceUpL.height);//確認表示
    faceUpR = new Rectangle(faces[0].x+faces[0].width/2, faces[0].y, faces[0].width/2, faces[0].height/2);
    //rect(faceUpR.x, faceUpR.y, faceUpR.width, faceUpR.height);//確認表示
  }

  stroke(0, 255, 0);

  opencv.loadCascade(OpenCV.CASCADE_EYE);
  Rectangle[] eyes = opencv.detect();
  for (int i = 0; i < eyes.length; i++) {
    //目の中心座標を計算
    PVector eyeL = new PVector(eyes[i].x+eyes[i].width/2, eyes[i].y+eyes[i].height/2);
    PVector eyeR = new PVector(eyes[i].x+eyes[i].width/2, eyes[i].y+eyes[i].height/2);
    if(pointIsInRect(eyeL, faceUpL)){//顔の左上の矩形内なら、左側の目
      rect(eyes[i].x, eyes[i].y, eyes[i].width, eyes[i].height);
    }
    if(pointIsInRect(eyeR, faceUpR)){//顔の右上の矩形内なら、右側の目
      rect(eyes[i].x, eyes[i].y, eyes[i].width, eyes[i].height);
    }
  }
}

void captureEvent(Capture c) {
  c.read();
}

//点が矩形のなかにあるかどうか判定する関数
boolean pointIsInRect(PVector p, Rectangle r){
  if(r.x<p.x && p.x<r.x+r.width){
    if(r.y<p.y && p.y<r.y+r.height){
      return true;
    }
  }
  return false;
}
```


# 顔の領域だけ色を反転させるプログラム。
```java
import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture video;
OpenCV opencv;
Rectangle[] faces;

void setup() {
  size(640, 480);
  video = new Capture(this, width, height);
  opencv = new OpenCV(this, width, height);
  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
  video.start();
}

void draw() {
  opencv.loadImage(video);
  image(video, 0, 0);
  noFill();
  stroke(0, 255, 0);
  strokeWeight(3);
  Rectangle[] faces = opencv.detect();

  loadPixels();
  for (int i = 0; i < faces.length; i++) {
    rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height);
    for(int y=faces[i].y; y<faces[i].y+faces[i].height; y++){
      for(int x=faces[i].x; x<faces[i].x+faces[i].width; x++){
        int 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) {
  c.read();
}
```

drawのforが、追加した部分。
矩形領域部分について、x,yでfor文を回す。
矩形領域は以下となる。

faces[i].xから faces[i].x+faces[i].widthまで
faces[i].yから faces[i].y+faces[i].heightまで

以後は、この投稿の最初のリンクの内容。
ピクセルの色を取得し、ＲＧＢ成分に分ける。
255から引き算し、反転させる。
反転後、ピクセルに書き込む。
ここでは、loadPixelsとupdatePixelsの場所は、大きめにforの外側に配置した。

# モザイクフィルタ

一定領域のピクセルの値を、一定値にする処理。フォトショップのフィルタ参照。

プログラムでは、簡単な計算で実装した。
プログラムの説明の前に、概念をフォトショップの操作で示す。
フォトショップで画像解像度を使う。
まず画像サイズを１／１０に縮小する。
そして、画像サイズを１０倍に拡大する。
その際、引き伸ばすアルゴリズムを選択でき、ニアレストネイバーを選択する。
すると、元のサイズに戻り、解像度の情報が削られる。
これをプログラムでするために、単純な方法がある。
整数１０で割って、１０を掛ける。
整数１０で割るのがミソである。
プログラムでは、整数で割ると、答えが、整数となり、小数点以下は、切り捨てられる。
例えば、５５を１０で割ると、５になる。５を１０倍すると５０になる。
つまり、１の位を切り捨てることが出来る。


```java
import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture video;
OpenCV opencv;
Rectangle[] faces;

void setup() {
  size(640, 480);
  video = new Capture(this, width, height);
  opencv = new OpenCV(this, width, height);

  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);

  video.start();
}

void draw() {
  opencv.loadImage(video);
  image(video, 0, 0);

  Rectangle[] faces = opencv.detect();

  loadPixels();
  for (int i = 0; i < faces.length; i++) {
    rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height);
    for(int y=faces[i].y; y<faces[i].y+faces[i].height; y++){
      for(int x=faces[i].x; x<faces[i].x+faces[i].width; x++){
        int c = pixels[(y/20*20)*width+(x/20*20)];
        pixels[y*width+x] = c;
      }
    }
  }
  updatePixels();
}

void captureEvent(Capture c) {
  c.read();
}
```

# 顔の入れ替え
２人の顔領域を入れ替える。

以下の問題は、かんたんには解決が難しいので省略。
■領域が震える問題
フレーム毎に、認識座標が震えるので、吸収する。（過去フレームのデータの保持必須）

■色が合わない問題
領域の平均色を求めて、補正する

■領域のエッジが目立つ問題
透明度を考慮しつつ描画する。

```java
import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture video;
OpenCV opencv;
Rectangle[] faces;

void setup() {
  size(640, 240);
  video = new Capture(this, 320, 240);
  opencv = new OpenCV(this, 320, 240);

  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);

  video.start();
}

void draw() {
  opencv.loadImage(video);
  image(video, 0, 0);

  Rectangle[] faces = opencv.detect();

  if(faces.length==2){
    rect(faces[0].x, faces[0].y, faces[0].width, faces[0].height);
    rect(faces[1].x, faces[1].y, faces[1].width, faces[1].height);
    copy(faces[1].x+10, faces[1].y+10, faces[1].width-20, faces[1].height-20,
320, 0, faces[1].width-20, faces[1].height-20);
    copy(faces[0].x+10, faces[0].y+10, faces[0].width-20, faces[0].height-20,
         faces[1].x+10, faces[1].y+10, faces[1].width-20, faces[1].height-20);
    copy(320, 0, faces[1].width-20, faces[1].height-20,
faces[0].x+10, faces[0].y+10, faces[0].width-20, faces[0].height-20);
  }
}

void captureEvent(Capture c) {
  c.read();
}
```

# リアルタイムに顔認識し「メガネや帽子」の画像を顔に合わせて合成する。
①photoshop等で、背景を透明にした「PNG画像」を用意し、表示する。
②顔領域(Rectangle)の幅や高さに応じてサイズを自動調整する。

画像の表示サイズ
https://processing.org/reference/image_.html

image(x,y,w,h);
x,y	画像を表示する左上の座標
w,h	画像を表示したい幅と高さ

プログラムと、メガネや帽子をかけた状態のスクショ(⌘ SHIFT 5)など。

# 咀嚼カウント
```java
import gab.opencv.*;
import processing.video.*;
import java.awt.*;

PrintWriter output;//ファイル保存

Capture video;
OpenCV opencv;
Rectangle[] faces;
Rectangle[] eyes;
Rectangle[] mouths;

color _EYE = color(255, 0, 0);
color _FACE = color(0, 255, 0);
color _MOUTH = color(0, 0, 255);
color _OTHER = color(255, 255, 255, 64);

int prevMouthY, nowMouthY;//口のy座標の保持、１フレーム前と現在
int mogumoguCount;//咀嚼回数
int invTime;//符号が反転した時のframeCount値を保存

void setup() {
  size(640, 480);
  output = createWriter("mouthY.csv");
  output.println("frame,y,咀嚼回数");//csvのタイトル行

  video = new Capture(this, width, height);
  opencv = new OpenCV(this, width, height);

  video.start();
}

void draw() {
  opencv.loadImage(video);
  image(video, 0, 0);

  noFill();
  strokeWeight(3);

  //顔を認識
  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
  faces = opencv.detect();
  //顔１つのみ対応
  if (faces.length==1) {
    //顔枠描画
    stroke(_FACE);
    rect(faces[0].x, faces[0].y, faces[0].width, faces[0].height);


    //口の認識
    opencv.loadCascade(OpenCV.CASCADE_MOUTH);
    mouths = opencv.detect();
    for (int i = 0; i < mouths.length; i++) {
      //顔の位置に対して、位置的に口か？
      if (isMouse(mouths[i], faces[0])) {
        stroke(_MOUTH);

        //咀嚼カウント
        //口のy座標の傾きについて考える
        //傾きが変われば、次式は負になる
        //一回の咀嚼で＋２カウントされるはず？
        int t = (prevMouthY-nowMouthY)*(nowMouthY-mouths[i].y);
        if (t<0) {
          //直前の咀嚼タイミング（符号反転した時間）と、今を比較して、２フレームor３フレームor４フレームの差だったら、咀嚼回数としてカウントする
          if((frameCount - invTime == 2)||(frameCount - invTime == 3)||(frameCount - invTime == 4)){
            mogumoguCount++;
          }
          //３０回の咀嚼で０へ
          if(mogumoguCount>30){
            mogumoguCount=0;
          }
          //次回フレームのために、時間を保存
          invTime = frameCount;
        }
        
        //咀嚼回数の表示
        textSize(100);
        text(mogumoguCount, 100, 100);
        
        //口のy座標と咀嚼回数をファイル保存
        output.print(frameCount);
        output.print(",");
        output.print(mouths[i].y);
        output.print(",");
        output.println(mogumoguCount);

        //次回フレームのために、口のy座標保持
        prevMouthY = nowMouthY;
        nowMouthY = mouths[i].y;
        
      } else {
        stroke(_OTHER);
      }
      //口の描画
      rect(mouths[i].x, mouths[i].y, mouths[i].width, mouths[i].height);
    }
  }
}


void keyPressed() {
  output.flush();
  output.close();
  exit();
}


void captureEvent(Capture c) {
  c.read();
}

//////////////////////
//顔の位置に対して、位置的に口か？
boolean isMouse(Rectangle mouth, Rectangle face) {
  //口の幅と顔の幅の比が、一定範囲か？0.25~0.5に設定。要微調整。
  float ratioW = (float)mouth.width/face.width;
  if (0.25<ratioW && ratioW<0.5) {
    //次へ進む
  } else {
    return false;
  }

  //口の縦位置が、顔の何％にあるか。0.8~1.0に設定。要微調整。
  float mouseCenterY = mouth.y+mouth.height/2;
  float ratioH = (mouseCenterY-face.y)/face.height;
  if (0.8<ratioH && ratioH<1.0) {
    return true;
  } else {
    return false;
  }
}

```