Skip to content

使用 Orbbec Astra 3D 相机

本教程专门介绍 Orbbec 3D 相机的 Astra 系列(https://www.orbbec.com/products/structured-light-camera/astra-series/)。 这类相机在普通彩色传感器之外还配备了一个深度传感器(depth sensor)。可以使用开源的 OpenNI API 通过 cv::VideoCapture 类来读取深度传感器。视频流则通过常规相机接口提供。

要在 OpenCV 中使用 Astra 相机的深度传感器,你应当完成以下步骤:

  1. 下载最新版本的 Orbbec OpenNI SDK(从这里获取)。解压归档文件,根据你的操作系统选择相应的构建版本,并按照 Readme 文件中提供的步骤进行安装。

  2. 例如,如果你使用 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 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 社区论坛的这里。

  3. 现在你可以通过在 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)
  4. 编译 OpenCV:

    Terminal window
    $ make

astra_color.jpg astra_depth.png

Astra Pro 相机有两个传感器——一个深度传感器和一个彩色传感器。深度传感器可以通过 OpenNI 接口配合 cv::VideoCapture 类来读取。视频流无法通过 OpenNI API 获取,只能通过常规相机接口提供。因此,为了同时获取深度帧和彩色帧,需要创建两个 cv::VideoCapture 对象:

// Open depth stream
VideoCapture depthStream(CAP_OPENNI2_ASTRA);
// Open color stream
VideoCapture colorStream(0, CAP_V4L2);

第一个对象将使用 OpenNI2 API 来获取深度数据。第二个对象使用 Video4Linux2 接口来访问彩色传感器。请注意,上面的示例假设 Astra 相机是系统中的第一个相机。如果你连接了不止一个相机,可能需要显式地设置正确的相机编号。

在使用所创建的 VideoCapture 对象之前,你可能希望通过设置对象属性来配置流参数。最重要的参数是帧宽度、帧高度和 fps。在本示例中,我们将两个流的宽度和高度都配置为 VGA 分辨率,这是两个传感器都可用的最大分辨率,并且我们希望两个流的参数相同,以便更方便地进行彩色到深度数据的配准:

// Set color and depth stream parameters
colorStream.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 parameters
cout << "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 frames
std::list<Frame> depthFrames, colorFrames;
const std::size_t maxFrames = 64;
// Synchronization objects
std::mutex mtx;
std::condition_variable dataReady;
std::atomic<bool> isFinish;
isFinish = false;
// Start depth reading thread
std::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 thread
std::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 可以获取以下数据:

  1. 来自深度生成器的数据:

    • 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)
  2. 来自彩色传感器的数据是一幅常规的 BGR 图像(CV_8UC3)。

当有新数据可用时,每个读取线程都会通过条件变量通知主线程。帧被存放在一个有序列表中——列表中第一帧是最早捕获的,最后一帧是最新捕获的。由于深度帧和彩色帧是从相互独立的源读取的,即使两个流设置为相同的帧率,两路视频流也可能变得不同步。可以对两路流应用一个后同步过程,将深度帧和彩色帧配对。下面的示例代码演示了这一过程:

// Pair depth and color frames
while (!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 目录下。