JavaでOpenCV (6)-画像データのやりとり(修正)-

当然というか何というか、画像データのやりとりはbyte[]型でやったほうが早かった

以下変更部分のみ

・Javaコード

ネイティブメソッドの引数をbyte[]に
public native int[] faceDetect(byte[] image);

int列を取得するメソッドをbyte列を取得するメソッドに変更
byte[] b = getImageBytesForCV(buffimg);

getImageBytesForCVの実装
intをbyteにキャストしただけ
public static byte[] getImageBytesForCV(BufferedImage image) throws IOException {
 int[] pixel = new int[image.getWidth()*image.getHeight()];
 byte[] imagedata = new int[image.getWidth()*image.getHeight()*3];
 DataBufferInt buffer = (DataBufferInt)(image.getRaster().getDataBuffer());
 //pixel情報取得
 int j =0;
 for(int i = 0; i < image.getWidth()*image.getHeight(); i++){
  pixel[i] = buffer.getElem(i);
  imagedata[j] = (byte)(pixel[i]&0x000000FF); //b
  imagedata[j+1] = (byte)(pixel[i]&0x0000FF00)>>8; //g
  imagedata[j+2] = (byte)(pixel[i]&0x00FF0000)>>16; //r
  j+=3;
 }
 return imagedata;
}


・ネイティブコードの画像取得部分
jint *array = env->GetIntArrayElements(imagebyte, 0);
//ポインタを付け替えるだけ
img->imageData = array;

コードの最後、returnの前に配列の開放を行う
env->ReleaseIntArrayElements(imagebyte,array,0);

以上

今回はネイティブ側で画像加工を行わないため、単純にポインタの付け替えで済ませたが
ネイティブ側で加工し、加工した画像をJavaに返すといった処理をしたい場合はmemcpy()を使ったほうが良いと思う

JavaでOpenCV (5)-顔認識プログラムの作成-

先の4つの記事を踏まえて、Java+JNI+OpenCVで顔認識プログラムを作っていく

大まかなプログラムの流れは以下
・Javaを用いて画像取得(ファイルから又はカメラから)
・得た画像を変換し、JNIからOpenCVを呼び出す
・OpenCVによって画像認識
・認識した画像の座標をJavaに返す

まず、Java側のプログラムから示していく
 面倒なので直書き

public class JavaCode {
  Player player;
  BufferedImage buffimg = null;

  static {//JNI用ライブラリ
    System.loadLibrary("FaceDetection");
}
  //ネイティブメソッドの宣言を行う
  public native int initdll(int width, int height);
  public native int[] faceDetect(int[] image);


  public JavaCode(){
    buffimg = new BufferedImage(320,240,BufferedImage.TYPE_INT_RGB);
    if(initdll(320,240)==-1){//ネイティブコード側の初期化
      System.exit(1);
    }
  }

  public static void main(String[] args) {
new JavaCode();
    grab_proc();
  }


  void grab_proc(){//カメラからのキャプチャ、画像処理
    try {
      player = Manager.createRealizedPlayer(new MediaLocator("vfw://0"));
    } catch(Exception e){
      System.exit(1);
    }
    FrameGrabbingControl frameGrabber
=(FrameGrabbingControl)player.getControl("javax.media.control.FrameGrabbingControl");
    Buffer buf = frameGrabber.grabFrame();
    BufferToImage b2i = new BufferToImage((VideoFormat) buf.getFormat());
    buffimg = (BufferedImage)b2i.createImage(buf);
//ここまででBufferedImageの取得が終わった

    if(buffimg != null){
      try{
        //画像のピクセル配列の変換
        int[] b = getImageIntsForCV(buffimg);
        //顔認識用ネイティブコードの呼び出し
        int[] facepoint = faceDetect(b);
        //facepointに顔認識の結果が入る
      }catch ( Exception e ){
        System.out.println(e.toString());
      }
    }


    public static int[] getImageIntsForCV(BufferedImage image) throws IOException {
      int[] pixel = new int[image.getWidth()*image.getHeight()];
      int[] imagedata = new int[image.getWidth()*image.getHeight()*3];
      WritableRaster ras = image.getRaster();
      DataBufferInt buffer = (DataBufferInt)(ras.getDataBuffer());
      //pixel情報取得
      int j =0;
      for(int i = 0; i < image.getWidth()*image.getHeight(); i++){
        pixel[i] = buffer.getElem(i);
        imagedata[j] = (pixel[i]&0x000000FF); //b
        imagedata[j+1] = (pixel[i]&0x0000FF00)>>8; //g
        imagedata[j+2] = (pixel[i]&0x00FF0000)>>16; //r
        j+=3;
      }
      return imagedata;
    }
}



以上

BufferedImageの取得までは少し調べればすぐできる
取得したBufferdImageをどうやってネイティブコードで扱うかが肝

なお、出力部分は今回は省略したのでこのコードをコピペしても何も起こらない
とりあえずfacepointの値を出力すれば確認可
# そもそも動かないかも。
# 元コードの関係ない部分を省いて載せたが、必要ある部分も削っちゃたかも。未確認

# 色々なサイトを参考にしました。本当に感謝
# この書き方はねーよwwwとか、もっと良い方法あるよとかありましたら是非教えて下さいお願いします



ちなみに、このコードから生成されるヘッダは以下のようになる

/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class JavaCode */

#ifndef _Included_JavaCode
#define _Included_JavaCode
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JavaCode
* Method: initdll
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_JavaCode_initdll
(JNIEnv *, jobject, jint, jint);

/*
* Class: JavaCode
* Method: faceDetect
* Signature: ([I)[I
*/
JNIEXPORT jintArray JNICALL Java_JavaCode_faceDetect
(JNIEnv *, jobject, jintArray);

#ifdef __cplusplus
}
#endif
#endif





最後にネイティブコード
これはほとんどOpenCVのサンプルまんま

static CvMemStorage* storage = 0;
static CvHaarClassifierCascade* cascade = 0;
IplImage* img;

const char* cascade_name = "C:/OpenCV/data/haarcascades/haarcascade_frontalface_alt2.xml";

//初期化メソッド
JNIEXPORT jint JNICALL Java_UDPCameraClient_initdll(JNIEnv *env, jobject jo, jint width, jint height)
{
  //カスケードとIplImageの初期化を行う
  cascade = (CvHaarClassifierCascade*)cvLoad( cascade_name, 0, 0, 0 );

  if( !cascade )
  {
    return -1;
  }
  storage = cvCreateMemStorage(0);
  img = cvCreateImage(cvSize(width, height), IPL_DEPTH_8U, 3);
  return 0;
}

//顔認識を行うメソッド
JNIEXPORT jintArray JNICALL Java_UDPCameraClient_faceDetect (JNIEnv *env, jobject jo, jintArray imagebyte)
{
  if (imagebyte == NULL){
    return NULL;
  }
//------------ここから画像コピー開始
  jint *array = env->GetIntArrayElements(imagebyte, 0);

  for(int k=0; k< img->width * img->height *3; k++){
    img->imageData[k] = array[k];
  }
//配列の解放
  env->ReleaseIntArrayElements(imagebyte,array,0);
  //-------------ここまで画像コピー処理


  double scale = 1.3;
  IplImage* gray = cvCreateImage( cvSize(img->width,img->height), 8, 1 );
  IplImage* small_img
= cvCreateImage( cvSize( cvRound (img->width/scale), cvRound (img->height/scale)),8, 1 );
  int i;
  int radius = 0;
  CvPoint center;
  center.x = -1;
  center.y = -1;

  cvCvtColor( img, gray, CV_BGR2GRAY );
  cvResize( gray, small_img, CV_INTER_LINEAR );
  cvEqualizeHist( small_img, small_img );
  cvClearMemStorage( storage );

  if( cascade )
  {
    //顔検出
    CvSeq* faces = cvHaarDetectObjects( small_img, cascade, storage,1.1, 2, 0, cvSize(30, 30) );
    for( i = 0; i < (faces ? faces->total : 0); i++ )
    {
      CvRect* r = (CvRect*)cvGetSeqElem( faces, i );
      //半径が一番大きい顔を残す
      if(cvRound((r->width + r->height)*0.25*scale) >= radius){
         center.x = cvRound((r->x + r->width*0.5)*scale);
         center.y = cvRound((r->y + r->height*0.5)*scale);
         radius = cvRound((r->width + r->height)*0.25*scale);
      }
    }
  }
  jintArray result = env->NewIntArray( 5 );//x,y,radの順

  int tmp[5];
  tmp[0] = (jint)center.x;
  tmp[1] = (jint)center.y;
  tmp[2] = (jint)radius;
  env->SetIntArrayRegion(result ,0, 4, (const long*)tmp);

  cvReleaseImage( &gray );
  cvReleaseImage( &small_img );

  return result;
}




以上でJava+JNI+OpenCVで顔認識ができたはず
画像ピクセルのコピーがネックになっているようで、速度はかなり遅い
C2D2.5GHz MEM2Gの環境で、ネイティブ部分だけで80msほど掛るようだ


今回は時間の制約上、とりあえず動けばいいや的なノリで作った

なので、改善点やおかしな点があれば教えてほしいです。切に

JavaでOpenCV (4)-画像データのやりとり-

今回一番苦労した部分
Javaで取得した画像データをJNIを通してOpenCVに送る手法に関して

Javaで用いられる一般的な画像データの形式はBufferedImage
これをOpenCVの画像形式であるIplImageに変換する

JAVA側の処理から解説していく
# あまり詳しく調べてないので間違っているかも
BufferedImageのピクセルデータは以下のコードで取得できる

int[] pixel = new int[image.getWidth()*image.getHeight()];
BufferedImage image;
WritableRaster ras = image.getRaster();
DataBufferInt buffer = (DataBufferInt)(ras.getDataBuffer());
for(int i = 0; i < image.getWidth()*image.getHeight(); i++){
  pixel[i] = buffer.getElem(i);
}

このpixelにはRGBの値が合成(?)されて格納されている
pixel[0] = ピクセル1のRGBの値
pixel[1] = ピクセル2のRGBの値
こんな感じ

対してIplImageのピクセルデータは、char*型で以下のように格納される
IplImage* img;
img->imageData[0] = ピクセル1のBの値
img->imageData[1] = ピクセル1のGの値
img->imageData[2] = ピクセル1のRの値
img->imageData[3] = ピクセル2のBの値
img->imageData[4] = ピクセル2のGの値
img->imageData[5] = ピクセル2のRの値


そのため、まずはBufferedImageのピクセルデータをRGBそれぞれに分解し、BGRの順に並び変えた新しい配列を作る必要がある
http://naka.sfc.keio.ac.jp/~satoru/mt/archives/2006/10/bufferedimage.php
この辺を参考にしつつ、新しい配列を作る
配列のサイズは[画像の幅*画像の高さ*3]になるはず

上のサイトではbyte[]型で取得しているが、自分は今回int[]型で取得した
# byte[]型の方が良さそうな気がする、試さないと


こうして作成したint[]型の配列をネイティブ側に渡す


次にネイティブ側の処理について
Javaから受け取ったint[]型の配列を
img->imageData にコピーする

今回はfor文でぶん回し、1要素づつコピーする方法をとった
以下のコードで実装できる
なお、imagebyteはJavaから受け取る引数で、jintArray型である

jint *array = env->GetIntArrayElements(imagebyte, 0);
for(int k=0; k< img->width * img->height *3; k++){
  img->imageData[k] = array[k];//値のコピー
}
//使い終わった配列は解放する
env->ReleaseIntArrayElements(imagebyte,array,0);


以上でJavaで取得した画像をネイティブ側に送ることができた
# 1要素づつのコピーは、正直あまり良くない手法だと思う
# ポインタで直接指せれば最高
# memcpyを使ってみたがうまくいかなかった
# byte[]型で送ればポインタで上手くできるのだろうか


暇があれば、次で顔認識のサンプルコードを紹介する

JavaでOpenCV (3)-OpenCVコードのコンパイル-

前回の記事で、とりあえずJNIの超基本的な使い方はわかった

次はOpenCVのコードからJNI用の.dllを作成するためのコンパイル手法について
実はここで結構ハマったりしてた

最初の手順は前回と同じ
ネイティブコードを含んだJavaコードを作成し、
javac
javah -jni
でヘッダーファイル生成

次にOpenCVを使ったネイティブコードopentest.cppを書く


そしてコンパイルの手順

まずはbcc32.cfgのインクルードパスに
OpenCV\cv\include
OpenCV\cvaux\include
OpenCV\cxcore\include
OpenCV\otherlibs\highgui
以上4つのディレクトリを追加

次にOpenCV\binに移動し、
implibコマンドで各種dllをlibに変換する
変換が必要なdllは
cv100.dll → cv.lib
cxcore100.dll → cxcore.dll
highgui100.dll → highgui.lib
の3つ
#実はOpenCV\lib\にも各種libファイルがあるのだが、何か形式が違うらしく、bcc32でコンパイルできない
#詳しくはぐぐれば出てくると思う

最後にコマンドラインから、先ほど作成したlibファイルを含めてコンパイルする
bcc32 -tWD opentest.cpp C:\OpenCV\bin\cxcore.lib C:\OpenCV\bin\highgui.lib C:\OpenCV\bin\cv.lib
これでdllが生成される

つづく

JavaでOpenCV (2)-JNIの基礎-

とりあえず自分の開発環境は以下
OS:WindowsXP SP2
CPU:Core2Duo T9300(2.5GHz)
MEM:2GB


まずはJNIとOpenCVのインストール
このへんはググればいくらでも出てくるから省略

次にとりあえずJNIを使ってHelloWorldを書いてみた
参考にしたサイトは以下
http://www.northvan.jp/wiki/index.php?JNI

基本的にサイトに従ってやればうまくいくが、Windows版gccを使うと上手くいかないようだ
Windowsでやる場合はコンパイラをbcc32にしたほうが良い

原因はよくわからん、とりあえずbcc32を使っておけば間違いない

ネイティブコード、JAVAコードを同じディレクトリに置き
コマンドラインから
javac、javah を行う
これは大抵うまくいく
*コマンドエラーが出たら環境変数Pathにjavahのあるディレクトリが追加されているか確認

ヘッダーが完成したら、
bcc32 -tWD HelloWorldJNI.c
と打てば.dllができるはず

この時、bcc32.cfgにインクルードディレクトリとしてjni.iniとjni_md.hがあるディレクトリを追加しておく必要がある
jni.iniとjni_md.hは自分の環境では
C:\Program Files\Java\jdk1.6.0_06\include
にあった。

#bcc32はパスにスペースが入ると正しく認識しなくなるかもしれない
#自分の場合は面倒だったので、jni.iniとjni_md.iniを他の場所にコピーして使った

つづく

JavaでOpenCV (1)-JAVAで顔認識がしたい-

最近、Javaで顔認識を行う必要が出てきた

Javaで顔認識するにはどうするか
ぐぐって一番上に出てきたサイトがこれである
http://www.nilab.info/zurazure2/000603.html

はっきり言って使い物にならない

C言語であれば、OpenCVを使った顔認識が有名だ
実際にOpenCVのサンプルを触ってみたところ、かなりの精度、速度で顔の認識ができた


そこでOpenCVをJAVAから呼び出して使うことにする
JNI(Java Native Interface)を使えばできそうだ


つづく

#JNI+OpenCVの備忘録
#自分が詰まった部分の補足的な記事を書いていこうかと
#基本的に殴り書き
Comments
Archives
Profile
kani
現在M1
  • ライブドアブログ