【openFrameworks】ポイントクラウドデータの分割テクスチャ保存

ポイントクラウドのデータをテクスチャとして保存する。という手順を最近、周りの方から教えていただきました。
今まできちんとプログラムをデータとして見てこなかったせいで、途中の処理過程や、発生したミスをスムーズに解決できませんでした。
やってみたことを備忘録としてまとめます。

今回やること

・openFrameworksでkinectV2の深度カメラとカラーカメラから色付きのポイントクラウドを取得する。
・取得したポイントクラウドの情報をテクスチャデータとして保存。
・保存したテクスチャデータをSyphonで送信。

最終的には以下のようにポイントクラウドから3つのテクスチャを取り出します。

開発環境

・Mac Book Pro (Catalina)
・openFrameworks v0.11.0
・Xcode 10.3

・openFrameworksでkinectV2の深度カメラとカラーカメラから色付きのポイントクラウドを取得する。

今回はofxKinectV2を使用します。
このアドオンのexampleでは深度データの取得と、カラーカメラを深度カメラの画角に合わせるキャリブレーションをかけたカラーデータの取得がされています。
これをそのまま使用してカラー付きのポイントクラウドを取得します。

表示の際にPCLをつかってダウンサンプリングを行いましたが、ここについては別の記事でまとめています。

ofApp.hファイル

#include "ofMain.h"
#include "ofxKinectV2.h"

class ofApp : public ofBaseApp{
public:
		void setup();
		void update();
		void draw();
    
    std::vector<std::shared_ptr<ofxKinectV2>> kinects;
    std::vector<ofTexture> texRGB;
    std::vector<ofTexture> texRGBRegistered;
    std::vector<ofTexture> texIR;
    std::vector<ofTexture> texDepth;
    
    std::size_t currentKinect = 0;
    
    ofEasyCam ecam;
    ofVboMesh mesh;
};

ofApp.cppファイル

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
    ofSetVerticalSync(true);
    ofSetFrameRate(30);
    ofBackground(0);
    
    //see how many devices we have.
    ofxKinectV2 tmp;
    std::vector <ofxKinectV2::KinectDeviceInfo> deviceList = tmp.getDeviceList();
    
    //allocate for this many devices
    kinects.resize(deviceList.size());
    texDepth.resize(kinects.size());
    texRGB.resize(kinects.size());
    texRGBRegistered.resize(kinects.size());
    texIR.resize(kinects.size());
    
    ofxKinectV2::Settings ksettings;
    ksettings.enableRGB = true;
    ksettings.enableIR = true;
    ksettings.enableDepth = true;
    ksettings.enableRGBRegistration = true;
    ksettings.config.MinDepth = 0.5;
    ksettings.config.MaxDepth = 8.0;
    
    for(int d = 0; d < kinects.size(); d++) {
         kinects[d] = std::make_shared<ofxKinectV2>();
         kinects[d]->open(deviceList[d].serial, ksettings);
    }
    
    //drawMesh
    mesh.setUsage(GL_DYNAMIC_DRAW);
    mesh.setMode(OF_PRIMITIVE_POINTS);
    
    //Camera
    ecam.setAutoDistance(false);
    ecam.setPosition(0, 0, 100);
    ecam.lookAt(ofVec3f(0,0,0));
    
    

}

//--------------------------------------------------------------
void ofApp::update(){
    
    //Kinect
    for (int d = 0; d < kinects.size(); d++)
    {
         kinects[d]->update();
         
         if (kinects[d]->isFrameNew())
         {
              if( kinects[d]->isRGBEnabled()) texRGB[d].loadData(kinects[d]->getPixels());
              if(kinects[d]->getRegisteredPixels().getWidth() > 10) texRGBRegistered[d].loadData(kinects[d]->getRegisteredPixels());
              if(kinects[d]->isDepthEnabled() ) texDepth[d].loadData(kinects[d]->getDepthPixels());
              
              //depth
              
              mesh.clear();
              int count = 0;
              if (showPointCloud)
              {
                   for(int y = 0; y < h; y += step) {
                        for(int x = 0; x < w; x += step) {
                             float dist = kinects[d]->getDistanceAt(x, y)*1000;
                             ofPoint pt = kinects[d]->getWorldCoordinateAt(x, y)*1;
                             if(dist > kinects[d]->minDistance && dist < kinects[d]->maxDistance) {
                                  
                                  ofColor c;
                                  c=(kinects[d]->getRegisteredPixels().getColor(x, y));
                                       mesh.addColor(c);
                                       mesh.addVertex(pt);
                                  }
                             }
                        }
                   }
              }
         }
    }

}

//--------------------------------------------------------------
void ofApp::draw(){
    fboS.begin();
    ofClear(0);
    if (isAssist)
    {
         texRGB[0].draw(0, texRGB[0].getHeight()*0.8, texRGB[0].getWidth()*0.2, texRGB[0].getHeight()*0.2);
    }
    ofPushStyle();
    glPointSize(pointSize);
    ecam.begin();
    glEnable(GL_DEPTH_TEST);
    if(isAssist){
         ofDrawAxis(100);
         ofDrawGrid(50,10);
    }
    ofPushMatrix();
    ofTranslate(0, 0, 100);
    ofScale(100, -100, -100);
    mesh.draw();
    ofPopMatrix();
    ecam.end();
    ofPopStyle();
    
    fboS.end();
    fboS.draw(0, 0, ofGetWidth(), ofGetHeight());
    
    glDisable(GL_DEPTH_TEST);
    
    if( kinects.size() < 1 ) {
         ofDrawBitmapStringHighlight( "No Kinects Detected", 40, 40 );
    }else{
         ofDrawBitmapStringHighlight(ofToString(ofGetFrameRate()), 10, 20);
         ofDrawBitmapStringHighlight("Device Count : " + ofToString(kinects.size()), 10, 40);
    }
    
    output.publishTexture(&fboS.getTexture());
}


・取得したポイントクラウドの情報をテクスチャデータとして保存。

今回の記事の中心部分になります。
そもそもなぜポイントクラウドのデータをテクスチャにして送るのかというところですが、点ごとのデータにすることで、他のTouchDesignerなどの他のアプリケーションでポイントクラウドのデータを点ごとに扱えるようになるなどの利点があります。
また、ポイントクラウドの取得と描画のPCを分けることで処理ができたりなどの利点もあります。
Shaderを使えば受け取ったそのまま点群をGPU処理で扱えます。
そもそも3Dデータを点群で扱う方法はShaderを学んだことがある人にとって当たり前のことのようで。。
自分は今回をきっかけに初めて知りました。
実際にShaderで点群を扱った部分に関してはこちらの記事にまとめています。

今回はポイントクラウドのデータの位置情報を上位ビット、下位ビットにわけます。
これにカラー情報を加えてポイントクラウドのデータを全部で3つのテクスチャにします。
テクスチャへの位置情報を割り当ては以下の様に行われます。
まずポイントクラウドの一つ一つのポイントを何からの順番で(例えば手前から奥方向になど)で数えていきます。
この作業をソートというらしいです。
そして数えたポイントの(x,y,z)の座標情報を、テクスチャの(r,g,b)の情報に当てはめていきます。
テクスチャの(0,0)には最初に数えたポイント情報が入るという感じです。
ポイントのカラー情報を保存するテクスチャに関してはポイントの(R,G,B)情報がテクスチャの(r,g,b)の情報に当てはまります。

画像1

位置情報を上位ビット、下位ビットに分ける必要があるのかを説明します。
ofTextureに保存するデータの情報量は16bitです。
ポイントはカラー情報は値が0〜255の8bitの範囲におさまるので問題ありません。
しかし、位置情報は小数点まで含めると例えば
(-0.202315, 0.350982, 0.987822)
など、8bitのデータ量が保存できる256通り以上のデータ量を含んでいます。
なので、データを16bitのshort型を用意して内包した後に8bitのchar型を二つ用意し、上位ビットと下位ビットに分けて保存します。 

画像3


実際にc++ではnion型(共用体)を教えてもらいこれを使って記述しました。

union DataConverter {
    char data[4];
    short sValue[2];
    float fValue;
    int iValue;
};

union型では同じメモリ領域を複数のメンバが共用する構造を取ります。
以下の関数で上位ビットと下位ビットの分割を行います。

void ofApp::splitToBytes(ofPoint p_in, ofColor& low, ofColor& up)
{
    PointType p;
    p.x = floor(p_in.x * 10000);
    p.y = floor(p_in.z * 10000);
    p.z = floor(p_in.y * 10000);
    
    DataConverter converterX;
    converterX.iValue = 0;//初期化
    converterX.sValue[0] = p.x;
    
    DataConverter converterY;
    converterY.iValue = 0;//初期化
    converterY.sValue[0] = p.y;
    
    DataConverter converterZ;
    converterZ.iValue = 0;//初期化
    converterZ.sValue[0] = p.z;
    
    // lower
    char x_lower = converterX.data[0];
    char y_lower = converterY.data[0];
    char z_lower = converterZ.data[0];
    ofColor c_lower(x_lower, y_lower, z_lower, 255);
    
    // upper
    char x_upper = converterX.data[1];
    char y_upper = converterY.data[1];
    char z_upper = converterZ.data[1];
    ofColor c_upper(x_upper, y_upper, z_upper, 255);
    
    low = c_lower;
    up = c_upper;
    
}

最後に今回使用したプログラムを載せておきます。

ofApp.h

#include "ofMain.h"
#include "ofxKinectV2.h"
#include "ofxSyphon.h"
#include "ofxGui.h"

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();
    
    ofxPanel panel;
    
    std::vector<std::shared_ptr<ofxKinectV2>> kinects;
    std::vector<ofTexture> texRGB;
    std::vector<ofTexture> texRGBRegistered;
    std::vector<ofTexture> texIR;
    std::vector<ofTexture> texDepth;
    
    std::size_t currentKinect = 0;
    
    int numRows = 2;
    int numColumns = 2;
    
    ofEasyCam ecam;
    ofVboMesh mesh;
    bool showPointCloud = true;
    
    ofParameter<bool> isAssist;
    ofParameter<int> step;
    ofParameter<bool> isVGF;
    ofParameter<float> pointSize;
    
    //Syphon
    ofxSyphonServer output;
    ofFbo fboS;
    ofxSyphonServer outputUpByte;
    ofxSyphonServer outputLowByte;
    ofxSyphonServer outputColorByte;
    
    // Pixels
    ofPixels pixLowByte;
    ofPixels pixUpByte;
    ofPixels pixColorByte;
    // Texture
    int texWidth = 256;
    int texHeight = 256;
    ofTexture texLowByte;
    ofTexture texUpByte;
    ofTexture texColorByte;
    
    // Convert to Bytes
    void splitToBytes(ofPoint p_in, ofColor& low, ofColor& up);
};

union DataConverter {
    char data[4];
    short sValue[2];
    float fValue;
    int iValue;
};

ofApp.cpp

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
    ofSetVerticalSync(true);
    ofSetFrameRate(30);
    ofBackground(0);
    
    //see how many devices we have.
    ofxKinectV2 tmp;
    std::vector <ofxKinectV2::KinectDeviceInfo> deviceList = tmp.getDeviceList();
    
    //allocate for this many devices
    kinects.resize(deviceList.size());
    texDepth.resize(kinects.size());
    texRGB.resize(kinects.size());
    texRGBRegistered.resize(kinects.size());
    texIR.resize(kinects.size());
    
    panel.setup("", "settings.xml", 10, 100);
    
    ofxKinectV2::Settings ksettings;
    ksettings.enableRGB = true;
    ksettings.enableIR = true;
    ksettings.enableDepth = true;
    ksettings.enableRGBRegistration = true;
    ksettings.config.MinDepth = 0.5;
    ksettings.config.MaxDepth = 8.0;
    
    for(int d = 0; d < kinects.size(); d++) {
         kinects[d] = std::make_shared<ofxKinectV2>();
         kinects[d]->open(deviceList[d].serial, ksettings);
         panel.add(kinects[d]->params);
         panel.add(isAssist.set("Grid", false));
         panel.add(step.set("SamplrRate", 1, 1, 5));
         panel.add(isVGF.set("VGF", true));
         panel.add(pointSize.set("PointSize", 2, 1, 20));
    }
    panel.loadFromFile("settings.xml");
    
    //drawMesh
    mesh.setUsage(GL_DYNAMIC_DRAW);
    mesh.setMode(OF_PRIMITIVE_POINTS);
    
    //Camera
    ecam.setAutoDistance(false);
    ecam.setPosition(0, 0, 100);
    ecam.lookAt(ofVec3f(0,0,0));
    
    //Syphon
    output.setName("OUTPUT");
    fboS.allocate(1920, 1080, GL_RGBA);
    outputUpByte.setName("UpByte");
    outputLowByte.setName("LowByte");
    outputColorByte.setName("ColorByte");
    
    // Pixels
    pixLowByte.allocate(texWidth, texHeight, OF_PIXELS_RGB);
    pixUpByte.allocate(texWidth, texHeight, OF_PIXELS_RGB);
    pixColorByte.allocate(texWidth, texHeight, OF_PIXELS_RGB);
    
    // Texture
    texLowByte.allocate(pixLowByte);
    texLowByte.setTextureMinMagFilter(GL_NEAREST, GL_NEAREST);
    
    texUpByte.allocate(pixUpByte);
    texUpByte.setTextureMinMagFilter(GL_NEAREST, GL_NEAREST);
    
    texColorByte.allocate(pixColorByte);
    texColorByte.setTextureMinMagFilter(GL_NEAREST, GL_NEAREST);

}

//--------------------------------------------------------------
void ofApp::update(){
    
    for (int y = 0; y < texHeight; y++)
    {
         for (int x = 0; x < texWidth; x++)
         {
              pixLowByte.setColor(x, y, ofColor(0, 0, 0, 0));
              pixUpByte.setColor(x, y, ofColor(0, 0, 0, 0));
              pixColorByte.setColor(x, y, ofColor(0, 0, 0, 0));
         }
    }
    
    
    //Kinect
    for (int d = 0; d < kinects.size(); d++)
    {
         kinects[d]->update();
         
         if (kinects[d]->isFrameNew())
         {
              if( kinects[d]->isRGBEnabled()) texRGB[d].loadData(kinects[d]->getPixels());
              if(kinects[d]->getRegisteredPixels().getWidth() > 10) texRGBRegistered[d].loadData(kinects[d]->getRegisteredPixels());
              if(kinects[d]->isDepthEnabled() ) texDepth[d].loadData(kinects[d]->getDepthPixels());
              
              //depth
              int h = kinects[0]->getDepthPixels().getHeight();
              int w = kinects[0]->getDepthPixels().getWidth();
              
              mesh.clear();
              int count = 0;
              ofPoint p_in;
              if (showPointCloud)
              {
                   for(int y = 0; y < h; y += step) {
                        for(int x = 0; x < w; x += step) {
                             float dist = kinects[d]->getDistanceAt(x, y)*1000;
                             float xy = x + (y*w);
                             ofPoint pt = kinects[d]->getWorldCoordinateAt(x, y)*1;
                             
                             if(dist > kinects[d]->minDistance && dist < kinects[d]->maxDistance) {
                                  
                                  ofColor c;
                                  float h = ofMap(dist, 50, 200, 180, 255, true);
                                  c=(kinects[d]->getRegisteredPixels().getColor(x, y));
                                  mesh.addColor(c);
                                  mesh.addVertex(pt);
                                  count++;
                                  p_in.x = pt.x;
                                  p_in.y = pt.y;
                                  p_in.z = pt.z;
                             }
                        }
                   }

                   for(int i = 0; i <count; i++) {
                        ofColor c_lower;
                        ofColor c_upper;
                        splitToBytes(ofPoint(p_in.x, p_in.y, p_in.z), c_lower, c_upper);
                        
                        int x = i % texWidth;
                        int y = i / texWidth;
                        pixLowByte.setColor(x, y, c_lower);
                        pixUpByte.setColor(x, y, c_upper);
                        pixColorByte.setColor(x, y, ofColor(p_in.r, p_in.g, p_in.b, 255));
                   }
              }
         }
    }

}

//--------------------------------------------------------------
void ofApp::draw(){
    fboS.begin();
    ofClear(0);
    if (isAssist)
    {
         texRGB[0].draw(0, texRGB[0].getHeight()*0.8, texRGB[0].getWidth()*0.2, texRGB[0].getHeight()*0.2);
    }
    ofPushStyle();
    glPointSize(pointSize);
    ecam.begin();
    glEnable(GL_DEPTH_TEST);
    if(isAssist){
         ofDrawAxis(100);
         ofDrawGrid(50,10);
    }
    ofPushMatrix();
    ofTranslate(0, 0, 100);
    ofScale(100, -100, -100);
    mesh.draw();
    ofPopMatrix();
    ecam.end();
    ofPopStyle();
    
    fboS.end();
    fboS.draw(0, 0, ofGetWidth(), ofGetHeight());
    
    glDisable(GL_DEPTH_TEST);
    
    if( kinects.size() < 1 ) {
         ofDrawBitmapStringHighlight( "No Kinects Detected", 40, 40 );
    }else{
         ofDrawBitmapStringHighlight(ofToString(ofGetFrameRate()), 10, 20);
         ofDrawBitmapStringHighlight("Device Count : " + ofToString(kinects.size()), 10, 40);
    }
    panel.draw();
    
    output.publishTexture(&fboS.getTexture());
    
    texLowByte.loadData(pixLowByte);
    texUpByte.loadData(pixUpByte);
    texColorByte.loadData(pixColorByte);
    
    outputUpByte.publishTexture(&texUpByte);
    outputLowByte.publishTexture(&texLowByte);
    outputColorByte.publishTexture(&texColorByte);

}

//-----------
void ofApp::splitToBytes(ofPoint p_in, ofColor& low, ofColor& up)
{
    PointType p;
    p.x = floor(p_in.x * 10000);
    p.y = floor(p_in.z * 10000);
    p.z = floor(p_in.y * 10000);

    DataConverter converterX;
    converterX.iValue = 0;
    converterX.sValue[0] = p.x;
    
    DataConverter converterY;
    converterY.iValue = 0;
    converterY.sValue[0] = p.y;
    
    DataConverter converterZ;
    converterZ.iValue = 0;
    converterZ.sValue[0] = p.z;
    
    // lower
    char x_lower = converterX.data[0];
    char y_lower = converterY.data[0];
    char z_lower = converterZ.data[0];
    ofColor c_lower(x_lower, y_lower, z_lower, 255);
    
    
    // upper
    char x_upper = converterX.data[1];
    char y_upper = converterY.data[1];
    char z_upper = converterZ.data[1];
    ofColor c_upper(x_upper, y_upper, z_upper, 255);
    
    low = c_lower;
    up = c_upper;
    
}



この記事が気に入ったらサポートをしてみませんか?