opencv 如何解码一个视频(内存文件/字节串),并在python中一帧一帧地浏览它?

pcww981p  于 2022-12-13  发布在  Python
关注(0)|答案(3)|浏览(225)

我正在使用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,它几乎可以完成我所需要的。问题是我必须先将文件写入磁盘,然后再加载它。将字符串 Package 到IOStream中,并将其馈送到某个进行解码的函数中,看起来会更干净。
在python中有没有一种干净的方法来完成这一任务,或者是写入磁盘并再次加载它才是可行的方法?

6psbrbz9

6psbrbz91#

根据this帖子,您不能使用cv.VideoCapture在内存流中进行解码。
您可以通过“管道”将流解码为FFmpeg
解决方案有点复杂,而写入磁盘要简单得多,而且可能是更干净的解决方案。
我正在发布一个使用FFmpeg(和FFprobe)的解决方案。
有针对FFmpeg的Python绑定,但解决方案是使用subprocess模块将FFmpeg作为外部应用程序执行。
(The Python绑定在FFmpeg上运行良好,但到FFprobe的管道却不行)。
我使用的是Windows 10,我将ffmpeg.exeffprobe.exe放在执行文件夹中(您也可以设置执行路径)。
对于Windows,请下载最新的(静态喜欢的)稳定版本。
我创建了一个独立的示例,它执行以下操作:

  • 生成合成视频,并将其保存到WebM文件(用作测试的输入)。
  • 将文件作为二进制数据读入内存(用服务器中的blob替换它)。
  • 将二进制流传输到FFprobe,以查找视频分辨率。

如果事先知道解决方案,您可以跳过此部分。
FFprobe的管道使解决方案比它应该具有的更复杂。

  • 将二进制流通过管道传输到FFmpeg stdin进行解码,并从stdout管道读取解码后的原始帧。

写入stdin使用Python线程分块完成。
(The使用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()

备注:

  • 如果您收到类似 “file not found(未找到文件)的错误:ffmpeg...",请尝试使用完整路径。

例如(在Linux中):'/usr/bin/ffmpeg -i pipe: -f rawvideo -pix_fmt bgr24 -an -sn pipe:'

cyej8jka

cyej8jka2#

在Rotem写下答案两年后,现在有一种更干净/更简单的方法使用ImageIO来完成此任务。

  • 注意:假设路径中有ffmpeg,您可以使用以下命令生成测试视频以尝试此示例:ffmpeg -f lavfi -i testsrc=duration=10:size=1280x720:rate=30 testsrc.webm*
import imageio.v3 as iio
from pathlib import Path

webm_bytes = Path("testsrc.webm").read_bytes()

# read all frames from the bytes string
frames = iio.imread(webm_bytes, index=None, format_hint=".webm")
frames.shape
# Output:
#    (300, 720, 1280, 3)

for frame in iio.imiter(webm_bytes, format_hint=".webm"):
    print(frame.shape)

# Output:
#    (720, 1280, 3)
#    (720, 1280, 3)
#    (720, 1280, 3)
#    ...

要使用它,您需要ffmpeg后端(它实现了一个类似于Rotem所提出的解决方案):pip install imageio[ffmpeg]
在回应Rotem的评论有点解释:
上面的代码片段使用imageio==2.16.0。v3 API是一个即将推出的面向用户的API,它简化了阅读操作。该API从imageio==2.10.0开始可用,但是,在2.16.0之前的版本中,您必须使用import imageio as iio,并使用iio.v3.imiteriio.v3.imread
读取视频字节的能力已经永远存在了(〉5年,而且还在计数),但(正如我刚刚意识到的)从来没有直接记录下来...所以我很快就会为此添加一个PR ™:)
在ImageIO(v2 API)的旧版本(在v2.9.0上测试)上,您仍然可以读取视频字节字符串;但是,这稍微有点冗长:

import imageio as iio
import numpy as np
from pathlib import Path

webm_bytes = Path("testsrc.webm").read_bytes()

# read all frames from the bytes string
frames = np.stack(iio.mimread(webm_bytes, format="FFMPEG", memtest=False))

# iterate over frames one by one
reader = iio.get_reader(webm_bytes, format="FFMPEG")
for frame in reader:
    print(frame.shape)
reader.close()
watbbzwu

watbbzwu3#

有一种Python方法可以通过使用decord包来完成此操作。

import io
from decord import VideoReader

# This is the bytes object of your video.
video_str 

# Load video
file_obj = io.BytesIO(video_str)
container = decord.VideoReader(file_obj)

# Get the total number of video frames
len(container)
# Access the NDarray of the (i+1)-th frame 
container[i]

您可以在decord github repo中了解有关decord的更多信息。
您可以在mmaction repo中了解有关视频IO的更多信息。请参阅DecordInit了解如何使用decord IO。

相关问题