から続きます。
ライブラリの準備
スケッチ>ライブラリをインポート>の中に、
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、
カメラキャリブレーション(樽型ゆがみ補正)
コンピュテーショナルフォトグラフィ、「顔認識」
ライブラリの指定では、
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;
}
}