OpenCV和Python多线程-在VideoCapture对象中寻找

时间:2018-10-04 22:06:08

标签: python multithreading opencv

我一直在研究一个Python应用程序,该应用程序使用OpenCV读取视频中的帧并创建“活动”(即从一帧变为另一帧的事物)的组合。为此,我真的只想每秒检查一帧。

很长一段时间以来,我一直在使用以下代码(为简化起见,简化了一些错误检查,删除了类,等等)来获取视频对象和第一帧:

video_capture = cv2.VideoCapture(video_fullpath)
this_frame = get_frame(0)

def get_frame(time):
    video_capture.set(cv2.CAP_PROP_POS_MSEC, time)
    capture_success, this_frame = video_capture.read()
    return this_frame

使用上面的后两行代码来获取后续帧的过程确实很慢。在2015年的MacBook Pro上,每帧的获取时间为0.3-0.4秒(视频中的间隔为1秒,这是一个约100MB的.mp4视频文件)。相比之下,我将其余各帧与其前一帧进行比较的其余操作非常快-通常少于0.01s。

因此,我一直在研究多线程,但我一直在努力。

我可以在“超前”的基础上进行多线程工作,即在处理一帧的同时,我可以获取下一帧。处理完前一帧后,我将等待“超前”操作完成后再继续。我使用以下代码来做到这一点:

while True:
    this_frame, next_frame_thread = get_frame_async(prev_frame.time + time_increment)
    << do processing of this_frame ... >>
    next_frame_thread.join()

def get_frame_async(time):
    if time not in frames:
        frames[time] = get_frame(time)
    next_frame_thread = Thread(target=get_frame, args=(time,))
    next_frame_thread.start()
    return frames[time], next_frame_thread

上面的方法似乎可行,但是因为与其他操作相比搜索操作是如此缓慢,因此实际上并没有节省太多时间-实际上很难看到任何好处。

然后我想知道是否可以并行获取多个帧。但是,每当我尝试尝试时,都会遇到一系列错误,大部分与async_lock有关(例如Assertion fctx->async_lock failed at libavcodec/pthread_frame.c:155)。我想知道这是否仅仅是OpenCV VideoCapture对象不能一次定位到多个位置……这似乎是合理的。但是,如果是这样,是否有任何方法可以大大加快此操作的速度?

我一直在使用一些不同的来源,包括显示https://nrsyed.com/2018/07/05/multithreading-with-opencv-python-to-improve-video-processing-performance/的一个巨大的提速方法,但是我在为为什么在async_lock周围出现这些错误而感到困惑。仅仅是搜索操作吗?在搜索视频时,我找不到任何多线程示例,而只是人们顺序阅读所有帧的示例。

最欢迎在哪里/哪些零件最有可能从多线程(或另一种方法)中受益的任何提示或指南。这是我第一次尝试多线程,因此完全接受我可能错过了一些显而易见的事情!基于此页面(https://www.toptal.com/python/beginners-guide-to-concurrency-and-parallelism-in-python),可用的各种选项让我有些不知所措。

谢谢!

2 个答案:

答案 0 :(得分:2)

基于对原始问题的评论,我进行了一些测试,认为值得分享(有趣的)结果。使用OpenCV的VideoCapture.set(CAP_PROP_POS_MSEC)VideoCapture.set(CAP_PROP_POS_FRAMES)的任何人都可以节省大量资金。

我比较了三个选项,做了一些分析:

1。通过寻求时间来获取框架:

frames = {}
def get_all_frames_by_ms(time):
    while True:
        video_capture.set(cv2.CAP_PROP_POS_MSEC, time)
        capture_success, frames[time] = video_capture.read()
        if not capture_success:
            break
        time += 1000

2。通过寻求框架号来获取框架:

frames = {}
def get_all_frames_by_frame(time):
    while True:
        # Note my test video is 12.333 FPS, and time is in milliseconds
        video_capture.set(cv2.CAP_PROP_POS_FRAMES, int(time/1000*12.333))
        capture_success, frames[time] = video_capture.read()
        if not capture_success:
            break
        time += 1000

3。通过全部抓取来获取帧,但只检索我想要的单音:

def get_all_frames_in_order():
    prev_time = -1
    while True:
        grabbed = video_capture.grab()
        if grabbed:
            time_s = video_capture.get(cv2.CAP_PROP_POS_MSEC) / 1000
            if int(time_s) > int(prev_time):
                # Only retrieve and save the first frame in each new second
                self.frames[int(time_s)] = video_capture.retrieve()
            prev_time = time_s
        else:
            break

通过这三种方法运行,计时(每次运行三个)如下:

  1. 33.78s 29.65s 29.24s
  2. 31.95s 29.16s 28.35s
  3. 11.81s 10.76s 11.73s

在每种情况下,它都以1秒的间隔将100帧保存到.mp4视频文件中的字典中,其中每帧是3072x1728图像。全部安装在配备2.9 GHz Intel Core i5和8GB RAM的2015 MacBookPro上。

到目前为止的结论...如果您只想从视频中检索某些帧,那么非常值得一看的是按顺序遍历所有帧并抓取它们,但是仅检索您感兴趣的那些-阅读的另一种选择(一次性获取和检索)。使我的速度提高了将近3倍。

在此基础上,我还重新考虑了多线程。我有两个测试过程,一个是获取框架,另一个是在框架可用时对其进行处理:

frames = {}

def get_all_frames_in_order():
    prev_time = -1
    while True:
        grabbed = video_capture.grab()
        if grabbed:
            time_s = video_capture.get(cv2.CAP_PROP_POS_MSEC) / 1000
            if int(time_s) > int(prev_time):
                # Only retrieve and save the first frame in each new second
                frames[int(time_s)] = video_capture.retrieve()
            prev_time = time_s
        else:
            break

def process_all_frames_as_available(processing_time):
    prev_time = 0
    while True:
        this_time = prev_time + 1000
        if this_time in frames and prev_time in frames:
            # Dummy processing loop - just sleeps for specified time
            sleep(processing_time)
            prev_time += self.time_increment
            if prev_time + self.time_increment > video_duration:
                break
        else:
            # If the frames aren't ready yet, wait a short time before trying again
            sleep(0.02)

对于此测试,我随后一个接一个地调用它们(依次为单线程),或使用以下多线程代码:

get_frames_thread = Thread(target=get_all_frames_in_order)
get_frames_thread.start()
process_frames_thread = Thread(target=process_all_frames_as_available, args=(0.02,))
process_frames_thread.start()
get_frames_thread.join()
process_frames_thread.join()

基于此,我现在很高兴多线程有效地工作并且节省了大量时间。我分别为上述两个函数生成了计时,然后在单线程和多线程模式下共同生成了计时。结果如下(括号中的数字是每帧“处理”所花费的时间,以秒为单位,在这种情况下,这只是一个虚拟/延迟):

get_all_frames_in_order - 2.99s

Process time = 0.02s per frame:
process_all_frames_as_available - 0.97s
single-threaded - 3.99s
multi-threaded - 3.28s

Process time = 0.1s per frame:
process_all_frames_as_available - 4.31s
single-threaded - 7.35s
multi-threaded - 4.46s

Process time = 0.2s per frame:
process_all_frames_as_available - 8.52s
single-threaded - 11.58s
multi-threaded - 8.62s

您可以看到,多线程结果非常好。从本质上讲,并行执行两个功能所需的时间仅比完全独立运行的两个功能中的较慢时间慢约0.2s。

希望对某人有帮助!

答案 1 :(得分:1)

巧合的是,我已经解决了类似的问题,并且我创建了一个python库(更多是薄包装器)来读取视频。该库称为mydia

使用OpenCV。它使用FFmpeg作为读取和处理视频的后端。

mydia支持自定义帧选择,帧大小调整,灰度转换等。可以查看文档here

因此,如果您想每秒选择N帧(在您的情况下,N = 1),则可以使用以下代码:

import numpy as np
from mydia import Videos

video_path = "path/to/video"

def select_frames(total_frames, num_frames, fps, *args):
    """This function will return the indices of the frames to be captured"""
    N = 1
    t = np.arange(total_frames)
    f = np.arange(num_frames)
    mask = np.resize(f, total_frames)

    return t[mask < N][:num_frames].tolist()

# Let's assume that the duration of your video is 120 seconds
# and you want 1 frame for each second 
# (therefore, setting `num_frames` to 120)
reader = Videos(num_frames=120, mode=select_frames)

video = reader.read(video_path)  # A video tensor/array

最好的部分是在内部仅读取所需的那些帧,因此该过程要快得多(我相信您正在寻找的东西)。

mydia的安装非常简单,可以here进行查看。

这可能会有轻微的学习曲线,但是我相信这正是您所需要的。

此外,如果您有多个视频,则可以使用多个工作人员并行阅读它们。例如:

from mydia import Videos

path = "path/to/video"
reader = Videos()
video = reader.read(path, workers=4)

根据您的CPU,这可以显着提高速度。

希望这会有所帮助!