【opencv】教程代码 —videoio(1)使用Orbbec Astra 3D摄像头获取和处理深度和颜色摄像头流的视觉数据...

时间:2024-04-03 08:08:19

24e817165c09c430b46fadd1ffc817b1.png

orbbec_astra.cpp获取和处理来自深度和颜色摄像头流的视觉数据

代码编写了一个多线程的视觉应用程序,其中利用OpenCV库进行视频流处理。主要功能如下:

  • 开启两个视频流,一个用于深度图像,一个用于彩色图像。

  • 设置视频流参数(如分辨率和帧速率)。

  • 并行地在两个线程中分别读取并存储深度和彩色视频流的帧。

  • 将深度帧和彩色帧进行配对,并显示这些帧。

  • 如果用户按下Esc键,程序会终止处理并关闭。

程序中还使用了互斥锁和条件变量来同步两个视频流的帧,确保它们被适时地处理和显示。

这段代码在两个单独的线程中处理深度和彩色视频。然后,它以同步的方式配对深度和彩色帧,并在窗口中显示它们。当用户按下 ESC 键时,程序会停止。

82520e20c16362277e5bf7f5097f60c8.png

#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

2f300f8b8585d63a78a965c21163f974.png

/** @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);

8b200debadd8e7ad82440f6c9dd91144.png

depthStream.retrieve(f.frame, CAP_OPENNI_DEPTH_MAP);

a8eff8cecee397a5df3d0cac58389ddc.png

std::lock_guard<std::mutex> lk(mtx); 与  std::unique_lock<std::mutex> lk(mtx);

41f3cdb2a907863a8c5900685d48d306.png

`std::lock_guard<std::mutex> lk(mtx);`和`std::unique_lock<std::mutex> lk(mtx);`分别是什么?它们有什么区别?

7cf518f15563c1e516d9186ef7fbac06.png

const int64 maxTdiff = int64(1000000000 / (2 * colorStream.get(CAP_PROP_FPS)));

b71fd80bbc9bea5ddf78654e36a1fb33.png

depthFrame.frame.convertTo(d8, CV_8U, 255.0 / 2500);

6487af956265c81913ff5023f9422499.png

applyColorMap(d8, dColor, COLORMAP_OCEAN);

85cbd53718c5d63bc2f6023075cac1de.png

 使用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函数中,但你可以在这里插入任何其他处理代码。

在最顶部的样本图像中,你可以看到表示相同场景的彩色帧和深度帧。
从彩色帧看起来很难区分植物叶子和墙上画的叶子,
但深度数据可以轻松做到。