使用 Orbbec Astra 3D 相机
本教程专门介绍 Orbbec 3D 相机的 Astra 系列(https://www.orbbec.com/products/structured-light-camera/astra-series/)。
这类相机在普通彩色传感器之外还配备了一个深度传感器(depth sensor)。可以使用开源的 OpenNI API 通过 cv::VideoCapture 类来读取深度传感器。视频流则通过常规相机接口提供。
要在 OpenCV 中使用 Astra 相机的深度传感器,你应当完成以下步骤:
-
下载最新版本的 Orbbec OpenNI SDK(从这里获取)。解压归档文件,根据你的操作系统选择相应的构建版本,并按照 Readme 文件中提供的步骤进行安装。
-
例如,如果你使用 64 位 GNU/Linux,运行:
Terminal window $ cd Linux/OpenNI-Linux-x64-2.3.0.63/$ sudo ./install.sh安装完成后,请务必重新插拔设备,以使 udev 规则生效。此时相机应当能像普通相机设备一样工作。请注意,当前用户应属于
video组才能访问相机。此外,请确保 source 了OpenNIDevEnvironment文件:Terminal window $ source OpenNIDevEnvironment要验证 source 命令是否生效、能否找到 OpenNI 库及头文件,请运行以下命令,你应当会在终端中看到类似内容:
Terminal window $ 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如果以上两个变量为空,那么你需要重新 source
OpenNIDevEnvironment。注意: Orbbec OpenNI SDK 2.3.0.86 及更新版本不再提供
install.sh。你可以使用以下脚本来初始化环境:# Check if user is root/running with sudoif [ `whoami` != root ]; thenecho Please run this script with sudoexitfiORIG_PATH=`pwd`cd `dirname $0`SCRIPT_PATH=`pwd`cd $ORIG_PATHif [ "`uname -s`" != "Darwin" ]; then# Install UDEV rules for USB devicecp ${SCRIPT_PATH}/orbbec-usb.rules /etc/udev/rules.d/558-orbbec-usb.rulesecho "usb rules file install at /etc/udev/rules.d/558-orbbec-usb.rules"fiOUT_FILE="$SCRIPT_PATH/OpenNIDevEnvironment"echo "export OPENNI2_INCLUDE=$SCRIPT_PATH/../sdk/Include" > $OUT_FILEecho "export OPENNI2_REDIST=$SCRIPT_PATH/../sdk/libs" >> $OUT_FILEchmod a+r $OUT_FILEecho "exit"注意: 最后试用过的版本
2.3.0.86_202210111154_4c8f5aa4_beta6即使按照说明重建 libusb 之后,也无法在现代 Linux 上正常工作。最后一个已知可用的配置是 2.3.0.63 版本(在 Ubuntu 18.04 amd64 上测试通过)。官方下载页面不再提供该版本,但由 Orbbec 技术支持发布在 Orbbec 社区论坛的这里。 -
现在你可以通过在 CMake 中设置
WITH_OPENNI2标志,将 OpenCV 配置为启用 OpenNI 支持。你可能还希望启用BUILD_EXAMPLES标志,以获得一个能在你的 Astra 相机上运行的代码示例。在包含 OpenCV 源代码的目录中运行以下命令以启用 OpenNI 支持:Terminal window $ mkdir build$ cd build$ cmake -DWITH_OPENNI2=ON ..如果找到 OpenNI 库,OpenCV 将随 OpenNI2 支持一起编译。你可以在 CMake 日志中看到 OpenNI2 支持的状态:
-- 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:
Terminal window $ make

Astra Pro 相机有两个传感器——一个深度传感器和一个彩色传感器。深度传感器可以通过 OpenNI 接口配合 cv::VideoCapture 类来读取。视频流无法通过 OpenNI API 获取,只能通过常规相机接口提供。因此,为了同时获取深度帧和彩色帧,需要创建两个 cv::VideoCapture 对象:
// Open depth streamVideoCapture depthStream(CAP_OPENNI2_ASTRA);// Open color streamVideoCapture colorStream(0, CAP_V4L2);第一个对象将使用 OpenNI2 API 来获取深度数据。第二个对象使用 Video4Linux2 接口来访问彩色传感器。请注意,上面的示例假设 Astra 相机是系统中的第一个相机。如果你连接了不止一个相机,可能需要显式地设置正确的相机编号。
在使用所创建的 VideoCapture 对象之前,你可能希望通过设置对象属性来配置流参数。最重要的参数是帧宽度、帧高度和 fps。在本示例中,我们将两个流的宽度和高度都配置为 VGA 分辨率,这是两个传感器都可用的最大分辨率,并且我们希望两个流的参数相同,以便更方便地进行彩色到深度数据的配准:
// Set color and depth stream parameterscolorStream.set(CAP_PROP_FRAME_WIDTH, 640);colorStream.set(CAP_PROP_FRAME_HEIGHT, 480);depthStream.set(CAP_PROP_FRAME_WIDTH, 640);depthStream.set(CAP_PROP_FRAME_HEIGHT, 480);depthStream.set(CAP_PROP_OPENNI2_MIRROR, 0);要设置和获取传感器数据生成器的某些属性,分别使用 cv::VideoCapture::set 和 cv::VideoCapture::get 方法,例如:
// Print depth stream parameterscout << "Depth stream: " << depthStream.get(CAP_PROP_FRAME_WIDTH) << "x" << depthStream.get(CAP_PROP_FRAME_HEIGHT) << " @" << depthStream.get(CAP_PROP_FPS) << " fps" << endl;通过 OpenNI 接口可用的相机支持以下深度生成器属性:
cv::CAP_PROP_FRAME_WIDTH—— 帧宽度,以像素为单位。cv::CAP_PROP_FRAME_HEIGHT—— 帧高度,以像素为单位。cv::CAP_PROP_FPS—— 帧率,以 FPS 为单位。cv::CAP_PROP_OPENNI_REGISTRATION—— 通过改变深度生成器的视点(当标志为 “on” 时)将重映射的深度图注册到图像图的标志,或(当标志为 “off” 时)将该视点恢复为正常视点。注册过程产生的图像是像素对齐的,这意味着图像中的每个像素都与深度图像中的一个像素对齐。cv::CAP_PROP_OPENNI2_MIRROR—— 用于启用或禁用该流镜像的标志。设为 0 可禁用镜像。
以下属性仅支持获取:
cv::CAP_PROP_OPENNI_FRAME_MAX_DEPTH—— 相机支持的最大深度,以 mm 为单位。cv::CAP_PROP_OPENNI_BASELINE—— 基线值,以 mm 为单位。
在 VideoCapture 对象配置好之后,你就可以开始从中读取帧了。
注意: OpenCV 的 VideoCapture 提供的是同步 API,因此你必须在新的线程中抓取帧,以避免在读取一个流时阻塞另一个流。VideoCapture 不是线程安全的类,因此你需要小心避免任何可能的死锁或数据竞争。
由于存在两个需要同时读取的视频源,因此必须创建两个线程以避免阻塞。下面是一个示例实现:在每个传感器的新线程中获取帧,并将帧连同其时间戳一起存入一个列表:
// Create two lists to store framesstd::list<Frame> depthFrames, colorFrames;const std::size_t maxFrames = 64;
// Synchronization objectsstd::mutex mtx;std::condition_variable dataReady;std::atomic<bool> isFinish;
isFinish = false;
// Start depth reading threadstd::thread depthReader([&]{ while (!isFinish) { // Grab and decode new frame if (depthStream.grab()) { Frame f; f.timestamp = cv::getTickCount(); depthStream.retrieve(f.frame, CAP_OPENNI_DEPTH_MAP); if (f.frame.empty()) { cerr << "ERROR: Failed to decode frame from depth stream" << endl; break; }
{ std::lock_guard<std::mutex> lk(mtx); if (depthFrames.size() >= maxFrames) depthFrames.pop_front(); depthFrames.push_back(f); } dataReady.notify_one(); } }});
// Start color reading threadstd::thread colorReader([&]{ while (!isFinish) { // Grab and decode new frame if (colorStream.grab()) { Frame f; f.timestamp = cv::getTickCount(); colorStream.retrieve(f.frame); if (f.frame.empty()) { cerr << "ERROR: Failed to decode frame from color stream" << endl; break; }
{ std::lock_guard<std::mutex> lk(mtx); if (colorFrames.size() >= maxFrames) colorFrames.pop_front(); colorFrames.push_back(f); } dataReady.notify_one(); } }});VideoCapture 可以获取以下数据:
-
来自深度生成器的数据:
cv::CAP_OPENNI_DEPTH_MAP—— 以 mm 为单位的深度值(CV_16UC1)cv::CAP_OPENNI_POINT_CLOUD_MAP—— 以米为单位的 XYZ 坐标(CV_32FC3)cv::CAP_OPENNI_DISPARITY_MAP—— 以像素为单位的视差(CV_8UC1)cv::CAP_OPENNI_DISPARITY_MAP_32F—— 以像素为单位的视差(CV_32FC1)cv::CAP_OPENNI_VALID_DEPTH_MASK—— 有效像素掩码(未被遮挡、未被阴影遮挡等)(CV_8UC1)
-
来自彩色传感器的数据是一幅常规的 BGR 图像(
CV_8UC3)。
当有新数据可用时,每个读取线程都会通过条件变量通知主线程。帧被存放在一个有序列表中——列表中第一帧是最早捕获的,最后一帧是最新捕获的。由于深度帧和彩色帧是从相互独立的源读取的,即使两个流设置为相同的帧率,两路视频流也可能变得不同步。可以对两路流应用一个后同步过程,将深度帧和彩色帧配对。下面的示例代码演示了这一过程:
// Pair depth and color frameswhile (!isFinish){ std::unique_lock<std::mutex> lk(mtx); while (!isFinish && (depthFrames.empty() || colorFrames.empty())) dataReady.wait(lk);
while (!depthFrames.empty() && !colorFrames.empty()) { if (!lk.owns_lock()) lk.lock();
// Get a frame from the list Frame depthFrame = depthFrames.front(); int64 depthT = depthFrame.timestamp;
// Get a frame from the list Frame colorFrame = colorFrames.front(); int64 colorT = colorFrame.timestamp;
// Half of frame period is a maximum time diff between frames const int64 maxTdiff = int64(1000000000 / (2 * colorStream.get(CAP_PROP_FPS))); if (depthT + maxTdiff < colorT) { depthFrames.pop_front(); continue; } else if (colorT + maxTdiff < depthT) { colorFrames.pop_front(); continue; } depthFrames.pop_front(); colorFrames.pop_front(); lk.unlock();
// Show depth frame Mat d8, dColor; depthFrame.frame.convertTo(d8, CV_8U, 255.0 / 2500); applyColorMap(d8, dColor, COLORMAP_OCEAN); imshow("Depth (colored)", dColor);
// Show color frame imshow("Color", colorFrame.frame);
// Exit on Esc key press int key = waitKey(1); if (key == 27) // ESC { isFinish = true; break; } }}在上面的代码片段中,执行会一直阻塞,直到两个帧列表中都存在一些帧。当有新帧时,会检查它们的时间戳——如果差异超过半个帧周期,则丢弃其中一帧。如果时间戳足够接近,则将两帧配对。现在我们有两帧:一帧包含彩色信息,另一帧包含深度信息。在上面的示例中,获取到的帧只是用 cv::imshow 函数简单地显示出来,但你可以在这里插入任何其他的处理代码。
在示例输出中,你可以看到表示同一场景的彩色帧和深度帧。仅看彩色帧很难区分真实的植物叶片和画在墙上的叶片,但深度数据让这一切变得容易。
完整的实现可以在
openni_orbbec_astra.cpp
中找到,位于 samples/cpp/tutorial_code/videoio 目录下。