前のチュートリアル: Kinect やその他の OpenNI 互換深度センサーの利用
次のチュートリアル: Orbbec 3D カメラ(UVC)の利用
はじめに
このチュートリアルは Orbbec 3D カメラの Astra シリーズ(https://www.orbbec.com/products/structured-light-camera/astra-series/)を扱う。これらのカメラは通常のカラーセンサーに加えて深度センサーを備えている。深度センサーはオープンソースの OpenNI API を用いて cv::VideoCapture クラスで読み取ることができる。ビデオストリームは通常のカメラインターフェイスを通じて提供される。
インストール手順
OpenCV で Astra カメラの深度センサーを使用するには、次の手順を実行する必要がある:
- Orbbec OpenNI SDK の最新版をダウンロードする(こちらから https://www.orbbec.com/developers/openni-sdk/)。アーカイブを解凍し、使用しているオペレーティングシステムに応じたビルドを選択し、Readme ファイルに記載されたインストール手順に従う。
たとえば 64bit の GNU/Linux を使用している場合は次を実行する:
$ cd Linux/OpenNI-Linux-x64-2.3.0.63/
$ sudo ./install.sh
インストールが完了したら、udev ルールを有効にするためにデバイスを必ず接続し直すこと。これでカメラは一般的なカメラデバイスとして動作するはずである。カメラにアクセスするには、現在のユーザーが video グループに属している必要があることに注意する。また、OpenNIDevEnvironment ファイルを必ず source すること:
$ source OpenNIDevEnvironment
source コマンドが機能し、OpenNI ライブラリとヘッダファイルが見つかることを確認するには、次のコマンドを実行する。ターミナルに次のような出力が表示されるはずである:
$ echo $OPENNI2_INCLUDE
/home/user/OpenNI_2.3.0.63/Linux/OpenNI-Linux-x64-2.3.0.63/Include
$ echo $OPENNI2_REDIST
/home/user/OpenNI_2.3.0.63/Linux/OpenNI-Linux-x64-2.3.0.63/Redist
上記の 2 つの変数が空の場合は、OpenNIDevEnvironment を再度 source する必要がある。
- 覚え書き
- Orbbec OpenNI SDK バージョン 2.3.0.86 以降では
install.sh は提供されなくなった。環境を初期化するには次のスクリプトを使用できる: # Check if user is root/running with sudo
if [ `whoami` != root ]; then
echo Please run this script with sudo
exit
fi
ORIG_PATH=`pwd`
cd `dirname $0`
SCRIPT_PATH=`pwd`
cd $ORIG_PATH
if [ "`uname -s`" != "Darwin" ]; then
# Install UDEV rules for USB device
cp ${SCRIPT_PATH}/orbbec-usb.rules /etc/udev/rules.d/558-orbbec-usb.rules
echo "usb rules file install at /etc/udev/rules.d/558-orbbec-usb.rules"
fi
OUT_FILE="$SCRIPT_PATH/OpenNIDevEnvironment"
echo "export OPENNI2_INCLUDE=$SCRIPT_PATH/../sdk/Include" > $OUT_FILE
echo "export OPENNI2_REDIST=$SCRIPT_PATH/../sdk/libs" >> $OUT_FILE
chmod a+r $OUT_FILE
echo "exit"
- 最後に試したバージョン
2.3.0.86_202210111154_4c8f5aa4_beta6 は、手順の推奨どおり libusb を再ビルドしても、最近の Linux では正しく動作しない。最後に判明している正常動作する構成はバージョン 2.3.0.63 である(Ubuntu 18.04 amd64 でテスト済み)。これはダウンロードページでは公式には提供されていないが、Orbbec のテクニカルサポートが Orbbec コミュニティフォーラムの こちらで公開している。
- これで、CMake で
WITH_OPENNI2 フラグを設定することで、OpenNI サポートを有効にして OpenCV を構成できる。Astra カメラで動作するコードサンプルを取得するために BUILD_EXAMPLES フラグも有効にするとよい。OpenNI サポートを有効にするには、OpenCV のソースコードを含むディレクトリで次のコマンドを実行する: $ mkdir build
$ cd build
$ cmake -DWITH_OPENNI2=ON ..
OpenNI ライブラリが見つかると、OpenCV は OpenNI2 サポート付きでビルドされる。OpenNI2 サポートの状態は CMake のログで確認できる: -- Video I/O:
-- DC1394: YES (2.2.6)
-- FFMPEG: YES
-- avcodec: YES (58.91.100)
-- avformat: YES (58.45.100)
-- avutil: YES (56.51.100)
-- swscale: YES (5.7.100)
-- avresample: NO
-- GStreamer: YES (1.18.1)
-- OpenNI2: YES (2.3.0)
-- v4l/v4l2: YES (linux/videodev2.h)
- OpenCV をビルドする:
コード
Astra Pro カメラには 2 つのセンサー(深度センサーとカラーセンサー)がある。深度センサーは OpenNI インターフェイスを用いて cv::VideoCapture クラスで読み取ることができる。ビデオストリームは OpenNI API からは利用できず、通常のカメラインターフェイスを通じてのみ提供される。したがって、深度フレームとカラーフレームの両方を取得するには、2 つの cv::VideoCapture オブジェクトを作成する必要がある:
1 つ目のオブジェクトは OpenNI2 API を使って深度データを取得する。2 つ目は Video4Linux2 インターフェイスを使ってカラーセンサーにアクセスする。上記の例では Astra カメラがシステム内の最初のカメラであることを前提としている点に注意する。複数のカメラが接続されている場合は、適切なカメラ番号を明示的に設定する必要があるかもしれない。
作成した VideoCapture オブジェクトを使用する前に、オブジェクトのプロパティを設定してストリームパラメータを設定するとよい。最も重要なパラメータはフレーム幅、フレーム高さ、fps である。この例では、両方のストリームの幅と高さを、両センサーで利用可能な最大解像度である VGA 解像度に設定し、カラーから深度へのデータレジストレーションを容易にするために両ストリームのパラメータを同じにする:
60
61 colorStream.set(CAP_PROP_FRAME_WIDTH, 640);
62 colorStream.set(CAP_PROP_FRAME_HEIGHT, 480);
63 depthStream.set(CAP_PROP_FRAME_WIDTH, 640);
64 depthStream.set(CAP_PROP_FRAME_HEIGHT, 480);
65 depthStream.set(CAP_PROP_OPENNI2_MIRROR, 0);
センサーデータジェネレータのプロパティを設定および取得するには、それぞれ cv::VideoCapture::set と cv::VideoCapture::get メソッドを使用する。例:
74
75 cout << "Depth stream: "
76 << depthStream.get(CAP_PROP_FRAME_WIDTH) << "x" << depthStream.get(CAP_PROP_FRAME_HEIGHT)
77 << " @" << depthStream.get(CAP_PROP_FPS) << " fps" << endl;
OpenNI インターフェイスを通じて利用可能なカメラの次のプロパティが、深度ジェネレータでサポートされている:
VideoCapture オブジェクトの設定が完了したら、それらからフレームの読み取りを開始できる。
- 覚え書き
- OpenCV の VideoCapture は同期 API を提供するため、一方のストリームが読み取られている間にもう一方のストリームがブロックされるのを避けるには、新しいスレッドでフレームを grab する必要がある。VideoCapture はスレッドセーフなクラスではないため、デッドロックやデータ競合が起こらないよう注意する必要がある。
同時に読み取るべきビデオソースが 2 つあるため、ブロッキングを避けるには 2 つのスレッドを作成する必要がある。各センサーから新しいスレッドでフレームを取得し、タイムスタンプとともにリストに格納する実装例:
81
82 std::list<Frame> depthFrames, colorFrames;
83 const std::size_t maxFrames = 64;
84
85
86 std::mutex mtx;
87 std::condition_variable dataReady;
88 std::atomic<bool> isFinish;
89
90 isFinish = false;
91
92
93 std::thread depthReader([&]
94 {
95 while (!isFinish)
96 {
97
98 if (depthStream.grab())
99 {
100 Frame f;
102 depthStream.retrieve(f.frame, CAP_OPENNI_DEPTH_MAP);
103 if (f.frame.empty())
104 {
105 cerr << "ERROR: Failed to decode frame from depth stream" << endl;
106 break;
107 }
108
109 {
110 std::lock_guard<std::mutex> lk(mtx);
111 if (depthFrames.size() >= maxFrames)
112 depthFrames.pop_front();
113 depthFrames.push_back(f);
114 }
115 dataReady.notify_one();
116 }
117 }
118 });
119
120
121 std::thread colorReader([&]
122 {
123 while (!isFinish)
124 {
125
126 if (colorStream.grab())
127 {
128 Frame f;
130 colorStream.retrieve(f.frame);
131 if (f.frame.empty())
132 {
133 cerr << "ERROR: Failed to decode frame from color stream" << endl;
134 break;
135 }
136
137 {
138 std::lock_guard<std::mutex> lk(mtx);
139 if (colorFrames.size() >= maxFrames)
140 colorFrames.pop_front();
141 colorFrames.push_back(f);
142 }
143 dataReady.notify_one();
144 }
145 }
146 });
VideoCapture は次のデータを取得できる:
- data given from the depth generator:
- カラーセンサーから得られるデータは通常の BGR 画像(CV_8UC3)である。
新しいデータが利用可能になると、各読み取りスレッドは条件変数を使ってメインスレッドに通知する。フレームは順序付きリストに格納される。リストの最初のフレームが最も早く取得されたもの、最後のフレームが最も新しく取得されたものである。深度フレームとカラーフレームは独立したソースから読み取られるため、両ストリームが同じフレームレートに設定されていても、2 つのビデオストリームは同期がずれることがある。深度フレームとカラーフレームをペアに組み合わせるために、ストリームに対して後処理としての同期手順を適用できる。以下のサンプルコードはこの手順を示している:
150
151 while (!isFinish)
152 {
153 std::unique_lock<std::mutex> lk(mtx);
154 while (!isFinish && (depthFrames.empty() || colorFrames.empty()))
155 dataReady.wait(lk);
156
157 while (!depthFrames.empty() && !colorFrames.empty())
158 {
159 if (!lk.owns_lock())
160 lk.lock();
161
162
163 Frame depthFrame = depthFrames.front();
164 int64 depthT = depthFrame.timestamp;
165
166
167 Frame colorFrame = colorFrames.front();
168 int64 colorT = colorFrame.timestamp;
169
170
171 const int64 maxTdiff =
int64(1000000000 / (2 * colorStream.get(CAP_PROP_FPS)));
172 if (depthT + maxTdiff < colorT)
173 {
174 depthFrames.pop_front();
175 continue;
176 }
177 else if (colorT + maxTdiff < depthT)
178 {
179 colorFrames.pop_front();
180 continue;
181 }
182 depthFrames.pop_front();
183 colorFrames.pop_front();
184 lk.unlock();
185
187
189 depthFrame.frame.convertTo(d8,
CV_8U, 255.0 / 2500);
191 imshow(
"Depth (colored)", dColor);
192
193
194 imshow(
"Color", colorFrame.frame);
196
197
199 if (key == 27)
200 {
201 isFinish = true;
202 break;
203 }
204 }
205 }
上記のコードスニペットでは、両方のフレームリストにフレームが存在するまで実行がブロックされる。新しいフレームがある場合、それらのタイムスタンプが確認される。フレーム周期の半分以上の差がある場合は、いずれか一方のフレームが破棄される。タイムスタンプが十分に近い場合は、2 つのフレームがペアにされる。これで、カラー情報を含むフレームと深度情報を含むフレームの 2 つが得られる。上記の例では取得したフレームを単に cv::imshow 関数で表示しているが、ここに任意の他の処理コードを挿入できる。
以下のサンプル画像では、同じシーンを表すカラーフレームと深度フレームを確認できる。カラーフレームを見ると、植物の葉と壁に描かれた葉を区別するのは難しいが、深度データを使えば容易になる。
完全な実装は samples/cpp/tutorial_code/videoio ディレクトリ内の openni_orbbec_astra.cpp にある。