上一个教程 : 使用 Kinect 和其他兼容 OpenNI 的深度传感器
下一个教程 : 使用 Creative Senz3D 和其他兼容 Intel RealSense SDK 的深度传感器
简介
本教程专门介绍奥尔贝克 3D 摄像机 Astra 系列 (https://orbbec3d.com/index/Product/info.html?cate=38&id=36)。除了普通的彩色传感器外,该相机还配有深度传感器。深度传感器可通过 cv::VideoCapture 类的开源 OpenNI API 读取。视频流通过普通摄像头接口提供。
安装说明
要将 Astra 摄像机的深度传感器与 OpenCV 结合使用,需要执行以下步骤:
- 下载最新版本的 Orbbec OpenNI SDK(从此处 https://orbbec3d.com/index/download.html)。解压压缩包,根据操作系统选择构建版本,然后按照自述文件中提供的步骤进行安装。
- 例如,如果使用的是 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
。您可以使用以下脚本初始化环境:
# 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"
- 现在,你可以通过在 CMake 中设置
WITH_OPENNI2
标志,在配置 OpenCV 时启用 OpenNI 支持。您可能还想启用BUILD_EXAMPLES
标志,以获得与 Astra 摄像头配合使用的代码示例。在包含 OpenCV 源代码的目录中运行以下命令以启用 OpenNI 支持:
$ 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:
$ make
代码
Astra Pro 相机有两个传感器:深度传感器和颜色传感器。深度传感器可通过 cv::VideoCapture 类的 OpenNI 接口读取。OpenNI API 无法提供视频流,只能通过常规的摄像头接口提供。因此,要同时获取深度和颜色帧,需要创建两个 cv::VideoCapture 对象:
// 打开深度流
VideoCapture depthStream(CAP_OPENNI2_ASTRA);
// 打开颜色流
VideoCapture colorStream(0, CAP_V4L2);
第一个对象将使用 OpenNI2 API 获取深度数据。第二个对象使用 Video4Linux2 接口访问颜色传感器。请注意,上面的示例假定 Astra 摄像机是系统中的第一台摄像机。如果连接了多台摄像机,可能需要明确设置适当的摄像机编号。
在使用创建的 VideoCapture 对象之前,您可能需要通过设置对象属性来设置流参数。最重要的参数是帧宽、帧高和帧频。在本示例中,我们将把两个数据流的宽度和高度都配置为 VGA 分辨率,这是两个传感器的最大分辨率,而且我们希望两个数据流的参数都相同,以方便颜色到深度数据的注册:
// 设置色彩流和深度流参数
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 方法,例如 :
// 打印深度流参数
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 - 通过改变深度生成器的视点(如果该标志为 “开”)或将该视点设置为正常视点(如果该标志为 “关”),将深度图重映射到图像映射的标志。配准过程生成的图像是像素对齐的,这意味着图像中的每个像素都与深度图中的一个像素对齐。
- cv::CAP_PROP_OPENNI2_MIRROR - 启用或禁用此数据流镜像的标志。设置为 0 表示禁用镜像
以下属性仅用于获取:
- cv::CAP_PROP_OPENNI_FRAME_MAX_DEPTH - 摄像机的最大支持深度(单位:毫米)。
- cv::CAP_PROP_OPENNI_BASELINE - 基线值(单位:毫米)。
设置好 VideoCapture 对象后,就可以开始读取帧了。
注意事项
OpenCV 的 VideoCapture 提供同步API,因此必须在新线程中抓取帧,以避免在读取另一个流时阻塞一个流。VideoCapture并非线程安全类,因此需要小心避免任何可能的死锁或数据竞争。
由于需要同时读取两个视频源,因此有必要创建两个线程来避免阻塞。示例实现在一个新线程中从每个传感器获取帧,并将它们连同时间戳一起存储在一个列表中:
// 创建两个列表来存储帧
std::list<Frame> depthFrames, colorFrames;
const std::size_t maxFrames = 64;
// 同步对象
std::mutex mtx;
std::condition_variable dataReady;
std::atomic<bool> isFinish;
isFinish = false;
// 启动深度读取线程
std::thread depthReader([&])
{
while (!isFinish)
{
// 抓取并解码新帧
if (depthStream.grab())
{
Frame f;
f.timestamp = cv::getTickCount();
depthStream.retrieve(f.frame, CAP_OPENNI_DEPTH_MAP);
if (f.frame.empty())
{
cerr << "ERROR: 从深度流解码帧失败" << endl;
break;
}
{
std::lock_guard<std::mutex> lk(mtx);
if(depthFrames.size() >= maxFrames)
depthFrames.pop_front();
depthFrames.push_back(f);
}
dataReady.notify_one();
}
}
});
// 启动读取颜色的线程
std::thread colorReader([&])
{
while (!isFinish)
{
// 抓取并解码新帧
if (colorStream.grab())
{
Frame f;
f.timestamp = cv::getTickCount();
colorStream.retrieve(f.frame);
if(f.frame.empty())
{
cerr << "ERROR: 从颜色流解码帧失败" << 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 - 深度值,以毫米为单位 (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)。
当有新数据时,每个读取线程会使用条件变量通知主线程。帧被存储在有序列表中–列表中的第一帧是最早捕获的帧,最后一帧是最新捕获的帧。由于深度和彩色帧是从独立的信号源读取的,因此即使两个视频流都设置为相同的帧频,也可能会出现不同步的情况。可以对视频流采用后同步程序,将深度和色彩帧合成一对。下面的示例代码演示了这一过程:
// 将深度和色彩帧配对
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();
// 从列表中获取一个帧
Frame depthFrame = depthFrames.front();
int64 depthT = depthFrame.timestamp;
// 从列表中获取一个帧
Frame colorFrame = colorFrames.front();
int64 colorT = colorFrame.timestamp;
// 帧周期的一半是帧之间的最大时间差
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();
// 显示深度框
Mat d8, dColor;
depthFrame.frame.convertTo(d8, CV_8U, 255.0 / 2500);
applyColorMap(d8, dColor, COLORMAP_OCEAN);
imshow("Depth (colored)", dColor);
// 显示颜色框
imshow("Color", colorFrame.frame);
// 按 Esc 键退出
int key = waitKey(1);
if (key == 27) // ESC
{
isFinish = true;
break;
}
}
}
在上面的代码片段中,执行被阻塞,直到两个帧列表中都有一些帧。当出现新的帧时,将检查它们的时间戳–如果它们的时间戳相差超过帧周期的一半,那么其中一个帧将被丢弃。如果时间戳足够接近,则将两个帧配对。现在,我们有两个帧:一个包含颜色信息,另一个包含深度信息。在上面的示例中,我们使用 cv::imshow 函数简单地显示了检索到的帧,但您也可以在此插入任何其他处理代码。
在下面的示例图像中,您可以看到代表同一场景的颜色帧和深度帧。从彩色帧中很难分辨出植物的叶子和画在墙上的叶子,而深度数据则让分辨变得容易。
完整的实现可以在 samples/cpp/tutorial_code/videoio 目录下的 orbbec_astra.cpp 中找到。