OpenCV实时流视频捕获速度较慢。如何丢帧或与实时同步?

38

目标和问题

我想搭建一个opencv系统来处理HLS流或RMTP流,但是,我遇到了一个奇怪的问题,即帧率降低和累积的延迟。就好像视频在流中应该出现的位置越来越远。

我正在寻找一种方法,即使意味着丢帧,也能跟上实时来源的进度。

当前方法

import cv2

cap = cv2.VideoCapture()
cap.open('https://videos3.earthcam.com/fecnetwork/9974.flv/chunklist_w1421640637.m3u8')

while (True):
    _, frame = cap.read()
    cv2.imshow("camCapture", frame)
    cv2.waitKey(1)

我已经在VLC上验证了流的质量,那里似乎工作得很好。

cv2速度

.

预期速度

问题:

  • 我在这里做错了什么?
  • 为什么它运行得这么慢?
  • 如何将其同步到实时速度?

1
在我的机器上,使用您的代码会得到加速的视频约~2秒钟。然后停顿大约2秒钟,重复。我尝试强制视频流显示最新的一帧 cap.set(cv2.CAP_PROP_POS_AVI_RATIO,1)。它似乎可以工作,但是我每隔<随机>秒才能获得一个帧。从print(cap.get(cv2.CAP_PROP_FPS))中得到的FPS为180,000.0,这太高了。但是如果您能够得到准确的FPS,也许可以通过多个cap.grab()的巧妙操作来提前到达应该在的流位置,然后再用单个的cap.retrieve来获取帧。 - bfris
我看到你已经有了在Python中进行视频采集的最小代码。因此,不可能改善运行时间。也许问题是由于视频分辨率,如果W和H大小很长,那么缓冲区读取和显示视频帧将更加延迟。一个简单的解决方案是在读取之前降低视频的分辨率。另一个解决方案是创建第二个线程仅用于视频采集。另一个解决方案是使用C++读取视频。我已经比较了性能,它大约快了3倍。 - Roger Figueroa Quintero
1
请注意,您的问题可能是硬件问题:内存和数据总线带宽以及处理器速度。 - Roger Figueroa Quintero
使用更快的设备(更高的处理能力)或使用硬件加速或优化代码来接收、解码和显示流。OpenCV可能不是接收、解码和渲染最快的库(但它非常易于使用)。 - Micka
4个回答

40

我的假设是,抖动很可能是由于网络限制而发生的,并且当帧包被丢弃时会发生。当帧被丢弃时,这会导致程序显示上一个“好”的帧,从而导致显示冻结。这可能是硬件或带宽问题,但我们可以通过软件来缓解一些问题。以下是一些可能的更改:

1. 设置最大缓冲区大小

我们使用 cv2.CAP_PROP_BUFFERSIZE 参数将cv2.videoCapture()对象设置为具有有限的缓冲区大小。这样做的想法是通过限制缓冲区,我们将始终拥有最新帧。这也可以帮助减轻帧随机跳跃的问题。

2. 设置帧检索延迟

目前,我认为read()读取太快了,尽管它在自己的专用线程中。这可能是所有帧似乎汇集并在下一帧突然爆发的原因之一。例如,在一个一秒钟的时间间隔内,它可能会产生15个新帧,但在下一个一秒钟的时间间隔内,只返回3个帧。这可能是由于网络包帧丢失,因此为了确保我们获得恒定的帧速率,我们只需在帧检索线程中添加延迟即可。延迟以获得大约〜30 FPS可以很好地“标准化”帧速率,并在发生数据包丢失时平滑过渡帧之间的转换。

注意:我们应该尝试匹配流的帧速率,但我不确定网络摄像头的FPS是多少,所以我猜测是30 FPS。此外,通常有一个“直接”流链接而不是通过中间Web服务器进行连接,这可以大大提高性能。


如果您尝试使用保存的.mp4视频文件,则会发现没有抖动。这证实了我的怀疑,问题很可能是由于网络延迟引起的。

from threading import Thread
import cv2, time

class ThreadedCamera(object):
    def __init__(self, src=0):
        self.capture = cv2.VideoCapture(src)
        self.capture.set(cv2.CAP_PROP_BUFFERSIZE, 2)
       
        # FPS = 1/X
        # X = desired FPS
        self.FPS = 1/30
        self.FPS_MS = int(self.FPS * 1000)
        
        # Start frame retrieval thread
        self.thread = Thread(target=self.update, args=())
        self.thread.daemon = True
        self.thread.start()
        
    def update(self):
        while True:
            if self.capture.isOpened():
                (self.status, self.frame) = self.capture.read()
            time.sleep(self.FPS)
            
    def show_frame(self):
        cv2.imshow('frame', self.frame)
        cv2.waitKey(self.FPS_MS)

if __name__ == '__main__':
    src = 'https://videos3.earthcam.com/fecnetwork/9974.flv/chunklist_w1421640637.m3u8'
    threaded_camera = ThreadedCamera(src)
    while True:
        try:
            threaded_camera.show_frame()
        except AttributeError:
            pass

相关摄像头/IP/RTSP/流媒体、FPS、视频、线程和多进程贴子

  1. Python OpenCV 摄像头流 - 多线程,时间戳

  2. 使用 OpenCV cv2.VideoCapture 在 Python 中从 IP 摄像头进行视频流传输

  3. 如何使用 OpenCV 捕获多个摄像头流?

  4. OpenCV 实时视频流捕获速度慢。如何丢帧或与实时同步?

  5. 使用 OpenCV VideoWriter 存储 RTSP 流为视频文件

  6. OpenCV 视频保存

  7. Python OpenCV 多进程 cv2.VideoCapture mp4


1
你能告诉我为什么需要等待(FPS同步)吗?难道缓冲区的目的不是捕获帧吗?感觉从缓冲区顶部获取什么并不重要。对我来说,建立人工延迟感觉很反直觉。这是关于线程中的资源共享还是其他什么原因吗?或者是图像不是实时到达,而是在能够重构或重新传递时到达。无论如何,非常感谢您详细的回答。这很有效。 - Conic
@Conic FPS同步是必要的,因为通常您希望与源进行同步。如果您轮询得太慢,则会有一段时间没有新的帧,因此您将显示旧帧。如果您轮询得太快(像以前一样),则会立即显示所有帧。在这两种情况下,这可能会导致抖动/冻结,因此通过插入延迟,您可以平均分配帧分布,并在其显示时进行调整。由于存在两个单独的线程,因此一旦接收到帧,它将立即显示,这也是我们需要延迟的另一个原因。 - nathancy
这里有一个类比,假设你正在以2倍速观看Youtube视频,但你使用的是拨号上网。缓冲速率将比实际播放的视频加载得更慢,因此最终帧将赶上缓冲区并且视频将会冻结。当FPS未同步时就会发生这种情况。如果我们恢复到1倍速,则帧将被平衡并匹配缓冲速率的速度,因此我们不会出现任何冻结/抖动。视频看起来就很流畅。 - nathancy
OpenCV VideoCapture完全没有轮询。您能解释一下您的意思吗? - Christoph Rackwitz
1
@BenZayed 如果只有一个摄像头,设置缓冲区大小和延迟应该就可以了,但如果同时运行多个摄像头,则可能需要使用线程(我使用此技术处理8个同时运行的摄像头流)。 - nathancy
显示剩余4条评论

7

尝试进行线程处理

我尝试使用这个解决方案,作者是nathancy,取得了一定的成功。

它包括:

  • 为图像捕获创建一个单独的线程
  • 仅使用主线程进行显示。

代码:

import cv2
from threading import Thread

class ThreadedCamera(object):
    def __init__(self, source = 0):

        self.capture = cv2.VideoCapture(source)

        self.thread = Thread(target = self.update, args = ())
        self.thread.daemon = True
        self.thread.start()

        self.status = False
        self.frame  = None

    def update(self):
        while True:
            if self.capture.isOpened():
                (self.status, self.frame) = self.capture.read()

    def grab_frame(self):
        if self.status:
            return self.frame
        return None  

if __name__ == '__main__':
    stream_link = "https://videos3.earthcam.com/fecnetwork/9974.flv/chunklist_w1421640637.m3u8"
    streamer = ThreadedCamera(stream_link)

    while True:
        frame = streamer.grab_frame()
        if frame is not None:
            cv2.imshow("Context", frame)
        cv2.waitKey(1) 

抖动的、但实时的结果

.

视频流可以正常工作并保持实时性。然而,所有帧似乎会积攒起来,然后突然爆发成视频。我希望有人能够解释这个现象。

改进空间

可以在这里找到实时流。

https://www.earthcam.com/usa/newyork/timessquare/?cam=tsstreet

使用Python的 streamlink 流采集器从该网站中获取了 m3u8 文件。


import streamlink

streams = streamlink.streams("https://www.earthcam.com/usa/newyork/timessquare/?cam=tsstreet")
print(streams)

这将产生:

OrderedDict([

('720p',<HLSStream('https://videos3.earthcam.com/fecnetwork/9974.flv/chunklist_w202109066.m3u8')>),

('live', <RTMPStream({'rtmp': 'rtmp://videos3.earthcam.com/fecnetwork/', 'playpath': '9974.flv', 'pageUrl': 'https://www.earthcam.com/usa/newyork/timessquare/?cam=tsstreet','swfUrl': 'http://static.earthcam.com/swf/streaming/stream_viewer_v3.swf', 'live': 'true'}, redirect=False>),

('worst', <HLSStream('https://videos3.earthcam.com/fecnetwork/9974.flv/chunklist_w202109066.m3u8')>),

('best', <RTMPStream({'rtmp': 'rtmp://videos3.earthcam.com/fecnetwork/', 'playpath': '9974.flv', 'pageUrl': 'https://www.earthcam.com/usa/newyork/timessquare/?cam=tsstreet', 'swfUrl': 'http://static.earthcam.com/swf/streaming/stream_viewer_v3.swf', 'live': 'true'}, redirect=False>)

])



流被错误读取的可能性。

你找到解决方案了吗? - Arunava
是的,我做到了。那个带有绿色勾号的答案是来自nathancy的。 - Conic
抱歉,我误解了您对nathancy的回答的评价。我以为您指的是这个问题的答案(没有检查链接)。无论如何,谢谢。 - Arunava
我能理解那可能会误导人。我指的是他在另一个Stack Overflow帖子中提供的不同解决方案。然后他回复了一个更新后的答案,其中包括FPS同步。最终那个解决方案对我起了作用。 - Conic
是的。谢谢 :) - Arunava

0
这对我来说有很大的改变(在Windows 11上,使用ELP USB2相机):
不再使用self.capture = cv2.VideoCapture(1) 而是使用self.capture = cv2.VideoCapture(1, cv2.CAP_DSHOW) 这使我能够使用所有的cv2.CAP_PROP_属性。

0

我建议您检查硬件与兼容的视频流编解码器是否匹配。我曾遇到过同样的问题,流式传输期间帧率仅降至5 fps,因为它默认使用的格式未被流式传输,所以它会将其转换,然后显示非常滞后(约1秒),也会有更低的fps。

请在适当的编解码器中使用Self.capture.set(cv2.CAP_PROP_FOURCC,cv2.VideoWriter_fourcc('M','J','P','G'))代替MJPG,并使用您的cv2.VideoCapture查看是否有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接