更快地将动画 webp 转换为 mp4

Convert animated webp to mp4 faster

四处寻找后,我看到有人 suggested this for a animated webp to webm. But that seemed cumbersome. So I made this to convert a animated webp to mp4 or webm, which I have live here。它需要一些将 gif 转换为视频的逻辑并应用它。问题是,它需要一点时间。

我想知道是否有人对如何提高速度有建议?

import os
import moviepy.video.io.ImageSequenceClip

def analyseImage(path):
    '''
    Pre-process pass over the image to determine the mode (full or additive).
    Necessary as assessing single frames isn't reliable. Need to know the mode
    before processing all frames.
    '''
    im = PIL.Image.open(path)
    results = {
        'size': im.size,
        'mode': 'full',
    }
    try:
        while True:
            if im.tile:
                tile = im.tile[0]
                update_region = tile[1]
                update_region_dimensions = update_region[2:]
                if update_region_dimensions != im.size:
                    results['mode'] = 'partial'
                    break
            im.seek(im.tell() + 1)
    except EOFError:
        pass
    return results


def processImage(path):
    '''
    Iterate the animated image extracting each frame.
    '''
    images = []
    mode = analyseImage(path)['mode']

    im = PIL.Image.open(path)

    i = 0
    p = im.getpalette()
    last_frame = im.convert('RGBA')

    try:
        while True:
            print("saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile))

            '''
            If the GIF uses local colour tables, each frame will have its own palette.
            If not, we need to apply the global palette to the new frame.
            '''
            if '.gif' in path:
                if not im.getpalette():
                    im.putpalette(p)

            new_frame = PIL.Image.new('RGBA', im.size)

            '''
            Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
            If so, we need to construct the new frame by pasting it on top of the preceding frames.
            '''
            if mode == 'partial':
                new_frame.paste(last_frame)

            new_frame.paste(im, (0, 0), im.convert('RGBA'))
            nameoffile = path.split('/')[-1]
            output_folder = path.replace(nameoffile, '')

            name = '%s%s-%d.png' % (output_folder, ''.join(os.path.basename(path).split('.')[:-1]), i)
            print(name)
            new_frame.save(name, 'PNG')
            images.append(name)
            i += 1
            last_frame = new_frame
            im.seek(im.tell() + 1)
    except EOFError:
        pass
    return images



def webp_mp4(filename, outfile):
    images = processImage("%s" % filename)
    fps = 30
    clip = moviepy.video.io.ImageSequenceClip.ImageSequenceClip(images, fps=fps)
    clip.write_videofile(outfile)
    return [outfile]

webp_mp4(filename, outfile)

它目前是如何工作的,是当你 运行 webp_mp4(filename, outfile) 它调用 processImage 调用 analyseImage 时。最后这一切都很好。只是想要它更快。

视频转码通常是一项“令人尴尬的并行”任务,processImage是在一个大序列中做事。如果 processImage 是最慢的部分,您可以使用 multiprocessing.Pool and assign each worker (which can run on a separate CPU core) its own range of frames to process. PIL objects aren't pickle-able,因此您将不得不编写临时文件,您似乎已经在这样做了。

我对PIL不是很了解,所以如果有更好的方法来使用这个库,我就不去看了。也许保存每一帧,因为 PNG 很慢;值得尝试 TIF 或 JPEG。 (我会自己尝试,但我的 Python 安装没有在这台笔记本电脑上设置。)

    threads
      Number of threads to use for ffmpeg. Can speed up the writing of
      the video on multicore computers.

* clip.write_videofile(outfile, threads=4)

您可以设置线程数以提高写入速度。

查看代码的一些优化建议:

  • 您在 analyseImageprocessImage 中都从磁盘读取图像。这不是什么大事,因为每个图像只发生一次,但是从磁盘读取仍然是一个相对较慢的操作,所以最好不要做不必要的事情。相反,您可以在 processImage 中打开并将打开的图像传递给 analyseImage

  • 您处理部分帧的代码比它需要做的更多:

    new_frame = PIL.Image.new('RGBA', im.size)
    if mode == 'partial':
        new_frame.paste(last_frame)
    new_frame.paste(im, (0, 0), im.convert('RGBA'))
    

    (一张新图片,两次粘贴,一次转换)

    可能会变成

    im = im.convert("RGBA")
    if mode == 'partial':
        new_frame = last_frame
        new_frame.paste(im, (0, 0), im)
    else:
        new_frame = im
    

    (无新图,一贴一转化)

  • 我怀疑最大的减速是将每一帧写入磁盘,然后再次读取它们。最好将它们保存在内存中(理想情况下,一次只保存一个)。

    imageiomoviepy 内部使用的库,提供了一种方法来做到这一点。您只需使用 imageio.get_writer(path) 创建一个视频编写器,然后使用 writer.append_data(frame_data).

    按顺序向其添加帧

    我自己试过了,但最后我弄混了颜色,所以我没有工作代码可以提供。不过作为提示,您可以将 PIL 图像转换为 imageio 期望的原始帧数据,例如 numpy.array(im).

一切都记在心里。 ImageSequenceClip accepts NumPy arrays for the frames. Instead of saving the frames to files, convert each frame to a NumPy arrayyield 他们来自 processImage.

这应该会显着提高性能。完成此改进后,您可以查看 编码为 mp4 是否提供了进一步的改进。

或者您可以 append 将帧发送给作者(例如,在 for 循环中),如 .

添加此功能可删除用于创建 MP4 文件后的图像。

def processImage2(path):
'''
Iterate the animated image extracting each frame.
'''
images = []
mode = analyseImage(path)['mode']

im = Image.open(path)

i = 0
p = im.getpalette()
last_frame = im.convert('RGBA')

try:
    while True:
        print("saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile))

        '''
        If the GIF uses local colour tables, each frame will have its own palette.
        If not, we need to apply the global palette to the new frame.
        '''
        if '.gif' in path:
            if not im.getpalette():
                im.putpalette(p)

        new_frame = Image.new('RGBA', im.size)

        '''
        Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
        If so, we need to construct the new frame by pasting it on top of the preceding frames.
        '''
        if mode == 'partial':
            new_frame.paste(last_frame)

        new_frame.paste(im, (0, 0), im.convert('RGBA'))
        nameoffile = path.split('/')[-1]
        output_folder = path.replace(nameoffile, '')

        name = '%s%s-%d.png' % (output_folder, ''.join(os.path.basename(path).split('.')[:-1]), i)
        print(name)
        os.remove(name)
        i += 1
        last_frame = new_frame
        im.seek(im.tell() + 1)
except EOFError:
    pass

只需通过添加一行 "processImage2("%s" % filename)" 来编辑此函数,就可以了。

def webp_mp4(filename, outfile):
images = processImage("%s" % filename)
fps = 30
clip = moviepy.video.io.ImageSequenceClip.ImageSequenceClip(images, fps=fps)
clip.write_videofile(outfile)

processImage2("%s" % filename)
return [outfile]