由于捕获缓冲区,OpenCV VideoCapture 滞后

Posted

技术标签:

【中文标题】由于捕获缓冲区,OpenCV VideoCapture 滞后【英文标题】:OpenCV VideoCapture lag due to the capture buffer 【发布时间】:2015-07-13 22:52:09 【问题描述】:

我正在通过提供 mjpeg 流的网络摄像头捕捉视频。 我在工作线程中进行了视频捕获。 我这样开始捕获:

const std::string videoStreamAddress = "http://192.168.1.173:80/live/0/mjpeg.jpg?x.mjpeg";
qDebug() << "start";
cap.open(videoStreamAddress);
qDebug() << "really started";
cap.set(CV_CAP_PROP_FRAME_WIDTH, 720);
cap.set(CV_CAP_PROP_FRAME_HEIGHT, 576);

摄像头以 20fps 的速度输入视频流。 但如果我像这样以 20fps 的速度阅读:

if (!cap.isOpened()) return;

        Mat frame;
        cap >> frame; // get a new frame from camera
        mutex.lock();

        m_imageFrame = frame;
        mutex.unlock();

然后会有 3 秒以上的延迟。 原因是捕获的视频首先存储在缓冲区中。当我第一次启动相机时,缓冲区被累积但我没有读取帧。所以如果我从缓冲区读取它总是给我旧帧。 我现在唯一的解决方案是以 30fps 的速度读取缓冲区,这样它会快速清理缓冲区并且不会出现更严重的延迟。

是否有任何其他可能的解决方案,以便我可以在每次启动相机时手动清洁/冲洗缓冲区?

【问题讨论】:

为什么要限制为 20fps?你在工作线程中等待吗? 是在 cv::VideoCapture 中缓冲你自己的或其他东西吗? @mirosval,是的,我这样做是因为我不想要太多的 cpu... video_capture.set(cv2.CAP_PROP_POS_FRAMES, 0) 在每次video_capture.read() 调用之前帮助我使用 Python 3、OpenCV 4.2 和 GStreamer 从 USB 摄像头获取最新帧。而 CAP_PROP_BUFFERSIZE 给出 GStreamer unhandled property 警告 在每个video_capture.read() 之前设置video_capture.set(cv2.CAP_PROP_POS_FRAMES,0) 实际上使我的视频流更加滞后... 【参考方案1】:

使用 Python 在 Maarten 的回答中实现 Hackaround 2。它启动一个线程并将来自 camera.read() 的最新帧保存为类属性。类似的策略可以在 c++ 中完成

import threading
import cv2

# Define the thread that will continuously pull frames from the camera
class CameraBufferCleanerThread(threading.Thread):
    def __init__(self, camera, name='camera-buffer-cleaner-thread'):
        self.camera = camera
        self.last_frame = None
        super(CameraBufferCleanerThread, self).__init__(name=name)
        self.start()

    def run(self):
        while True:
            ret, self.last_frame = self.camera.read()

# Start the camera
camera = cv2.VideoCapture(0)

# Start the cleaning thread
cam_cleaner = CameraBufferCleanerThread(camera)

# Use the frame whenever you want
while True:
    if cam_cleaner.last_frame is not None:
        cv2.imshow('The last frame', cam_cleaner.last_frame)
    cv2.waitKey(10)

【讨论】:

使用 threading.Lock() 同步访问 last_frame 会更安全。【参考方案2】:

如果您使用 GStreamer 管道,则可以选择删除旧缓冲区。 appsink drop=true option “缓冲区队列满时丢弃旧缓冲区”。在我的特殊情况下,实时流处理过程中存在延迟(不时),因此每次VideoCapture.read 调用都需要获取最新帧。

#include <chrono>
#include <thread>

#include <opencv4/opencv2/highgui.hpp>

static constexpr const char * const WINDOW = "1";

void video_test() 
    // It doesn't work properly without `drop=true` option
    cv::VideoCapture video("v4l2src device=/dev/video0 ! videoconvert ! videoscale ! videorate ! video/x-raw,width=640 ! appsink drop=true", cv::CAP_GSTREAMER);

    if(!video.isOpened()) 
        return;
    

    cv::namedWindow(
        WINDOW,
        cv::WINDOW_GUI_NORMAL | cv::WINDOW_NORMAL | cv::WINDOW_KEEPRATIO
    );
    cv::resizeWindow(WINDOW, 700, 700);

    cv::Mat frame;
    const std::chrono::seconds sec(1);
    while(true) 
        if(!video.read(frame)) 
            break;
        
        std::this_thread::sleep_for(sec);
        cv::imshow(WINDOW, frame);
        cv::waitKey(1);
    

【讨论】:

【参考方案3】:

如果您知道相机的帧速率,则可以使用此信息(即每秒 30 帧)来抓取帧,直到获得较低的帧速率。 它之所以有效,是因为如果抓取功能被延迟(即比标准帧速率获得更多时间来抓取一帧),这意味着您将每一帧都放在缓冲区中,opencv 需要等待下一帧来自相机。

while(True):
    prev_time=time.time()
    ref=vid.grab()
    if (time.time()-prev_time)>0.030:#something around 33 FPS
        break
ret,frame = vid.retrieve(ref)

【讨论】:

prev_time=time.time() 应该移到 while 循环之外【参考方案4】:

OpenCV 解决方案

根据this源,可以设置cv::VideoCapture对象的buffersize。

cv::VideoCapture cap;
cap.set(CV_CAP_PROP_BUFFERSIZE, 3); // internal buffer will now store only 3 frames

// rest of your code...

但是有一个重要的限制:

CV_CAP_PROP_BUFFERSIZE 存储在内部缓冲存储器中的帧数(注意:目前仅支持 DC1394 v 2.x 后端

从 cmets 更新。在较新版本的 OpenCV(3.4+)中,限制似乎消失了,代码使用范围枚举:

cv::VideoCapture cap;
cap.set(cv::CAP_PROP_BUFFERSIZE, 3);

解决方法 1

如果解决方案不起作用,请查看 this post,其中说明了如何解决该问题。

简而言之:查询一帧所需的时间是衡量的;如果它太低,则意味着该帧是从缓冲区中读取的并且可以被丢弃。继续查询帧,直到测量的时间超过某个限制。发生这种情况时,缓冲区为空并且返回的帧是最新的。

(链接帖子上的答案显示:从缓冲区返回帧大约需要返回最新帧的时间的 1/8。当然,您的里程可能会有所不同!)


解决方法 2

受this 帖子启发的另一种解决方案是创建第三个线程,以高速连续抓取帧以保持缓冲区为空。此线程应使用cv::VideoCapture.grab() 以避免开销。

您可以使用简单的自旋锁在真正的工作线程和第三个线程之间同步阅读帧。

【讨论】:

我实际上想知道有没有办法告诉我缓冲区是否为空而不是测量时间。很不方便…… cv::VideoCapture 接口不允许您获取该信息。另一种解决方案是创建一个不同的线程,以高速连续抓取帧(使用cv::VideoCapture.grab() 函数)。这将确保真正的工作线程读取下一帧时缓冲区为空(当然,在读取帧时不要忘记同步这些线程)。 谢谢,这就是我现在正在做的事情。 不幸的是,该常量似乎不在 Python opencv 中:运行 [thing for thing in dir(cv) if thing.find("CAP_")&gt;-1 ] “仅 DC1394 v 2.x 后端支持”是什么意思?那是一种相机吗?【参考方案5】:

伙计们,这是非常愚蠢和讨厌的解决方案,但由于某些原因,接受的答案并没有帮助我。 (python中的代码,但本质很清楚)

# vcap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
data = np.zeros((1140, 2560))
image = plt.imshow(data)

while True:
    vcap = cv2.VideoCapture("rtsp://admin:@192.168.3.231")
    ret, frame = vcap.read()
    image.set_data(frame)
    plt.pause(0.5) # any other consuming operation
    vcap.release()

【讨论】:

cv2.VideoCapture("rtsp://admin:@192.168.3.231") 每次都会新建一个对象,这会很慢【参考方案6】:

您可以确保抓取框架需要一些时间。编码很简单,虽然有点不可靠;此代码可能会导致死锁。

#include <chrono>
using clock = std::chrono::high_resolution_clock;
using duration_float = std::chrono::duration_cast<std::chrono::duration<float>>;
// ...
while (1) 
    TimePoint time_start = clock::now();
    camera.grab();
    if (duration_float(clock::now() - time_start).count() * camera.get(cv::CAP_PROP_FPS) > 0.5) 
        break;
    

camera.retrieve(dst_image);

代码使用C++11。

【讨论】:

根据docs该功能的主要用途是在多摄像头的环境中,尤其是在摄像头没有硬件同步的情况下。也就是说,您为每个摄像头调用 VideoCapture::grab(),然后调用较慢的方法 VideoCapture::retrieve() 来解码并从每个摄像头获取帧。这样就消除了去马赛克或运动 jpeg 解压缩等的开销,并且从不同相机检索到的帧将在时间上更接近。 那不是解决办法。但我赞成。

以上是关于由于捕获缓冲区,OpenCV VideoCapture 滞后的主要内容,如果未能解决你的问题,请参考以下文章

带有 IP 摄像机的 OpenCV 问题

使用opencv线程捕获空白图像

QT6 C++ 中音频捕获原始数据的选项[关闭]

java glReadpixels 到 OpenCV 垫

无法在 android 上使用 C++ OpenCV 打开相机

用于视频处理的图像缓冲区 [关闭]