OpenCV 4.11.0
开源计算机视觉
加载中…
搜索中…
无匹配项
使用奥比中光Astra 3D摄像头

上一教程: 使用Kinect和其他兼容OpenNI的深度传感器
下一教程: 使用奥比中光3D摄像头(UVC)

引言

本教程介绍奥比中光Astra系列3D摄像头 (https://www.orbbec.com/products/structured-light-camera/astra-series/)。这些摄像头除了普通的彩色传感器外,还具有深度传感器。可以使用开源OpenNI API和cv::VideoCapture类读取深度传感器数据。视频流通过常规摄像头接口提供。

安装说明

为了使用OpenCV访问Astra摄像头的深度传感器,您应该执行以下步骤:

  1. 下载最新版本的奥比中光OpenNI SDK(此处下载 https://www.orbbec.com/developers/openni-sdk/)。解压缩存档,根据您的操作系统选择相应的版本,并按照Readme文件中的安装步骤进行操作。
  2. 例如,如果您使用的是64位GNU/Linux系统,请运行:

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

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

    $ 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

    如果上述两个变量为空,则需要再次运行OpenNIDevEnvironment

    注意
    奥比中光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"
    最后尝试的版本2.3.0.86_202210111154_4c8f5aa4_beta6即使按照说明重新构建libusb后,也无法与现代Linux系统正常工作。最后一个已知可用的版本是2.3.0.63(在Ubuntu 18.04 amd64上测试)。该版本在官方下载页面上没有提供,但由奥比中光技术支持在奥比中光社区论坛发布在此处
  3. 现在,您可以通过在CMake中设置WITH_OPENNI2标志来配置具有OpenNI支持的OpenCV。您可能还希望启用BUILD_EXAMPLES标志以获得一个可与您的Astra摄像头一起使用的代码示例。在包含OpenCV源代码的目录中运行以下命令以启用OpenNI支持:
    $ mkdir build
    $ cd build
    $ cmake -DWITH_OPENNI2=ON ..
    如果找到OpenNI库,则OpenCV将构建OpenNI2支持。您可以在CMake日志中看到OpenNI2支持的状态:
    -- 视频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
    $ 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 << "深度流: "
76 << depthStream.get(CAP_PROP_FRAME_WIDTH) << "x" << depthStream.get(CAP_PROP_FRAME_HEIGHT)
77 << " @" << 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 – 此标志通过更改深度生成器的视点来将重新映射的深度图注册到图像图(如果标志为“开启”)或将其视点设置为其正常视点(如果标志为“关闭”)。注册过程生成的图像像素对齐,这意味着图像中的每个像素都与深度图像中的一个像素对齐。
  • cv::CAP_PROP_OPENNI2_MIRROR – 用于启用或禁用此流的镜像的标志。设置为 0 可禁用镜像。

    以下属性仅用于获取:

  • cv::CAP_PROP_OPENNI_FRAME_MAX_DEPTH – 相机最大支持深度(以毫米为单位)。
  • cv::CAP_PROP_OPENNI_BASELINE – 基线值(以毫米为单位)。

设置好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. 深度生成器提供的数据:
    • cv::CAP_OPENNI_DEPTH_MAP - 深度值(以毫米为单位)(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)。

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

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 // 显示深度图
188 Mat d8, dColor;
189 depthFrame.frame.convertTo(d8, CV_8U, 255.0 / 2500);
190 applyColorMap(d8, dColor, COLORMAP_OCEAN);
191 imshow("深度图 (彩色)", dColor);
192
193 // 显示彩色图
194 imshow("彩色图", colorFrame.frame);
196
197 // 按Esc键退出
198 int key = waitKey(1);
199 if (key == 27) // ESC
200 {
201 isFinish = true;
202 break;
203 }
204 }
205 }

在上面的代码片段中,程序会阻塞执行,直到两个帧列表中都有一些帧。当有新帧时,会检查它们的时间戳——如果它们的时间差大于帧周期的一半,则丢弃其中一帧。如果时间戳足够接近,则将两帧配对。现在,我们有两帧:一帧包含颜色信息,另一帧包含深度信息。在上面的示例中,检索到的帧只是用cv::imshow 函数显示,但是你可以在此处插入任何其他处理代码。

在下面的示例图像中,您可以看到表示同一场景的彩色帧和深度帧。查看彩色帧,很难区分植物叶子和墙上绘制的叶子,但深度数据使这变得容易。

完整的实现可以在openni_orbbec_astra.cpp (位于samples/cpp/tutorial_code/videoio 目录)中找到。