由于捕获缓冲区导致 OpenCV VideoCapture 滞后

OpenCV VideoCapture lag due to the capture buffer

我正在通过提供 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 秒以上的延迟。 原因是拍摄的视频先存到一个buffer.When 我先开相机,buffer 是累积的但是我没有把帧读出来。所以如果我从缓冲区读取它总是给我旧帧。 我现在唯一的解决方案是以 30fps 的速度读取缓冲区,这样它会快速清理缓冲区并且没有更严重的延迟。

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

OpenCV 解决方案

根据 this 来源,您可以设置 cv::VideoCapture 对象的缓冲区大小。

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 Amount of frames stored in internal buffer memory (note: only supported by DC1394 v 2.x backend currently)

根据评论更新。在较新版本的 OpenCV (3.4+) 中,限制似乎已经消失,代码使用范围枚举:

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

Hackaround 1

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

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

(链接 post 上的答案显示:从缓冲区返回帧所用时间大约是返回最新帧所用时间的 1/8。当然,您的情况可能会有所不同!)


Hackaround 2

一个不同的解决方案,灵感来自 this post, is to create a third thread that grabs frames continuously at high speed to keep the buffer empty. This thread should use the cv::VideoCapture.grab() 以避免开销。

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

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

#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。

伙计们,这是非常愚蠢和令人讨厌的解决方案,但由于某些原因,接受的答案对我没有帮助。 (代码在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()

如果您知道相机的帧率,您可以使用此信息(即每秒 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)

如果您使用 GStreamer 管道,可以选择删除旧缓冲区。 appsink drop=trueoption"Drops old buffers when the buffer queue is filled"。在我的特殊情况下,在实时流处理期间存在延迟(不时),因此需要在每次 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);
    }
}

Maarten 的回答中使用 Python 实现了 Hackaround 2。它启动一个线程并将来自 camera.read() 的最新帧保存为 class 属性。可以在 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)