OpenCV 和 Python 多线程 - 在 VideoCapture 对象中搜索

Posted

技术标签:

【中文标题】OpenCV 和 Python 多线程 - 在 VideoCapture 对象中搜索【英文标题】:OpenCV & Python Multithreading - Seeking within a VideoCapture Object 【发布时间】:2018-10-04 22:06:08 【问题描述】:

我一直在开发一个 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.01 秒。

因此,我一直在研究多线程,但我正在苦苦挣扎。

我可以让多线程在“前瞻”的基础上工作,即当我处理一帧时,我可以获得下一帧。一旦我完成了前一帧的处理,我将等待“前瞻”操作完成,然后再继续。我用以下代码做到这一点:

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-&gt;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 周围遇到这些错误而苦苦挣扎。仅仅是seek操作吗?我在寻找视频时找不到任何多线程示例 - 只是人们按顺序阅读所有帧的示例。

任何关于哪里/哪些部分最有可能从多线程(或其他方法)中受益的提示或指导都将受到欢迎。这是我第一次尝试多线程,所以完全接受我可能错过了一些明显的东西!根据这个页面 (https://www.toptal.com/python/beginners-guide-to-concurrency-and-parallelism-in-python),我对可用的不同选项的范围有点不知所措。

谢谢!

【问题讨论】:

把这个过程分成两部分怎么样:(1)将视频帧转换为图像文件,(2)处理图像?我认为并行化这些过程更容易 尝试在没有任何定位的情况下尽可能快地从视频中读取连续帧,您会发现确实非常快。我建议您阅读所有帧,但如果您的视频为 30 fps 而您只想要 1 fps,则丢弃 30 帧中的 29 帧。 您知道拍摄到特定帧而不是特定时间是否更快? IE。 POS_FRAMES 而不是 POS_MSEC?此外,为了搭载@MarkSetchell 的想法,您只能在您真正想要该帧时使用read(),否则您可以只使用grab(),它不会花费时间来解码帧(因为您只是要无论如何都要扔掉它)。或者同样地,只是grab()所有帧,然后只有retrieve()每隔多少帧抓取的帧。 感谢 cmets。今天下午我会尝试一些变体,看看每个变体的时间安排如何。我会报告我的结果。 【参考方案1】:

基于原始问题的 cmets,我进行了一些测试,并认为值得分享(有趣的)结果。任何使用 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

运行这三种方法,时间安排(从每三种运行)如下:

    33.78s 29.65s 29.24s 31.95s 29.16s 28.35s 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.2 秒,而这两个函数完全分开运行的速度较慢。

希望对某人有所帮助!

【讨论】:

我认为您的代码中存在毫秒错误。您将秒数放入 frames 并增加 this_time = prev_time + 1000 并使用 this_time in frames【参考方案2】:

巧合的是,我曾研究过类似的问题,并且我创建了一个用于阅读视频的 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。

这可能有一点学习曲线,但我相信它正是您正在寻找的。​​p>

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

from mydia import Videos

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

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

希望这会有所帮助!!

【讨论】:

以上是关于OpenCV 和 Python 多线程 - 在 VideoCapture 对象中搜索的主要内容,如果未能解决你的问题,请参考以下文章

opencv python 多线程视频捕获

OpenCV C++ 多线程加速

使用多线程调试 DLL (/MDd) C 运行时库构建静态 Opencv 库

Python多线程总结

Python—多线程文件名称查找

机器视觉行业实践技巧