首页 > 解决方案 > 如何在恒定时间内从 H264 流中提取 JPEG 图像

问题描述

我想从磁盘上的 H264 流中提取 JPEG 帧。提取需要尽可能快地满足我的实时要求。

到目前为止,我一直在使用ffmpeg-pythonlib,它只是ffmpeg. 这是一个代码片段:

out, _ = (
    ffmpeg
    .input('./5sec.h264')
    .filter('select', 'gte(n,{})'.format(144))
    .output('pipe:', vframes=1, format='image2', vcodec='h264')
    .run(capture_stdout=True)
)

这会将 jpeg 输出到标准输出,通过一些努力,我可以将其读入我的程序中。

但是,随着我使用越来越大的流文件,获取 JPEG 的提取时间会增加。我认为查找时间会保持不变,因为ffmpeg高度优化?

是否有固定时间的解决方案来从磁盘上的 h264(甚至 mjpeg)格式流中查找和返回帧?

编辑:继承人我在没有 python 包装器的情况下使用的命令: ffmpeg -i 5sec.h264 -frames:v 1 -filter:v "select=gte(n\,25)" -f image2 frame.jpg

这是输出:

ffmpeg version 4.1.6-1~deb10u1+rpt2 Copyright (c) 2000-2020 the FFmpeg developers
  built with gcc 8 (Raspbian 8.3.0-6+rpi1)
  configuration: --prefix=/usr --extra-version='1~deb10u1+rpt2' --toolchain=hardened --incdir=/usr/include/arm-linux-gnueabihf --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-omx-rpi --enable-mmal --enable-neon --enable-rpi --enable-vout-drm --enable-v4l2-request --enable-libudev --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared --libdir=/usr/lib/arm-linux-gnueabihf --cpu=arm1176jzf-s --arch=arm
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
Input #0, h264, from '5sec.h264':
  Duration: N/A, bitrate: N/A
    Stream #0:0: Video: h264 (High), yuv420p(progressive), 640x480, 25 fps, 25 tbr, 1200k tbn, 50 tbc
Stream mapping:
  Stream #0:0 -> #0:0 (h264 (native) -> mjpeg (native))
Press [q] to stop, [?] for help
[swscaler @ 0x1a25390] deprecated pixel format used, make sure you did set range correctly
Output #0, image2, to 'frame.jpg':
  Metadata:
    encoder         : Lavf58.20.100
    Stream #0:0: Video: mjpeg, yuvj420p(pc), 640x480, q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc
    Metadata:
      encoder         : Lavc58.35.100 mjpeg
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: -1
frame=    1 fps=0.4 q=6.8 Lsize=N/A time=00:00:01.04 bitrate=N/A speed=0.467x    
video:63kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

请注意,实现的 FPS 为 0.4。当我将请求的帧增加到第 125 帧而不是第 25 帧时,FPS 下降到 0.1。

标签: pythonffmpegvideo-streamingvideo-processingh.264

解决方案


假设 H.264 流有一个容器文件,您可能会寻找帧的位置,而不是从文件开头读取所有帧。

不要使用select过滤器,而是使用-ss参数(input方法的参数)。

FFmpeg 命令行类似于以下内容:

ffmpeg -ss 14.4 -i in.mp4 -vframes 1 frame.jpg

为什么我们需要一个容器(比如 MP4)?

MP4 容器文件使用带有视频文件中每个关键帧位置的“索引”。
当我们需要查找文件中的特定时间时,我们可以跳到“时间”之前最近的关键帧,并解码剩余的几帧。

没有容器的原始 h264 流没有索引,因此我们无法进行查找。


首先创建一个示例 MP4 文件(用于使代码示例可重现)。

以下代码示例使用高级帧计数器构建 10Hz 的合成视频文件:

import ffmpeg

in_file_name = 'in.mp4'
fps = 10

# Build synthetic video, for testing - 1500 frames with advanced counder:
# Use g=50, for forcing a key frame every 50 frames (for testing).
(
    ffmpeg
    .input('testsrc=size=192x108:rate=1:duration=1500', f='lavfi', r=fps)
    .filter('setpts', 'N/{}/TB'.format(fps))
    .output(in_file_name, vcodec='libx264', g=50, crf=17, pix_fmt='yuv420p', loglevel='panic')
    .global_args('-hide_banner')
    .overwrite_output()
    .run()
)

select使用过滤器测量时间,并使用seek

该示例提取 frame1440而不是144,以便更好地显示时间差异。

import ffmpeg
import time

in_file_name = 'in.mp4'

fps = 10

extracted_frame = 1440

# Build synthetic video, for testing - 1500 frames with advanced counder:
# Use g=50, for forcing a key frame every 50 frames (for testing).
#(
#    ffmpeg
#    .input('testsrc=size=192x108:rate=1:duration=1500', f='lavfi', r=fps)
#    .filter('setpts', 'N/{}/TB'.format(fps))
#    .output(in_file_name, vcodec='libx264', g=50, crf=17, pix_fmt='yuv420p', loglevel='panic')
#    .global_args('-hide_banner')
#    .overwrite_output()
#    .run()
#)

start = time.time()
for i in range(10):  # Measure 10 iterations.
    # Extract the extracted_frame'th frame into frame.jpg
    (
        ffmpeg
        .input(in_file_name)
        .filter('select', 'gte(n,{})'.format(extracted_frame))
        .output('frame.jpg', vframes=1, loglevel='panic')
        .global_args('-hide_banner')
        .overwrite_output()
        .run()
    )
end = time.time()
print('Time using select filter: {}'.format(end - start))


start = time.time()
for i in range(10):  # Measure 10 iterations.
    # Extract the extracted_frame'th frame into frame.jpg
    (
        ffmpeg
        .input(in_file_name, ss='{}'.format(extracted_frame / fps))
        .output('frame.jpg', vframes=1, loglevel='panic')
        .global_args('-hide_banner')
        .overwrite_output()
        .run()
    )
end = time.time()
print('Time using seeking: {}'.format(end - start))

结果:
Time using select filter: 1.1203510761260986
Time using seeking: 0.36171388626098633


在这两种情况下frame.jpg显示1440
在此处输入图像描述


推荐阅读