LoginSignup
0
0

QPToolkitを用いてカメラ画像上のマーカーの位置を取得し、それらを使ってProcessingで何か描画されるようにする。

Last updated at Posted at 2022-03-02

概要

大学でQPServerを使ってマーカーの位置姿勢計測をする機会があったので、QPToolkitの紹介やそれを用いた私の開発物を簡単にまとめてみました。

目次

QPServerについて
準備した物
マーカーを検出する
Processingで描画する
参考にさせて頂いたもの

QPServerについて

マーカーの登録や検出の認識を全てプログラムで済ますのはとても大変なことですが、QPServerという便利なフレームワークがあります。
これはARToolkitをベースに開発されており、Webカメラを使ったID認識,2次元/3次元の位置姿勢計測だけでなくカメラ画像上のマーカーの面積、回転ベクトル等も計測することができます。しかし、どうやら完全に位置計測に特化してるのでカメラ画像とCG合成機能は無いようです。描画などの処理は別にプログラムを書く必要があります。

詳しい説明や使い方は下記のリンクから参照してください。
↓↓↓

QPToolkit: Webカメラを使った簡単位置計測

準備した物

富士通LIFEBOOK UH08/E3 FMVUU8FUV3
ロジクールC270N HDウェブカメラ
QPServer(QPToolkit)
Processing

マーカーを検出する

まず、マーカーを用意します。今回使用するマーカーは以下の2つです。
マーカーの規定についての説明は省きます。QPToolkitのサイトを参照してください。
IMG_E3090.JPG

Webカメラをパソコンに繋いでQPServerを起動しましょう。
[Pattern Capture]でマーカーを登録し、マーカー名を適当につけます。
[Data format]で[Name]と[Position in image(px,py)]にチェックをつけます。
[Communication Mode]は[Automatic]にしましょう。
すると、以下のようになってると思います。
QPServer 初期.png
[Camera ID]や[fps]、[Image Size]は使用するカメラに合わせましょう。

それでは[Show preview window]にチェックを入れて、[Start]を押しましょう。
以下のようになります。
マーカーを読み取ってる.png

マーカーの検出は確認できました。次に、チェックを入れたマーカーの名前とカメラ画像上の2次元位置を使ってProcessingで描画する処理に移りましょう。

Processingで描画する

QPToolkitのサイトにProcessingのサンプルプログラムがあるので、これをベースに「マーカーが一定距離近づいたら背景を赤くし雷を描画する」プログラムを書きます。
以下はサイトに記載されているサンプルプログラムです。

サンプルプログラム
import processing.net.*; 
Client myClient;
 
void setup() {
  size(640, 480);
  frameRate(30);
  colorMode(HSB,100);
  myClient = new Client(this,"192.168.32.137",55555); // IPとポート番号
}
 
void draw() {
  background(0);
 
  if (myClient.available() > 0) {
    String dataIn = myClient.readStringUntil('\n');
 
    if ( dataIn != null ) {      
      // "Num="を含むデータ列かどうかをチェックし,データ数を取得
      if ( dataIn.indexOf("Num=")==0 ) {       
        int num = int(split(trim(dataIn),' ' )[1]);
 
        // num個のマーカのデータを取得する
        for (int i=0; i<num; i++) {  
            String marker_info = myClient.readStringUntil('\n');      
            if ( marker_info!= null ) {
              // 1行分のデータを切り分けて欲しいデータを取得する
              String[] data = split(trim(marker_info),',');
 
              // マーカの名前と2次元座標
              String name = data[0];
              int px = int(data[1]);
              int py = int(data[2]);
 
              // 結果を画面に円で表示する
              ellipse(px,py,10,10);
            }        
        }       
      }
    }
    myClient.clear();
  }
}

まずIPを書き換えましょう。こうしないとそもそもProcessingがQPServerからデータを受け取れません。[Server]の[IP]に書かれている数字をsetup関数内のClientインスタンスを生成している部分に入れます。
私の場合、こうなります。

//変更前
void setup() {
  size(640, 480);
  frameRate(30);
  colorMode(HSB,100);
  myClient = new Client(this,"192.168.32.137",55555); // IPとポート番号
}

///変更後
void setup() {
  size(640, 480);
  frameRate(30);
  colorMode(HSB,100);
  myClient = new Client(this,"192.168.0.13",55555); // IPとポート番号
}

これでQPServerからデータを受け取れるようになりました。

次に、受け取ったマーカーの2次元位置をグローバル変数に代入します。この際、名前と2次元位置のデータはそれぞれ切り分けられてProcessingに送られてくるんですが、その情報を区別するためにマーカーの名前を使いましょう。if文で区別するのも良いですが、私はSwicth文を使いました。個人的にこっちの方がマーカーの数が多くなっても見やすくなるかなと思ったからです。

//マーカーに代入するグローバル変数
int px1 = 0;
int py1 = 0;
int px2 = 0;
int py2 = 0;

//マーカーの2次元位置をそれぞれグローバル変数に代入
switch(name) {
case "marker1":
  px1 = px;
  py1 = py;
  break;
case "marker2":
  px2 = px;
  py2 = py;
  break;
}

このコードを以下のように書き加えます。

書き加えた後のプログラム
import processing.net.*; 
Client myClient;

//マーカーに代入するグローバル変数
int px1 = 0;
int py1 = 0;
int px2 = 0;
int py2 = 0;
 
void setup() {
  size(640, 480);
  frameRate(30);
  colorMode(HSB,100);
  myClient = new Client(this,"192.168.0.13",55555); // IPとポート番号
}
 
void draw() {
  background(0);
 
  if (myClient.available() > 0) {
    String dataIn = myClient.readStringUntil('\n');
 
    if ( dataIn != null ) {      
      // "Num="を含むデータ列かどうかをチェックし,データ数を取得
      if ( dataIn.indexOf("Num=")==0 ) {       
        int num = int(split(trim(dataIn),' ' )[1]);
 
        // num個のマーカのデータを取得する
        for (int i=0; i<num; i++) {  
            String marker_info = myClient.readStringUntil('\n');      
            if ( marker_info!= null ) {
              // 1行分のデータを切り分けて欲しいデータを取得する
              String[] data = split(trim(marker_info),',');
 
              // マーカの名前と2次元座標
              String name = data[0];
              int px = int(data[1]);
              int py = int(data[2]);

              //マーカーの2次元位置をそれぞれグローバル変数に代入
              switch(name) {
              case "marker1":
                px1 = px;
                py1 = py;
                break;
              case "marker2":
                px2 = px;
                py2 = py;
                break;
              }
              // 結果を画面に円で表示する
              ellipse(px,py,10,10);
            }       
        }       
      }
    }
    myClient.clear();
  }
}

2次元位置をマーカーの名前で判別し、グローバル変数に代入しました。
次は、マーカーが一定距離近づいたら背景を赤くし雷を描画する処理を書きます。まず雷のクラスを作り、インスタンスを生成します。雷のクラスについての説明は省きます。

雷のクラスとインスタンス
Thunder s1 = new Thunder();

//雷のクラス
class Thunder {
  float x, y;
  int size = 900;
  void draw() {
    noFill();
    int kx = width/2;
    int ky = 0;
    pushMatrix();
    translate(x, y);
    beginShape();
    vertex(kx, ky);

    while (dist(0, 0, kx, ky) < size) {
      stroke(255, 255, 0);
      strokeWeight(10);
      kx += random(60) - 30;
      ky += random(30) - 5;
      vertex(kx, ky);
    }
    endShape();
    popMatrix();
  }
}

一定距離近づいた時の描画処理は以下のようになります。
Processingにはdist関数という2点間距離を測定してくれる便利な関数があるのでそれを使いましょう。距離は適当に200にしましょう。

//マーカー間の距離が200以下なら雷を描画する
if (dist(px1, py1, px2, py2) <= 200) {
  background(200, 0, 0);
  s1.draw();
}

では、雷のクラスと一定距離近づいたときの描画処理を書き加えます。

雷のクラスと描画処理を書き加えたプログラム
import processing.net.*; 
Client myClient;
Thunder s1 = new Thunder();

//マーカーに代入するグローバル変数
int px1 = 0;
int py1 = 0;
int px2 = 0;
int py2 = 0;
 
void setup() {
  size(640, 480);
  frameRate(30);
  colorMode(HSB,100);
  myClient = new Client(this,"192.168.0.13",55555); // IPとポート番号
}
 
void draw() {
  background(0);
 
  if (myClient.available() > 0) {
    String dataIn = myClient.readStringUntil('\n');
 
    if ( dataIn != null ) {      
      // "Num="を含むデータ列かどうかをチェックし,データ数を取得
      if ( dataIn.indexOf("Num=")==0 ) {       
        int num = int(split(trim(dataIn),' ' )[1]);
 
        // num個のマーカのデータを取得する
        for (int i=0; i<num; i++) {  
            String marker_info = myClient.readStringUntil('\n');      
            if ( marker_info!= null ) {
              // 1行分のデータを切り分けて欲しいデータを取得する
              String[] data = split(trim(marker_info),',');
 
              // マーカの名前と2次元座標
              String name = data[0];
              int px = int(data[1]);
              int py = int(data[2]);

              //マーカーの2次元位置をそれぞれグローバル変数に代入
              switch(name) {
              case "marker1":
                px1 = px;
                py1 = py;
                break;
              case "marker2":
                px2 = px;
                py2 = py;
                break;
              }
               //マーカー間の距離が200以下なら雷を描画する
              if (dist(px1, py1, px2, py2) <= 200) {
                background(200, 0, 0);
                s1.draw();
              }
              
              // 結果を画面に円で表示する
              ellipse(px,py,10,10);
            }       
        }       
      }
    }
    myClient.clear();
  }
}

//雷のクラス
class Thunder {
  float x, y;
  int size = 900;
  void draw() {
    noFill();
    int kx = width/2;
    int ky = 0;
    pushMatrix();
    translate(x, y);
    beginShape();
    vertex(kx, ky);

    while (dist(0, 0, kx, ky) < size) {
      stroke(255, 255, 0);
      strokeWeight(10);
      kx += random(60) - 30;
      ky += random(30) - 5;
      vertex(kx, ky);
    }
    endShape();
    popMatrix();
  }
}

実行する前に余分な部分は消してしまいましょう。
colorMode(HSB,100);ellipse(px,py,10,10);を消して、frameRate(30);frameRate(15);にしましょう。(frameRate(30)frameRate(15)にするのは私個人のおすすめです)
すると以下のようなコードになるはずです。

import processing.net.*; 
Client myClient;
Thunder s1 = new Thunder();

//マーカーに代入するグローバル変数
int px1 = 0;
int py1 = 0;
int px2 = 0;
int py2 = 0;
 
void setup() {
  size(640, 480);
  frameRate(15);
  myClient = new Client(this,"192.168.0.13",55555); // IPとポート番号
}
 
void draw() {
  background(0);
 
  if (myClient.available() > 0) {
    String dataIn = myClient.readStringUntil('\n');
 
    if ( dataIn != null ) {      
      // "Num="を含むデータ列かどうかをチェックし,データ数を取得
      if ( dataIn.indexOf("Num=")==0 ) {       
        int num = int(split(trim(dataIn),' ' )[1]);
 
        // num個のマーカのデータを取得する
        for (int i=0; i<num; i++) {  
            String marker_info = myClient.readStringUntil('\n');      
            if ( marker_info!= null ) {
              // 1行分のデータを切り分けて欲しいデータを取得する
              String[] data = split(trim(marker_info),',');
 
              // マーカの名前と2次元座標
              String name = data[0];
              int px = int(data[1]);
              int py = int(data[2]);

              //マーカーの2次元位置をそれぞれグローバル変数に代入
              switch(name) {
              case "marker1":
                px1 = px;
                py1 = py;
                break;
              case "marker2":
                px2 = px;
                py2 = py;
                break;
              }
               //マーカー間の距離が200以下なら雷を描画する
              if (dist(px1, py1, px2, py2) <= 200) {
                background(200, 0, 0);
                s1.draw();
              }
            }       
        }       
      }
    }
    myClient.clear();
  }
}

//雷のクラス
class Thunder {
  float x, y;
  int size = 900;
  void draw() {
    noFill();
    int kx = width/2;
    int ky = 0;
    pushMatrix();
    translate(x, y);
    beginShape();
    vertex(kx, ky);

    while (dist(0, 0, kx, ky) < size) {
      stroke(255, 255, 0);
      strokeWeight(10);
      kx += random(60) - 30;
      ky += random(30) - 5;
      vertex(kx, ky);
    }
    endShape();
    popMatrix();
  }
}

ここまで来ると完成です。QPServerの[Start]を押してから実行してみましょう。
マーカーが200以上離れていると...
何も描画されてない.png
何も描画されていませんが、マーカーを段々近づけてみると...
実行画面 雷.png
赤い背景に雷のような黄色のジグザグした物が描画されていますね。
お好みで雷が描画されているときに電気が流れたときのようなビリビリ音をつけてみると面白いと思います。

参考にさせて頂いたもの

QPToolkit: Webカメラを使った簡単位置計測

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