使用 OpenCV 对视频进行 Alpha 混合

sd7*_*d70 3 python opencv alphablending lag

我想使用 alpha 视频将一个视频混合到另一个视频之上。这是我的代码。它工作得很好,但问题是这段代码根本没有效率,这是因为/255部分原因。它很慢并且有滞后问题。

有没有标准和有效的方法来做到这一点?我希望结果是实时的。谢谢

import cv2
import numpy as np

def main():
    foreground = cv2.VideoCapture('circle.mp4')
    background = cv2.VideoCapture('video.MP4')
    alpha = cv2.VideoCapture('circle_alpha.mp4')

    while foreground.isOpened():
        fr_foreground = foreground.read()[1]/255
        fr_background = background.read()[1]/255     
        fr_alpha = alpha.read()[1]/255

        cv2.imshow('My Image',cmb(fr_foreground,fr_background,fr_alpha))

        if cv2.waitKey(1) == ord('q'): break

    cv2.destroyAllWindows

def cmb(fg,bg,a):
    return fg * a + bg * (1-a)

if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

Dan*_*šek 6

让我们先解决一些明显的问题 -foreground.isOpened()即使在您到达视频结尾后也会返回 true,因此您的程序最终会在那时崩溃。解决方案是双重的。首先,VideoCapture在创建它们后立即测试所有 3 个实例,使用以下内容:

if not foreground.isOpened() or not background.isOpened() or not alpha.isOpened():
    print "Unable to open input videos."
    return
Run Code Online (Sandbox Code Playgroud)

这将确保所有这些都正确打开。下一部分是正确处理到达视频的结尾。这意味着要么检查 的两个返回值中的第一个read(),这是一个表示成功的布尔标志,要么测试框架是否为None

while True:
    r_fg, fr_foreground = foreground.read()
    r_bg, fr_background = background.read()
    r_a, fr_alpha = alpha.read()
    if not r_fg or not r_bg or not r_a:
        break # End of video
Run Code Online (Sandbox Code Playgroud)

此外,您似乎并没有真正打电话cv2.destroyAllWindows()-()失踪了。并不是说这真的很重要。


为了帮助调查和优化这一点,我添加了一些详细的计时,使用timeit模块和几个方便的功能

from timeit import default_timer as timer

def update_times(times, total_times):
    for i in range(len(times) - 1):
        total_times[i] += (times[i+1]-times[i]) * 1000

def print_times(total_times, n):
    print "Iterations: %d" % n
    for i in range(len(total_times)):
        print "Step %d: %0.4f ms" % (i, total_times[i] / n)
    print "Total: %0.4f ms" % (np.sum(total_times) / n)
Run Code Online (Sandbox Code Playgroud)

并修改main()函数以测量每个逻辑步骤所花费的时间——读取、缩放、混合、显示、waitKey。为此,我将部门拆分为单独的语句。我还做了一个小小的修改,使它在 Python 2.x 中也能工作(/255被解释为整数除法并产生错误的结果)。

times = [0.0] * 6
total_times = [0.0] * (len(times) - 1)
n = 0
while True:
    times[0] = timer()
    r_fg, fr_foreground = foreground.read()
    r_bg, fr_background = background.read()
    r_a, fr_alpha = alpha.read()
    if not r_fg or not r_bg or not r_a:
        break # End of video
    times[1] = timer()
    fr_foreground = fr_foreground / 255.0
    fr_background = fr_background / 255.0
    fr_alpha = fr_alpha / 255.0
    times[2] = timer()
    result = cmb(fr_foreground,fr_background,fr_alpha)
    times[3] = timer()
    cv2.imshow('My Image', result)
    times[4] = timer()
    if cv2.waitKey(1) == ord('q'): break
    times[5] = timer()
    update_times(times, total_times)
    n += 1

print_times(total_times, n)
Run Code Online (Sandbox Code Playgroud)

当我使用 1280x800 mp4 视频作为输入运行它时,我注意到它确实相当缓慢,而且它在我的 6 核机器上只使用了 15% 的 CPU。各部分时间安排如下:

Iterations: 1190
Step 0: 11.4385 ms
Step 1: 37.1320 ms
Step 2: 39.4083 ms
Step 3: 2.5488 ms
Step 4: 10.7083 ms
Total: 101.2358 ms
Run Code Online (Sandbox Code Playgroud)

这表明最大的瓶颈是缩放步骤和混合步骤。低 CPU 使用率也不是最理想的,但让我们首先关注容易实现的目标。


让我们看看我们使用的 numpy 数组的数据类型。read()为我们提供dtypenp.uint8-- 8 位无符号整数的数组。然而,浮点除法(如书面)将产生一个阵列dtypenp.float64- 64位浮点值。我们的算法并不真正需要这种级别的精度,所以我们最好只使用 32 位浮点数——这意味着如果任何操作被向量化,我们可能会在相同的情况下进行两倍的计算多少时间。

这里有两个选项。我们可以简单地将除数转换为np.float32,这将导致 numpy 给我们相同的结果dtype

fr_foreground = fr_foreground / np.float32(255.0)
fr_background = fr_background / np.float32(255.0)
fr_alpha = fr_alpha / np.float32(255.0)
Run Code Online (Sandbox Code Playgroud)

这给了我们以下时间:

Iterations: 1786
Step 0: 9.2550 ms
Step 1: 19.0144 ms
Step 2: 21.2120 ms
Step 3: 1.4662 ms
Step 4: 10.8889 ms
Total: 61.8365 ms
Run Code Online (Sandbox Code Playgroud)

或者我们可以np.float32先将数组转换为,然后就地进行缩放。

fr_foreground = np.float32(fr_foreground)
fr_background = np.float32(fr_background)
fr_alpha = np.float32(fr_alpha)

fr_foreground /= 255.0
fr_background /= 255.0
fr_alpha /= 255.0
Run Code Online (Sandbox Code Playgroud)

这给出了以下时序(将步骤 1 拆分为转换 (1) 和缩放 (2) - 其余移位 1):

Iterations: 1786
Step 0: 9.0589 ms
Step 1: 13.9614 ms
Step 2: 4.5960 ms
Step 3: 20.9279 ms
Step 4: 1.4631 ms
Step 5: 10.4396 ms
Total: 60.4469 ms
Run Code Online (Sandbox Code Playgroud)

两者大致相同,运行时间约为原始时间的 60%。我将坚持使用第二个选项,因为它将在后面的步骤中变得有用。让我们看看还有什么可以改进的。


从前面的时序我们可以看出,缩放不再是瓶颈,但脑子里还是冒出一个想法——除法通常比乘法慢,那么如果我们乘以一个倒数呢?

fr_foreground *= 1/255.0
fr_background *= 1/255.0
fr_alpha *= 1/255.0
Run Code Online (Sandbox Code Playgroud)

事实上,这确实让我们获得了一毫秒——没什么了不起的,但它很容易,所以不妨跟着它:

Iterations: 1786
Step 0: 9.1843 ms
Step 1: 14.2349 ms
Step 2: 3.5752 ms
Step 3: 21.0545 ms
Step 4: 1.4692 ms
Step 5: 10.6917 ms
Total: 60.2097 ms
Run Code Online (Sandbox Code Playgroud)

现在混合函数是最大的瓶颈,其次是所有 3 个数组的类型转换。如果我们看一下混合操作的作用:

foreground * alpha + background * (1.0 - alpha)
Run Code Online (Sandbox Code Playgroud)

我们可以观察到,要使数学起作用,唯一需要在 (0.0, 1.0) 范围内的值是alpha

如果我们只缩放 alpha 图像怎么办?另外,由于乘以浮点数会提升为浮点数,如果我们也跳过类型转换怎么办?这意味着cmb()必须返回np.uint8数组

def cmb(fg,bg,a):
    return np.uint8(fg * a + bg * (1-a))
Run Code Online (Sandbox Code Playgroud)

我们会有

    #fr_foreground = np.float32(fr_foreground)
    #fr_background = np.float32(fr_background)
    fr_alpha = np.float32(fr_alpha)

    #fr_foreground *= 1/255.0
    #fr_background *= 1/255.0
    fr_alpha *= 1/255.0
Run Code Online (Sandbox Code Playgroud)

这个时间是

Step 0: 7.7023 ms
Step 1: 4.6758 ms
Step 2: 1.1061 ms
Step 3: 27.3188 ms
Step 4: 0.4783 ms
Step 5: 9.0027 ms
Total: 50.2840 ms
Run Code Online (Sandbox Code Playgroud)

显然,第 1 步和第 2 步要快得多,因为我们只完成了 1/3 的工作。imshow也加快了速度,因为它不必从浮点转换。令人费解的是,读取也变得更快了(我想我们正在避免一些幕后重新分配,因为fr_foreground并且fr_background始终包含原始帧)。我们确实付出了额外演员的代价cmb(),但总的来说,这似乎是一场胜利——我们的时间是原来的 50%。


继续,让我们摆脱该cmb()功能,将其功能移至main()并拆分以衡量每个操作的成本。让我们也尝试重用以下结果alpha.read()(因为我们最近看到了read()性能的改进):

times = [0.0] * 11
total_times = [0.0] * (len(times) - 1)
n = 0
while True:
    times[0] = timer()
    r_fg, fr_foreground = foreground.read()
    r_bg, fr_background = background.read()
    r_a, fr_alpha_raw = alpha.read()
    if not r_fg or not r_bg or not r_a:
        break # End of video

    times[1] = timer()
    fr_alpha = np.float32(fr_alpha_raw)
    times[2] = timer()
    fr_alpha *= 1/255.0
    times[3] = timer()
    fr_alpha_inv = 1.0 - fr_alpha
    times[4] = timer()
    fr_fg_weighed = fr_foreground * fr_alpha
    times[5] = timer()
    fr_bg_weighed = fr_background * fr_alpha_inv
    times[6] = timer()
    sum = fr_fg_weighed + fr_bg_weighed
    times[7] = timer()
    result = np.uint8(sum)
    times[8] = timer()
    cv2.imshow('My Image', result)
    times[9] = timer()
    if cv2.waitKey(1) == ord('q'): break
    times[10] = timer()
    update_times(times, total_times)
    n += 1
Run Code Online (Sandbox Code Playgroud)

新时间:

Iterations: 1786
Step 0: 6.8733 ms
Step 1: 5.2742 ms
Step 2: 1.1430 ms
Step 3: 4.5800 ms
Step 4: 7.0372 ms
Step 5: 7.0675 ms
Step 6: 5.3082 ms
Step 7: 2.6912 ms
Step 8: 0.4658 ms
Step 9: 9.6966 ms
Total: 50.1372 ms
Run Code Online (Sandbox Code Playgroud)

我们没有真正获得任何东西,但读取速度明显加快。


这引出了另一个想法——如果我们尝试最小化分配并在后续迭代中重用数组怎么办?

numpy.zeros_like在我们读取第一组帧之后,我们可以在第一次迭代(使用)中预先分配必要的数组:

if n == 0: # Pre-allocate
    fr_alpha = np.zeros_like(fr_alpha_raw, np.float32)
    fr_alpha_inv = np.zeros_like(fr_alpha_raw, np.float32)
    fr_fg_weighed = np.zeros_like(fr_alpha_raw, np.float32)
    fr_bg_weighed = np.zeros_like(fr_alpha_raw, np.float32)
    sum = np.zeros_like(fr_alpha_raw, np.float32)
    result = np.zeros_like(fr_alpha_raw, np.uint8)
Run Code Online (Sandbox Code Playgroud)

现在,我们可以使用

我们还可以使用单个numpy.multiply.

times = [0.0] * 10
total_times = [0.0] * (len(times) - 1)
n = 0
while True:
    times[0] = timer()
    r_fg, fr_foreground = foreground.read()
    r_bg, fr_background = background.read()
    r_a, fr_alpha_raw = alpha.read()
    if not r_fg or not r_bg or not r_a:
        break # End of video

    if n == 0: # Pre-allocate
        fr_alpha = np.zeros_like(fr_alpha_raw, np.float32)
        fr_alpha_inv = np.zeros_like(fr_alpha_raw, np.float32)
        fr_fg_weighed = np.zeros_like(fr_alpha_raw, np.float32)
        fr_bg_weighed = np.zeros_like(fr_alpha_raw, np.float32)
        sum = np.zeros_like(fr_alpha_raw, np.float32)
        result = np.zeros_like(fr_alpha_raw, np.uint8)

    times[1] = timer()
    np.multiply(fr_alpha_raw, np.float32(1/255.0), fr_alpha)
    times[2] = timer()
    np.subtract(1.0, fr_alpha, fr_alpha_inv)
    times[3] = timer()
    np.multiply(fr_foreground, fr_alpha, fr_fg_weighed)
    times[4] = timer()
    np.multiply(fr_background, fr_alpha_inv, fr_bg_weighed)
    times[5] = timer()
    np.add(fr_fg_weighed, fr_bg_weighed, sum)
    times[6] = timer()
    np.copyto(result, sum, 'unsafe')
    times[7] = timer()
    cv2.imshow('My Image', result)
    times[8] = timer()
    if cv2.waitKey(1) == ord('q'): break
    times[9] = timer()
    update_times(times, total_times)
    n += 1
Run Code Online (Sandbox Code Playgroud)

这给了我们以下时间:

Iterations: 1786
Step 0: 7.0515 ms
Step 1: 3.8839 ms
Step 2: 1.9080 ms
Step 3: 4.5198 ms
Step 4: 4.3871 ms
Step 5: 2.7576 ms
Step 6: 1.9273 ms
Step 7: 0.4382 ms
Step 8: 7.2340 ms
Total: 34.1074 ms
Run Code Online (Sandbox Code Playgroud)

我们修改的所有步骤都有显着改进。我们减少了原始实现所需时间的约 35%。


小更新:

根据Silencer回答,我也进行了测量cv2.convertScaleAbs。它实际上运行得更快一点:

Step 6: 1.2318 ms
Run Code Online (Sandbox Code Playgroud)

这给了我另一个想法——我们可以利用cv2.add它让我们指定目标数据类型并进行饱和转换。这将允许我们将步骤 5 和 6 结合在一起。

cv2.add(fr_fg_weighed, fr_bg_weighed, result, dtype=cv2.CV_8UC3)
Run Code Online (Sandbox Code Playgroud)

出来在

Step 5: 3.3621 ms
Run Code Online (Sandbox Code Playgroud)

又一次小胜(以前我们大约是 3.9 毫秒)。

在此之后,cv2.subtract并且cv2.multiply是进一步的候选人。我们需要使用一个 4 元素元组来定义一个标量(Python 绑定的复杂性),我们需要显式地定义乘法的输出数据类型。

    cv2.subtract((1.0, 1.0, 1.0, 0.0), fr_alpha, fr_alpha_inv)
    cv2.multiply(fr_foreground, fr_alpha, fr_fg_weighed, dtype=cv2.CV_32FC3)
    cv2.multiply(fr_background, fr_alpha_inv, fr_bg_weighed, dtype=cv2.CV_32FC3)
Run Code Online (Sandbox Code Playgroud)

时间:

Step 2: 2.1897 ms
Step 3: 2.8981 ms
Step 4: 2.9066 ms
Run Code Online (Sandbox Code Playgroud)

这似乎是我们在没有一些并行化的情况下所能达到的。我们已经利用了 OpenCV 在单个操作方面可能提供的任何优势,因此我们应该专注于流水线我们的实现。

为了帮助我弄清楚如何在不同的流水线阶段(线程)之间划分代码,我制作了一个图表,其中显示了所有操作、我们对它们的最佳时间以及计算的相互依赖关系:

在此处输入图片说明

在我写这篇文章时,WIP查看评论以获取更多信息。