上一教程: 使用Kinect和其他兼容OpenNI的深度传感器
下一教程: 使用奥比中光3D摄像头(UVC)
引言
本教程介绍奥比中光Astra系列3D摄像头 (https://www.orbbec.com/products/structured-light-camera/astra-series/)。这些摄像头除了普通的彩色传感器外,还具有深度传感器。可以使用开源OpenNI API和cv::VideoCapture类读取深度传感器数据。视频流通过常规摄像头接口提供。
安装说明
为了使用OpenCV访问Astra摄像头的深度传感器,您应该执行以下步骤:
- 下载最新版本的奥比中光OpenNI SDK(此处下载 https://www.orbbec.com/developers/openni-sdk/)。解压缩存档,根据您的操作系统选择相应的版本,并按照Readme文件中的安装步骤进行操作。
例如,如果您使用的是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上测试)。该版本在官方下载页面上没有提供,但由奥比中光技术支持在奥比中光社区论坛发布在此处。
- 现在,您可以通过在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)
- 构建OpenCV
代码
Astra Pro摄像头有两个传感器——深度传感器和彩色传感器。可以使用OpenNI接口和cv::VideoCapture类读取深度传感器数据。视频流无法通过OpenNI API获得,只能通过常规摄像头接口提供。因此,要获取深度和彩色帧,应创建两个cv::VideoCapture对象。
第一个对象将使用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::set和cv::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接口可用的摄像头属性:
设置好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;
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;
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 可以检索以下数据:
- 深度生成器提供的数据:
- 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)
- 彩色传感器提供的数据是常规 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
189 depthFrame.frame.convertTo(d8,
CV_8U, 255.0 / 2500);
191 imshow(
"深度图 (彩色)", dColor);
192
193
194 imshow(
"彩色图", colorFrame.frame);
196
197
199 if (key == 27)
200 {
201 isFinish = true;
202 break;
203 }
204 }
205 }
在上面的代码片段中,程序会阻塞执行,直到两个帧列表中都有一些帧。当有新帧时,会检查它们的时间戳——如果它们的时间差大于帧周期的一半,则丢弃其中一帧。如果时间戳足够接近,则将两帧配对。现在,我们有两帧:一帧包含颜色信息,另一帧包含深度信息。在上面的示例中,检索到的帧只是用cv::imshow 函数显示,但是你可以在此处插入任何其他处理代码。
在下面的示例图像中,您可以看到表示同一场景的彩色帧和深度帧。查看彩色帧,很难区分植物叶子和墙上绘制的叶子,但深度数据使这变得容易。
完整的实现可以在openni_orbbec_astra.cpp (位于samples/cpp/tutorial_code/videoio
目录)中找到。