【问题标题】:How to decode a video (memory file / byte string) and step through it frame by frame in python?如何解码视频(内存文件/字节字符串)并在python中逐帧遍历它?
【发布时间】:2020-06-18 20:31:49
【问题描述】:

我正在使用python做一些基本的图像处理,并想扩展它以逐帧处理视频。

我从服务器获取视频作为 blob - .webm 编码 - 并将其作为字节字符串 (b'\x1aE\xdf\xa3\xa3B\x86\x81\x01B\xf7\x81\x01B\xf2\x81\x04B\xf3\x81\x08B\x82\x88matroskaB\x87\x81\x04B\x85\x81\x02\x18S\x80g\x01\xff\xff\xff\xff\xff\xff\xff\x15I\xa9f\x99*\xd7\xb1\x83\x0fB@M\x80\x86ChromeWA\x86Chrome\x16T\xaek\xad\xae\xab\xd7\x81\x01s\xc5\x87\x04\xe8\xfc\x16\t^\x8c\x83\x81\x01\x86\x8fV_MPEG4/ISO/AVC\xe0\x88\xb0\x82\x02\x80\xba\x82\x01\xe0\x1fC\xb6u\x01\xff\xff\xff\xff\xff\xff ...) 在 python 中保存。

我知道有cv.VideoCapture,几乎可以满足我的需求。问题是我必须先将文件写入磁盘,然后再次加载。将字符串包装起来似乎更干净,例如,放入 IOStream,并将其提供给执行解码的某个函数。

有没有一种干净的方法可以在 python 中执行此操作,或者正在写入磁盘并再次加载它?

【问题讨论】:

    标签: python string opencv video


    【解决方案1】:

    根据this的帖子,您不能使用cv.VideoCapture在内存流中进行解码。
    您可以通过“管道”将流解码到FFmpeg

    解决方案有点复杂,写入磁盘要简单得多,而且可能是更清洁的解决方案。

    我正在发布一个使用 FFmpeg(和 FFprobe)的解决方案。
    FFmpeg 有 Python 绑定,但解决方案是使用 subprocess 模块将 FFmpeg 作为外部应用程序执行。
    (Python 绑定在 FFmpeg 上运行良好,但到 FFprobe 的管道却不行)。
    我使用的是Windows 10,我将ffmpeg.exeffprobe.exe放在了执行文件夹中(你也可以设置执行路径)。
    对于 Windows,请下载最新的(静态喜欢的)稳定版本。

    我创建了一个执行以下操作的独立示例:

    • 生成合成视频,并将其保存到 WebM 文件(用作测试的输入)。
    • 将文件作为二进制数据读入内存(将其替换为来自服务器的 blob)。
    • 将二进制流通过管道传输到 FFprobe,以查找视频分辨率。
      如果事先知道分辨率,您可以跳过此部分。
      通过管道连接到 FFprobe 使解决方案变得比应有的复杂。
    • 将二进制流通过管道传输到 FFmpeg stdin 进行解码,并从 stdout 管道中读取解码后的原始帧。
      使用 Python 线程分块写入 stdin
      (使用stdinstdout 而不是命名管道的原因是为了与Windows 兼容。

    管道架构:

     --------------------  Encoded      ---------  Decoded      ------------
    | Input WebM encoded | data        | ffmpeg  | raw frames  | reshape to |
    | stream (VP9 codec) | ----------> | process | ----------> | NumPy array|
     --------------------  stdin PIPE   ---------  stdout PIPE  -------------
    

    代码如下:

    import numpy as np
    import cv2
    import io
    import subprocess as sp
    import threading
    import json
    from functools import partial
    import shlex
    
    # Build synthetic video and read binary data into memory (for testing):
    #########################################################################
    width, height = 640, 480
    sp.run(shlex.split('ffmpeg -y -f lavfi -i testsrc=size={}x{}:rate=1 -vcodec vp9 -crf 23 -t 50 test.webm'.format(width, height)))
    
    with open('test.webm', 'rb') as binary_file:
        in_bytes = binary_file.read()
    #########################################################################
    
    
    # https://stackoverflow.com/questions/5911362/pipe-large-amount-of-data-to-stdin-while-using-subprocess-popen/14026178
    # https://stackoverflow.com/questions/15599639/what-is-the-perfect-counterpart-in-python-for-while-not-eof
    # Write to stdin in chunks of 1024 bytes.
    def writer():
        for chunk in iter(partial(stream.read, 1024), b''):
            process.stdin.write(chunk)
        try:
            process.stdin.close()
        except (BrokenPipeError):
            pass  # For unknown reason there is a Broken Pipe Error when executing FFprobe.
    
    
    # Get resolution of video frames using FFprobe
    # (in case resolution is know, skip this part):
    ################################################################################
    # Open In-memory binary streams
    stream = io.BytesIO(in_bytes)
    
    process = sp.Popen(shlex.split('ffprobe -v error -i pipe: -select_streams v -print_format json -show_streams'), stdin=sp.PIPE, stdout=sp.PIPE, bufsize=10**8)
    
    pthread = threading.Thread(target=writer)
    pthread.start()
    
    pthread.join()
    
    in_bytes = process.stdout.read()
    
    process.wait()
    
    p = json.loads(in_bytes)
    
    width = (p['streams'][0])['width']
    height = (p['streams'][0])['height']
    ################################################################################
    
    
    # Decoding the video using FFmpeg:
    ################################################################################
    stream.seek(0)
    
    # FFmpeg input PIPE: WebM encoded data as stream of bytes.
    # FFmpeg output PIPE: decoded video frames in BGR format.
    process = sp.Popen(shlex.split('ffmpeg -i pipe: -f rawvideo -pix_fmt bgr24 -an -sn pipe:'), stdin=sp.PIPE, stdout=sp.PIPE, bufsize=10**8)
    
    thread = threading.Thread(target=writer)
    thread.start()
    
    
    # Read decoded video (frame by frame), and display each frame (using cv2.imshow)
    while True:
        # Read raw video frame from stdout as bytes array.
        in_bytes = process.stdout.read(width * height * 3)
    
        if not in_bytes:
            break  # Break loop if no more bytes.
    
        # Transform the byte read into a NumPy array
        in_frame = (np.frombuffer(in_bytes, np.uint8).reshape([height, width, 3]))
    
        # Display the frame (for testing)
        cv2.imshow('in_frame', in_frame)
    
        if cv2.waitKey(100) & 0xFF == ord('q'):
            break
    
    if not in_bytes:
        # Wait for thread to end only if not exit loop by pressing 'q'
        thread.join()
    
    try:
        process.wait(1)
    except (sp.TimeoutExpired):
        process.kill()  # In case 'q' is pressed.
    ################################################################################
    
    cv2.destroyAllWindows()
    

    备注:

    • 如果您收到类似“找不到文件:ffmpeg...”的错误,请尝试使用完整路径。
      例如(在 Linux 中):'/usr/bin/ffmpeg -i pipe: -f rawvideo -pix_fmt bgr24 -an -sn pipe:'

    【讨论】:

    • 我有一个有关使用streamlink 从直播中读取二进制数据的相关问题。我试图遵循您的思路并使用提供的代码来读取这样的流,但我无法做到。
    • 我对@9​​87654336@ 没有任何经验。我能想到的一个主要问题是,在从直播中读取二进制数据时,知道帧 widthheight
    • 看来我有点过火了,我没有尝试直接将链接传递给某些 API,而是尝试首先读取它的二进制文件,这是不必要的。 streamlink 只是提供了一个 HSL 广播信息,可以读取到二进制字符串 - 但似乎也可以作为源文件 URL 传递。
    猜你喜欢
    • 1970-01-01
    • 2016-02-28
    • 1970-01-01
    • 2013-01-18
    • 1970-01-01
    • 2010-09-18
    • 2014-04-02
    • 2022-01-16
    • 1970-01-01
    相关资源
    最近更新 更多