关于作者
Igor Murzov 拥有计算机科学硕士学位。他目前是 Xperience.AI 的软件工程师,从事 OpenCV 和物联网软件开发工作。
简介
在过去十年中,3D 技术已成为日常生活的一部分——我们观看 3D 电影,玩 VR 游戏,使用 3D 打印机创建立体细节,等等。如今,甚至智能手机也配备了深度传感器,以提供高级 3D 功能。因此,可以肯定地说,在不久的将来,拥有一个 3D 摄像头将变得非常普遍。3D 摄像头可用于任何地方:机器人技术、游戏、增强现实、虚拟现实、智能家居、医疗保健、零售、自动化等等。我相信 3D 摄像头在未来将得到更广泛的应用。
3D 摄像头能够实现更高级的用例。与传统仅处理彩色像素图的计算机视觉算法相比,3D 摄像头增加了另一维数据,这使得能够从根本上扩展可以应用于该数据的计算机视觉算法的范围,因为除了彩色像素图之外,还有深度信息。在下面的示例图像中,您可以看到显示相同场景的彩色帧和深度帧(深度帧是彩色的,颜色越深表示场景中物体越近)。仅查看彩色帧,很难区分植物叶子和墙上绘制的叶子,但深度数据使区分变得很容易。
在这篇文章中,我们将了解如何使用 Orbbec Astra Pro 摄像头和开源 OpenNI API。Orbbec 是领先的 3D 摄像头制造商之一。Astra Pro 摄像头提供高端响应速度、深度测量、平滑渐变和精确轮廓,以及过滤低质量深度像素的功能。该摄像头可以以每秒 30 帧的速度传输高清视频和 VGA 深度图像。
因此,让我们学习如何使用 OpenCV 库操作 Astra Pro 摄像头。
安装
为了使用 Astra 摄像头的深度传感器与 OpenCV,您应该首先安装 Orbbec OpenNI2 SDK,并在启用该 SDK 支持的情况下构建 OpenCV。目前没有提供带有 OpenNI2 SDK 支持的预构建 OpenCV,因此您需要自己进行操作。
- 从 这里下载最新版本的 Orbbec OpenNI2 SDK。
- 解压缩存档,根据您的操作系统选择构建版本,并按照 Readme 文件中提供的安装步骤进行操作。
- 然后,通过在 CMake 中将 WITH_OPENNI2 标志设置为 ON,配置启用 OpenNI2 支持的 OpenCV。您可能还想启用 BUILD_EXAMPLES 标志以获得一个与您的 Astra 摄像头配合使用的代码示例。
更详细的说明可以在以下 教程中找到。
视频流设置
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 对象之前,我们需要设置流参数,即 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 方法。深度生成器支持以下通过 OpenNI 接口提供的 3D 摄像头属性。
- 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,因此您必须在新线程中抓取帧,以避免在读取另一个流时阻塞流。由于有两个视频源应同时读取,因此有必要创建两个“读取器”线程以避免阻塞。示例实现从每个传感器在新线程中获取帧,并将它们与时间戳一起存储在列表中。
// 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\n"; 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\n"; 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)。
当有新数据可用时,每个“读取器”线程都会使用条件变量通知主线程。帧存储在有序列表中——列表中的第一帧是最早捕获的帧,最后一帧是最晚捕获的帧。
视频流同步
由于深度和彩色帧是从独立的源读取的,因此即使两个流都设置为相同的帧率,两个视频流也可能不同步。可以对流应用后同步过程,以将深度和彩色帧组合成对。以下示例代码演示了此过程。
std::list<std::pair<Frame, Frame>> dcFrames; std::mutex dcFramesMtx; std::condition_variable dcReady; // Pair depth and color frames std::thread framePairing([&] { // Half of frame period is a maximum time diff between frames const int64 maxTdiff = 1000000000 / (2 * colorStream.get(CAP_PROP_FPS)); while (!isFinish) { std::unique_lock<std::mutex> lk(mtx); while (!isFinish && (depthFrames.empty() || colorFrames.empty())) dataReady.wait(lk); while (!depthFrames.empty() && !colorFrames.empty()) { // 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; if (depthT + maxTdiff < colorT) { depthFrames.pop_front(); } else if (colorT + maxTdiff < depthT) { colorFrames.pop_front(); } else { dcFramesMtx.lock(); dcFrames.push_back(std::make_pair(depthFrame, colorFrame)); dcFramesMtx.unlock(); dcReady.notify_one(); depthFrames.pop_front(); colorFrames.pop_front(); } } } });
在上面的代码段中,执行将被阻塞,直到两个帧列表中都有一些帧。当有新帧时,将检查它们的时间戳——如果它们之间的差异大于帧周期的二分之一,则丢弃其中一个帧。如果时间戳足够接近,则将这两个帧放入 dcFrames 列表中,并通知处理线程有新数据可用。
视频处理
处理线程函数可能如下所示。
// Processing thread std::thread process([&] { while (!isFinish) { std::unique_lock<std::mutex> lk(dcFramesMtx); while (!isFinish && dcFrames.empty()) dcReady.wait(lk); while (!dcFrames.empty()) { if (!lk.owns_lock()) lk.lock(); Frame depthFrame = dcFrames.front().first; Frame colorFrame = dcFrames.front().second; dcFrames.pop_front(); lk.unlock(); // >>> Do something here <<< } } });
在处理线程中,您将有两个帧:一个包含颜色信息,另一个包含深度信息。在 >>> 在这里执行某些操作 <<< 标记之后插入您的代码——数据已准备好使用 OpenCV 或您的自定义算法进行处理。
完整的实现可以在这里找到 这里。
总结
3D 摄像头在现代工业中发挥着重要作用。它们为我们提供了解决无法使用 2D 数据完成的计算机视觉任务的绝佳机会。在这篇文章中,我们学习了如何使用 OpenNI2 接口在 OpenCV 中操作 3D 摄像头,以及如何解决可能出现的同步问题。