orbbec_astra.cpp获取和处理来自深度和颜色摄像头流的视觉数据
代码编写了一个多线程的视觉应用程序,其中利用OpenCV库进行视频流处理。主要功能如下:
开启两个视频流,一个用于深度图像,一个用于彩色图像。
设置视频流参数(如分辨率和帧速率)。
并行地在两个线程中分别读取并存储深度和彩色视频流的帧。
将深度帧和彩色帧进行配对,并显示这些帧。
如果用户按下Esc键,程序会终止处理并关闭。
程序中还使用了互斥锁和条件变量来同步两个视频流的帧,确保它们被适时地处理和显示。
这段代码在两个单独的线程中处理深度和彩色视频。然后,它以同步的方式配对深度和彩色帧,并在窗口中显示它们。当用户按下 ESC 键时,程序会停止。
#include <opencv2/videoio/videoio.hpp> // 包含OpenCV视频输入输出头文件
#include <opencv2/highgui.hpp> // 包含OpenCV的GUI(图形用户界面)头文件
#include <opencv2/imgproc.hpp> // 包含OpenCV图像处理头文件
#include <list> // 包含C++标准库列表容器头文件
#include <iostream> // 包含输入输出流头文件
// 检查是否定义了多线程支持
#if !defined(HAVE_THREADS)
int main()
{
std::cout << "This sample is built without threading support. Sample code is disabled." << std::endl; // 如果没有开启多线程支持,则输出信息
return 0;
}
#else
#include <thread> // 包含C++11标准线程头文件
#include <mutex> // 包含互斥锁头文件
#include <condition_variable> // 包含条件变量头文件
#include <atomic> // 包含原子操作头文件
using namespace cv; // 使用命名空间cv,用于OpenCV相关功能
using std::cout; // 使用标准输出流cout
using std::cerr; // 使用标准错误流cerr
using std::endl; // 使用换行符endl
// 存储帧及其时间戳的结构体
struct Frame
{
int64 timestamp; // 时间戳
Mat frame; // 帧数据
};
int main()
{
//! [Open streams]
// 打开深度摄像头流
VideoCapture depthStream(CAP_OPENNI2_ASTRA);
// 打开彩色摄像头流
VideoCapture colorStream(0, CAP_V4L2);
//! [Open streams]
// 检查彩色流是否已打开
if (!colorStream.isOpened())
{
cerr << "ERROR: Unable to open color stream" << endl;
return 1;
}
// 检查深度流是否已打开
if (!depthStream.isOpened())
{
cerr << "ERROR: Unable to open depth stream" << endl;
return 1;
}
//! [Setup streams]
// 设置彩色和深度流参数
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);
//! [Setup streams]
// 打印彩色流参数
cout << "Color stream: "
<< colorStream.get(CAP_PROP_FRAME_WIDTH) << "x" << colorStream.get(CAP_PROP_FRAME_HEIGHT)
<< " @" << colorStream.get(CAP_PROP_FPS) << " fps" << endl;
//! [Get properties]
// 打印深度流参数
cout << "Depth stream: "
<< depthStream.get(CAP_PROP_FRAME_WIDTH) << "x" << depthStream.get(CAP_PROP_FRAME_HEIGHT)
<< " @" << depthStream.get(CAP_PROP_FPS) << " fps" << endl;
//! [Get properties]
//! [Read streams]
// 创建两个列表存储帧数据
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: Failed to decode frame from depth stream" << 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: Failed to decode frame from color stream" << endl;
break;
}
{//实例化的时候就会立即尝试获取互斥锁,如果没有获取到,则会阻塞到获取锁为止;并且一旦lock_guard对象被创建,你就不能手动来释放锁或者再次获取锁。
std::lock_guard<std::mutex> lk(mtx); // 加锁
if (colorFrames.size() >= maxFrames) // 如果超过最大帧数
colorFrames.pop_front(); // 删除列表前端帧
colorFrames.push_back(f); // 加入新帧到列表
}
dataReady.notify_one(); // 通知条件变量
}
}
});
//! [Read streams]
//! [Pair frames]
// 配对深度和彩色帧
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;//当程序执行到continue这个语句时,它将立即结束本轮循环中剩余的操作,并跳过循环体中continue之后的代码,直接开始下一轮的循环。
}
else if (colorT + maxTdiff < depthT) // 如果彩色帧时间戳小于深度帧时间戳
{
colorFrames.pop_front(); // 删除彩色帧
continue;//当程序执行到continue这个语句时,它将立即结束本轮循环中剩余的操作,并跳过循环体中continue之后的代码,直接开始下一轮的循环。
}
depthFrames.pop_front(); // 删除深度帧
colorFrames.pop_front(); // 删除彩色帧
lk.unlock(); // 解锁
//! [Show frames]
// 显示深度帧
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); // 显示彩色帧图
//! [Show frames]
// 通过按Esc键退出
int key = waitKey(1);
if (key == 27) // 如果是ESC键
{
isFinish = true; // 设置结束标志为真
break;
}
}
}
//! [Pair frames]
dataReady.notify_one(); // 通知条件变量
depthReader.join(); // 等待深度读取线程结束
colorReader.join(); // 等待彩色读取线程结束
return 0;
}
#endif
运行报错:
[ INFO:0@0.052] global videoio_registry.cpp:244 cv::`anonymous-namespace'::VideoBackendRegistry::VideoBackendRegistry VIDEOIO: Enabled backends(9, sorted by priority): FFMPEG(1000); GSTREAMER(990); INTEL_MFX(980); MSMF(970); DSHOW(960); CV_IMAGES(950); CV_MJPEG(940); UEYE(930); OBSENSOR(920)
ERROR: Unable to open color stream
/** @brief Sets a property in the VideoCapture.
@param propId Property identifier from cv::VideoCaptureProperties (eg. cv::CAP_PROP_POS_MSEC, cv::CAP_PROP_POS_FRAMES, ...)
or one from @ref videoio_flags_others
@param value Value of the property.
@return `true` if the property is supported by backend used by the VideoCapture instance.
@note Even if it returns `true` this doesn't ensure that the property
value has been accepted by the capture device. See note in VideoCapture::get()
*/
CV_WRAP virtual bool set(int propId, double value);
depthStream.retrieve(f.frame, CAP_OPENNI_DEPTH_MAP);
std::lock_guard<std::mutex> lk(mtx); 与 std::unique_lock<std::mutex> lk(mtx);
`std::lock_guard<std::mutex> lk(mtx);`和`std::unique_lock<std::mutex> lk(mtx);`分别是什么?它们有什么区别?
const int64 maxTdiff = int64(1000000000 / (2 * colorStream.get(CAP_PROP_FPS)));
depthFrame.frame.convertTo(d8, CV_8U, 255.0 / 2500);
applyColorMap(d8, dColor, COLORMAP_OCEAN);
使用Orbbec Astra 3D摄像头
======================================================
简介
这篇教程是关于Orbbec Astra系列3D摄像头的使用 (https://orbbec3d.com/index/Product/info.html?cate=38&id=36)。
这些摄像头除了常见的彩色传感器外,还有一个深度传感器。可以使用开源的OpenNI API,
通过@ref cv::VideoCapture 类来读取深度传感器的数据。视频流是通过常规的摄像头接口提供的。
安装说明
为了能够使用Astra摄像头的深度传感器和OpenCV,你需要执行以下步骤:
-# 下载Orbbec OpenNI SDK的最新版本(从这里https://orbbec3d.com/index/download.html下载)。
解压缩文件,根据你的操作系统选择build,并按照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
。
@note 从Orbbec OpenNI SDK版本2.3.0.86开始,不再提供install.sh
。
你可以使用下面的脚本来初始化环境:
# 检查用户是否是root/用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
# 为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"
-# 现在你可以配置OpenCV,通过设置CMake中的WITH_OPENNI2
标志来启用OpenNI支持。
为了得到一个与你的Astra摄像头一起工作的代码样本,你可能还想启用BUILD_EXAMPLES
标志。
在包含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:
$ make
代码
Astra Pro摄像头有两个传感器 -- 一个深度传感器和一个彩色传感器。深度传感器可以使用OpenNI接口
通过@ref cv::VideoCapture 类读取。视频流不通过OpenNI API提供,而是通过常规摄像头接口提供。
因此,为了获取深度和彩色帧,需要创建两个@ref cv::VideoCapture对象:
@snippetlineno samples/cpp/tutorial_code/videoio/orbbec_astra/orbbec_astra.cpp 打开视频流
第一个对象将使用OpenNI2 API来检索深度数据。第二个对象使用Video4Linux2接口访问彩色传感器。
请注意,上面的例子假设Astra摄像头是系统中的第一个摄像头。如果你连接了多个摄像头,你可能需要显式设置正确的摄像头编号。
在使用创建的VideoCapture对象之前,你可能想通过设置对象的属性来设置流参数。
最重要的参数是帧宽度、帧高度和帧率。对于这个例子,我们将配置两个流的宽度和高度为VGA分辨率,
这是两个传感器可用的最大分辨率,我们希望两个流的参数都是一样的,以便于颜色数据到深度数据的注册更加容易:
@snippetlineno samples/cpp/tutorial_code/videoio/orbbec_astra/orbbec_astra.cpp 设置视频流
要设置和获取传感器数据生成器的一些属性,请分别使用@ref cv::VideoCapture::set 和
@ref cv::VideoCapture::get方法,例如:
@snippetlineno samples/cpp/tutorial_code/videoio/orbbec_astra/orbbec_astra.cpp 获取属性
以下是通过OpenNI接口可用的摄像头属性,适用于深度生成器:
@ref cv::CAP_PROP_FRAME_WIDTH -- 像素中的帧宽度。
@ref cv::CAP_PROP_FRAME_HEIGHT -- 像素中的帧高度。
@ref cv::CAP_PROP_FPS -- 帧率,单位FPS。
@ref cv::CAP_PROP_OPENNI_REGISTRATION -- 标志,注册了深度图到图像的映射(如果标志是“开”的话),
或者将这个视点设置为它的正常视点(如果标志是“关”的话)。注册的结果图像是像素对齐的,
也就是说图像中的每个像素都跟深度图像中的像素对齐。-
@ref cv::CAP_PROP_OPENNI2_MIRROR -- 标志,用于为这个流启用或禁用镜像功能。设置为0以禁用镜像。
下面的属性仅用于获取:
@ref cv::CAP_PROP_OPENNI_FRAME_MAX_DEPTH -- 摄像头最大支持的深度,单位mm。
@ref cv::CAP_PROP_OPENNI_BASELINE -- 基线值,单位mm。
设置好VideoCapture对象后,你可以开始从它们中读取帧。
@note
OpenCV的VideoCapture提供了同步API,因此你必须在新的线程中抓取帧,以避免一个流被阻塞,
而另一个流正在被读取。VideoCapture不是一个线程安全的类,所以你需要小心避免任何可能的死锁
或数据竞争。
由于需要同时从两个视频源读取数据,因此必须创建两个线程以避免阻塞。下面的示例实现了在新线程中
从每个传感器获取帧,并将它们连同其时间戳存储在列表中:
@snippetlineno samples/cpp/tutorial_code/videoio/orbbec_astra/orbbec_astra.cpp 读取视频流
VideoCapture可以检索以下数据:
-# 由深度生成器提供的数据:
- @ref cv::CAP_OPENNI_DEPTH_MAP - 深度值,单位mm(CV_16UC1)
- @ref cv::CAP_OPENNI_POINT_CLOUD_MAP - XYZ,单位m(CV_32FC3)
- @ref cv::CAP_OPENNI_DISPARITY_MAP - 视差值,单位像素(CV_8UC1)
- @ref cv::CAP_OPENNI_DISPARITY_MAP_32F - 视差值,单位像素(CV_32FC1)
- @ref cv::CAP_OPENNI_VALID_DEPTH_MASK - 有效像素的掩码(非被遮挡,非被阴影覆盖等)(CV_8UC1)
-# 由彩色传感器提供的数据是普通的BGR图像(CV_8UC3)。
当新数据可用时,每个读取线程使用条件变量通知主线程。
帧被存储在有序列表中 —— 列表中的第一个帧是最早捕获的,最后一个帧是最晚捕获的。
由于深度和彩色帧是从独立源读取的,即使位两个视频流设置了相同的帧率,它们也可能不同步。
可以对流进行后同步处理,将深度和彩色帧配对。下面的示例代码演示了这个过程:
@snippetlineno samples/cpp/tutorial_code/videoio/orbbec_astra/orbbec_astra.cpp 配对帧
在上面的代码片段中,执行将被阻塞,直到两个帧列表中都有一些帧为止。
当有新帧时,将检查它们的时间戳 - 如果它们的差异超过帧周期的一半,则其中一个帧将被丢弃。
如果时间戳足够接近,那么两个帧就被配对了。现在,我们有了两个帧:一个包含彩色信息,另一个包含深度信息。
在上面的示例中,检索到的帧简单地显示在cv::imshow函数中,但你可以在这里插入任何其他处理代码。
在最顶部的样本图像中,你可以看到表示相同场景的彩色帧和深度帧。
从彩色帧看起来很难区分植物叶子和墙上画的叶子,
但深度数据可以轻松做到。