【问题标题】:Alpha Blending using OpenCV for videos使用 OpenCV 对视频进行 Alpha 混合
【发布时间】:2018-05-08 19:33:25
【问题描述】:

我想使用 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()

【问题讨论】:

  • 尝试分别测量读取时间和处理时间,以确定需要改进的地方。
  • 我猜你使用的是 Python 3.x,因为 /255 在 2.x 中的工作方式不同...

标签: python opencv lag alphablending


【解决方案1】:

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

if not foreground.isOpened() or not background.isOpened() or not alpha.isOpened():
    print "Unable to open input videos."
    return

这将确保所有这些都正确打开。下一部分是正确处理到达视频的结尾。这意味着要么检查 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

此外,您似乎并没有真正打电话给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)

并修改了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)

当我以 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

这表明最大的瓶颈是缩放步骤和混合步骤。低 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)

这给了我们以下时间:

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

或者我们可以先将数组转换为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

这给出了以下时序(将步骤 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

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


从之前的时序中,我们可以看到缩放不再是瓶颈,但还是想到了一个想法——除法一般比乘法慢,那么如果我们乘以倒数呢?

fr_foreground *= 1/255.0
fr_background *= 1/255.0
fr_alpha *= 1/255.0

确实,这确实为我们赢得了一毫秒的时间——没有什么了不起的,但它很容易,所以不妨试试吧:

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

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

foreground * alpha + background * (1.0 - alpha)

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

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

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

我们会的

    #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

这个时间是

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

显然,第 1 步和第 2 步要快得多,因为我们只完成了 1/3 的工作。 imshow 也加快了速度,因为它不必从浮点转换。令人费解的是,读取速度也变得更快(我想我们正在避免一些内部重新分配,因为fr_foregroundfr_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

新的时间安排:

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

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


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

在我们读取第一组帧之后,我们可以在第一次迭代中预先分配必要的数组(使用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)

现在,我们可以使用

我们还可以将第 1 步和第 2 步合并在一起,使用单个 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

这给了我们以下时间:

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

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


小更新:

基于Silenceranswer 我也测量了cv2.convertScaleAbs。它实际上运行得更快一些:

Step 6: 1.2318 ms

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

cv2.add(fr_fg_weighed, fr_bg_weighed, result, dtype=cv2.CV_8UC3)

出来了

Step 5: 3.3621 ms

又一次取得了一些胜利(之前我们大约是 3.9 毫秒)。

从这里开始,cv2.subtractcv2.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)

时间安排:

Step 2: 2.1897 ms
Step 3: 2.8981 ms
Step 4: 2.9066 ms

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

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

WIP 在我写这篇文章时,请查看 cmets 了解更多信息。

【讨论】:

  • 并行化明天来,有点晚了。
  • 非常感谢 @DanMašek。播放 FPS 是 5,现在是 16。但是至少应该是 24,但我认为并行化可以提高这段代码的速度。我期待您对并行化的回复
  • @DanMašek 第 2 部分在哪里??!!
  • @JeruLuke 感谢您的关注(我阅读了您的个人资料,谢谢)。准备和编写它需要相当多的时间,而且我还有一些真正的工作要做。从技术上讲,我在早上的某个糟糕的时间写了那条评论,所以它仍然是“明天”......直到 2 小时前:D |无论如何,我刚刚达到每帧 10 毫秒的速度,waitKey 是一个瓶颈(Windows 内部有sleep),所以为了保持可视化,跳过了大约 30% 的处理帧。现在正在写一些关于它的连贯文字,我会及时通知你。 :) | pastebin.com/EzCy6bQK
  • 如果多线程的软件混合在 4K 视频中得到回报,我会很高兴吗?这就是我的方法开始受到影响的地方,因为两层传输了巨大的 58MB。
【解决方案2】:

如果它只是混合、渲染和忘记,那么在 GPU 上执行它是有意义的。其中,VTK (Visualization ToolKit) (https://www.vtk.org) 可以代替imshow 为您执行此操作。 VTK 已从 OpenCV 3D Visualizer-module (https://docs.opencv.org/3.2.0/d1/d19/group__viz.html) 中获知,因此不应添加太多依赖项。

此后,整个计算部分(不包括读取视频帧)归结为 cv2.mixChannels 并将像素数据传输到两个渲染器,在我的计算机上,对于 1280x720 视频,每次迭代大约需要 5 毫秒。

import sys
import cv2
import numpy as np
import vtk
from vtk.util import numpy_support
import time

class Renderer:
    # VTK renderer with two layers
    def __init__( self ):
        self.layer1 = vtk.vtkRenderer()
        self.layer1.SetLayer(0)
        self.layer2 = vtk.vtkRenderer()
        self.layer2.SetLayer(1)
        self.renWin = vtk.vtkRenderWindow()
        self.renWin.SetNumberOfLayers( 2 )
        self.renWin.AddRenderer(self.layer1)
        self.renWin.AddRenderer(self.layer2)
        self.iren = vtk.vtkRenderWindowInteractor()
        self.iren.SetRenderWindow(self.renWin)
        self.iren.Initialize()      
    def Render( self ):
        self.iren.Render()

# set background image to a given renderer (resets the camera)
# from https://www.vtk.org/Wiki/VTK/Examples/Cxx/Images/BackgroundImage
def SetBackground( ren, image ):    
    bits = numpy_support.numpy_to_vtk( image.ravel() )
    bits.SetNumberOfComponents( image.shape[2] )
    bits.SetNumberOfTuples( bits.GetNumberOfTuples()/bits.GetNumberOfComponents() )

    img = vtk.vtkImageData()
    img.GetPointData().SetScalars( bits );
    img.SetExtent( 0, image.shape[1]-1, 0, image.shape[0]-1, 0,0 );
    origin = img.GetOrigin()
    spacing = img.GetSpacing()
    extent = img.GetExtent()

    actor = vtk.vtkImageActor()
    actor.SetInputData( img )

    ren.RemoveAllViewProps()
    ren.AddActor( actor )
    camera = vtk.vtkCamera()
    camera.ParallelProjectionOn()
    xc = origin[0] + 0.5*(extent[0] + extent[1])*spacing[0]
    yc = origin[1] + 0.5*(extent[2] + extent[3])*spacing[1]
    yd = (extent[3] - extent[2] + 1)*spacing[1]
    d = camera.GetDistance()
    camera.SetParallelScale(0.5*yd)
    camera.SetFocalPoint(xc,yc,0.0)
    camera.SetPosition(xc,yc,-d)
    camera.SetViewUp(0,-1,0)
    ren.SetActiveCamera( camera )
    return img

# update the scalar data without bounds check
def UpdateImageData( vtkimage, image ):
    bits = numpy_support.numpy_to_vtk( image.ravel() )
    bits.SetNumberOfComponents( image.shape[2] )
    bits.SetNumberOfTuples( bits.GetNumberOfTuples()/bits.GetNumberOfComponents() )
    vtkimage.GetPointData().SetScalars( bits );

r = Renderer()
r.renWin.SetSize(1280,720)
cap = cv2.VideoCapture('video.mp4')
image = cv2.imread('hello.png',1)
alpha = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY )
ret, alpha = cv2.threshold( alpha, 127, 127, cv2.THRESH_BINARY )
alpha = np.reshape( alpha, (alpha.shape[0],alpha.shape[1], 1 ) )

src1=[]
src2=[]
overlay=[]
c=0
while ( 1 ):
    # read the data
    ret, mat = cap.read()
    if ( not ret ):
        break
    #TODO ret, image = cap2.read() #(rgb)
    #TODO ret, alpha = cap3.read() #(mono)

    # alpha blend
    t=time.time()
    if ( overlay==[] ):
        overlay = np.zeros( [image.shape[0],image.shape[1],4], np.uint8 ) 
    cv2.mixChannels( [image, alpha], [overlay], [0,0,1,1,2,2,3,3] )
    if ( src1==[] ):
        src1 = SetBackground( r.layer1, mat )
    else:
        UpdateImageData( src1, mat )
    if ( src2==[] ):
        src2 = SetBackground( r.layer2, overlay )
    else:
        UpdateImageData( src2, overlay )
    r.Render()
    # blending done
    t = time.time()-t;

    if ( c % 10 == 0 ):
        print 1000*t
    c = c+1;

【讨论】:

  • 很好,尽管如果您像 OP 那样使用 3 个输入视频会更有意义——即使同一文件的 3 个副本也会产生有意义的结果(与自身混合),并且还会给出你有意义的时间。
  • @DanMašek 但我在每次迭代时都做mixChannels,就好像每次迭代都更改了 alpha 和覆盖图像一样。时间只用于 alpha 混合,因为我的硬盘性能不是很有趣,是吗?此外,我必须使用 MJPG-avi;其中三个会消耗我所有的帧时间才能开始:)
  • 谢谢@mainactual 到目前为止我还没有使用VTK,应该阅读它以了解您的代码。再次感谢
  • @sd70 试试看 :) 或者,如果您最喜欢的 UI 库支持 RGBA 格式和 z 顺序(例如 Qt QGraphicsScene),它同样会在 mixChannels 之后支持步骤。
  • @mainactual 公平点,虽然硬盘位在你运行一次后消失并且数据被操作系统缓存(至少这是我在这里观察到的)。
【解决方案3】:

我正在使用OpenCV 4.00-prePython 3.6

  1. 无需执行三个xxx/255 操作。仅用于 Alpha 版即可。
  2. 注意类型转换,除了np.uint8(xxx)np.copyto(xxx,yyy, "unsafe") 之外,更喜欢cv2.convertScaleAbs(xxx)
  3. 预分配内存应该更好。

我使用#2,即cv2.convertScaleAbs 来避免underflow/overflow,范围在[0,255]。例如:

>>> x = np.array([[-1,256]])
>>> y = np.uint8(x)
>>> z = cv2.convertScaleAbs(x)
>>> x
array([[ -1, 256]])
>>> y
array([[255,   0]], dtype=uint8)
>>> z
array([[  1, 255]], dtype=uint8)

##! 2018/05/09 13:54:34

import cv2
import numpy as np
import time

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

def test2():
    cap = cv2.VideoCapture(0)
    ret, prev_frame = cap.read()
    """
    foreground = cv2.VideoCapture('circle.mp4')
    background = cv2.VideoCapture('video.MP4')
    alphavideo = cv2.VideoCapture('circle_alpha.mp4')
    """
    while cap.isOpened():
        ts = time.time()
        ret, fg = cap.read()
        alpha = fg.copy()
        bg = prev_frame
        """
        ret, fg = foreground.read()
        ret, bg = background.read()
        ret, alpha = alphavideo.read()
        """

        alpha = np.multiply(alpha, 1.0/255)
        blended = cv2.convertScaleAbs(cmb(fg, bg, alpha))
        te = time.time()
        dt = te-ts
        fps = 1/dt
        print("{:.3}ms, {:.3} fps".format(1000*dt, fps))
        cv2.imshow('Blended', blended)

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

    cv2.destroyAllWindows()

if __name__ == "__main__":
    test2()

一些输出是这样的:

39.0ms, 25.6 fps
37.0ms, 27.0 fps
38.0ms, 26.3 fps
37.0ms, 27.0 fps
38.0ms, 26.3 fps
37.0ms, 27.0 fps
38.0ms, 26.3 fps
37.0ms, 27.0 fps
37.0ms, 27.0 fps
37.0ms, 27.0 fps
37.0ms, 27.0 fps
38.0ms, 26.3 fps
37.0ms, 27.0 fps
37.0ms, 27.0 fps
37.0ms, 27.0 fps
37.0ms, 27.0 fps
...

【讨论】:

  • 谢谢@silencer 我按照你说的重写了我的代码,但是 FPS 是 6 并且速度很慢。这是我的新代码:code.py
  • 第 2 点背后的原因是什么? (我没想过检查这个)
  • 我使用#2,即cv2.convertScaleAbs来避免underflow/overflow,范围在[0, 255]。
  • @Silencer 公平点,虽然我认为这里没有危险。尽管如此,它似乎表现得更好一些,并且让我尝试了一些其他的东西,所以 +1。
猜你喜欢
  • 1970-01-01
  • 2020-04-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-26
  • 1970-01-01
  • 2011-07-23
  • 1970-01-01
相关资源
最近更新 更多