OpenCV  4.10.0
开源计算机视觉
加载中...
搜索中...
无匹配项
使用 Orbbec Astra 3D 摄像头

上一个教程: 使用 Kinect 和其他兼容 OpenNI 的深度传感器
下一个教程: 使用 Creative Senz3D 和其他兼容英特尔 RealSense SDK 的深度传感器

简介

本教程主要介绍 Orbbec 3D 摄像头 Astra 系列(https://orbbec3d.com/index/Product/info.html?cate=38&id=36)。除了常见的颜色传感器,这些摄像头还配有深度传感器。可以使用开源 OpenNI API 与 cv::VideoCapture 类读取深度传感器。视频流通过常规摄像头界面提供。

安装说明

要使用 OpenCV 中的 Astra 摄像头深度传感器,您需要执行以下步骤

  1. 下载最新版本的 Orbbec OpenNI SDK(此处 https://orbbec3d.com/index/download.html)。解压缩存档,根据您的操作系统选择版本,并按照自述文件中提供的安装步骤进行操作。
  2. 例如,如果您使用 64 位 GNU/Linux 运行

    $ cd Linux/OpenNI-Linux-x64-2.3.0.63/
    $ sudo ./install.sh

    安装完成后,务必重新插入设备以使 udev 规则生效。该摄像头现在应作为常规摄像头设备工作。请注意,您当前的用户应属于组 video 以访问摄像头。此外,务必提供 OpenNIDevEnvironment 文件

    $ source OpenNIDevEnvironment

    要验证源命令是否有效且能找到 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

    如果以上两个变量为空,则需要再次提供 OpenNIDevEnvironment

    注意
    Orbbec OpenNI SDK 2.3.0.86 及更高版本不再提供 install.sh。可以使用以下脚本初始化环境
    # 检查用户是否为 root/使用 sudo 运行
    if [ `whoami` != root ]; then
    echo 请使用 sudo 运行此脚本
    exit
    fi
    ORIG_PATH=`pwd`
    cd `dirname $0`
    SCRIPT_PATH=`pwd`
    cd $ORIG_PATH
    if [ "`uname -s`" != "Darwin" ]; then
    # 为 USB 设备安装 UDEV 规则
    cp ${SCRIPT_PATH}/orbbec-usb.rules /etc/udev/rules.d/558-orbbec-usb.rules
    echo "USB 规则文件安装于 /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"
  3. 现在,您可以通过在 CMake 中设置WITH_OPENNI2标志启用 OpenNI 支持来配置 OpenCV。您可能还希望启用BUILD_EXAMPLES标志,以便获得一个代码示例,用于配合您的 Astra 相机工作。在包含 OpenCV 源代码的目录中运行以下命令,以启用 OpenNI 支持
    $ mkdir build
    $ cd build
    $ cmake -DWITH_OPENNI2=ON ..
    如果找到 OpenNI 库,OpenCV 将使用 OpenNI2 支持进行构建。您可以在 CMake 日志中查看 OpenNI2 支持的状态
    -- 视频输入/输出
    -- DC1394: 是 (2.2.6)
    -- FFMPEG: 是
    -- avcodec: 是 (58.91.100)
    -- avformat: 是 (58.45.100)
    -- avutil: 是 (56.51.100)
    -- swscale: 是 (5.7.100)
    -- avresample: 否
    -- GStreamer: 是 (1.18.1)
    -- OpenNI2: 是 (2.3.0)
    -- v4l/v4l2: 是 (linux/videodev2.h)
  4. 构建 OpenCV
    $ make

代码

Astra Pro 相机有两个传感器——一个深度传感器和一个颜色传感器。深度传感器可以使用 OpenNI 接口与cv::VideoCapture类一起读取。视频流无法通过 OpenNI API 获得,只能通过常规相机接口提供。因此,若要同时获取深度帧和颜色帧,应创建两个cv::VideoCapture对象

39 // 打开深度流
40 VideoCapture depthStream(CAP_OPENNI2_ASTRA);
41 // 打开颜色流
42 VideoCapture colorStream(0, CAP_V4L2);

第一个对象将使用 OpenNI2 API 来检索深度数据。第二个对象使用 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::setcv::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,因此你必须创建一个新线程来获取帧,以避免在一个流读取时另一个流被阻塞。VideoCapture 不是一个线程安全类,因此你需要小心,避免任何可能的死锁或数据竞争。

由于有两个视频源应同时读取,因此,有必要创建两个线程以避免阻塞。实现示例,它获取每个传感器中的帧,并将其存储在一个列表中,同时带有它们的时间戳

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;
101 f.timestamp = cv::getTickCount();
102 depthStream.retrieve(f.frame, CAP_OPENNI_DEPTH_MAP);
103 if (f.frame.empty())
104 {
105 cerr << "ERROR: 无法对深度流中的帧进行解码" << 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;
129 f.timestamp = cv::getTickCount();
130 colorStream.retrieve(f.frame);
131 if (f.frame.empty())
132 {
133 cerr << "ERROR:解码彩色流的帧时失败" << 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 可以获取以下数据

  1. 深度发生器提供的数据
  2. 彩色传感器提供的数据是常规的 BGR 图像 (CV_8UC3)。

当有新的数据可用时,每个读取线程都使用条件变量通知主线程。一个帧存储在有序列表中,列表中的第一个帧是最早捕获的帧,最后一个帧是最晚捕获的帧。由于深度帧和彩色帧是从独立源读取的,因此即使两个流都针对相同的帧速率进行设置,它们也可能不同步。可以对流应用后同步程序,将深度帧和彩色帧组合成对。下面的示例代码演示了此过程

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 帧 `depthFrame` = `depthFrames` 中的第一个元素
164 int64 `depthT` = `depthFrame` 的时间戳
165
166 // 获取列表中的第一帧
167 帧 colorFrame = `colorFrames` 中的第一个元素
168 int64 `colorT` = `colorFrame` 的时间戳
169
170 // 半帧时差是帧之间时间差的最大值
171 const int64 `maxTdiff` = int64(1000000000 / (2 * `colorStream`.get(CAP_PROP_FPS)));
172 if (`depthT` + `maxTdiff` < `colorT`)
173 {
174 `depthFrames` 弹出前一个元素
175 continue;
176 }
177 else if (`colorT` + `maxTdiff` < `depthT`)
178 {
179 `colorFrames` 弹出前一个元素
180 continue;
181 }
182 `depthFrames` 弹出前一个元素
183 `colorFrames` 弹出前一个元素
184 解锁 lk
185
187 // 显示深度帧
188 Mat d8, dColor;
189 `depthFrame`.frame 转换为 d8,CV_8U,255.0 / 2500
190 使用 `COLORMAP_OCEAN` 将 d8 转换为 dColor
191 `imshow`("Depth (colored)", dColor);
192
193 // 显示彩色帧
194 `imshow`("Color", `colorFrame`.frame);
196
197 // 按下 ESC 键退出
198 int key = `waitKey`(1);
199 if (key == 27) // ESC
200 {
201 `isFinish` = true;
202 break;
203 }
204 }
205 }
#define CV_8U
定义 interface.h:73
int64_t int64
定义 interface.h:61

在上面的代码段中,执行将被阻塞,直到两个帧列表中存在一些帧。当出现新帧时,将对它们的时间戳进行检查——如果它们相差超过一帧周期的二分之一,则其中一帧将被丢弃。如果时间戳足够接近,则两个帧配对。现在,我们有两个帧:一个包含颜色信息,另一个包含深度信息。在上面的示例中,检索到的帧仅通过 cv::imshow 函数进行展示,但您可以在此处插入任何其他处理代码。

在下面的示例图像中,您可以看到表示同一场景的颜色帧和深度帧。通过查看颜色帧,很难将植物叶与绘制在墙上的叶子区别开来,但深度数据却很容易做到这一点。

完整的实现可以在 samples/cpp/tutorial_code/videoio 目录的 orbbec_astra.cpp 中找到。