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?

ProcessingのOpenCV(Computer Vision)

Last updated at Posted at 2024-11-11

から続きます。

ライブラリの準備

スケッチ>ライブラリをインポート>の中に、
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

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

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
より

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

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();
配列となっているのは、顔を複数認識した場合のためである。
顔が1つであれば、配列の大きさは1、顔が3つであれば、配列の大きさは3となる。
顔の数(配列の大きさ)は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


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

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

目の検出

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;

目の検出精度を高める(誤認識を減らす)

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) {// 顔検出が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;
}

顔の領域だけ色を反転させるプログラム。

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まで

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

モザイクフィルタ

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

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

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

顔の入れ替え

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

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

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

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

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)など。

咀嚼カウント

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座標の保持、1フレーム前と現在
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();
  //顔1つのみ対応
  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座標の傾きについて考える
        //傾きが変われば、次式は負になる
        //一回の咀嚼で+2カウントされるはず?
        int t = (prevMouthY-nowMouthY)*(nowMouthY-mouths[i].y);
        if (t<0) {
          //直前の咀嚼タイミング(符号反転した時間)と、今を比較して、2フレームor3フレームor4フレームの差だったら、咀嚼回数としてカウントする
          if((frameCount - invTime == 2)||(frameCount - invTime == 3)||(frameCount - invTime == 4)){
            mogumoguCount++;
          }
          //30回の咀嚼で0へ
          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;
  }
}

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?