FFmpeg 和 video4linux2 参数 - 如何更快地捕获静止图像?

FFmpeg and video4linux2 parameters - how to capture still images faster?

问题总结

我构建了一个 18 摄像头的 USB 网络摄像头阵列,连接到 Raspberry Pi 400 作为控制器。我的 Python 3.8 代码用于从每个网络摄像头捕获图像很慢,我正在努力寻找加快速度的方法。

FFMPEGvideo4linux2 命令行选项让我感到困惑,所以我不确定延迟是否是我的原因参数选择不当,一组更好的选项将解决问题。

目标

我正在尝试尽快从每个相机捕捉一张图像。

我正在使用 FFMPEGvideo4linux2 命令行选项来捕获所有相机循环中的每个图像,如下所示。

预期结果

我只想要每个相机的单个帧。帧速率为 30 fps,所以我预计捕获时间大约是第二个最坏情况的 1/30 到 1/10。但是性能计时器告诉我每次捕获需要 2-3 秒。

此外,我不太了解 ffmpeg 输出,但这个输出让我担心:

frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    1 fps=0.5 q=8.3 Lsize=N/A time=00:00:00.06 bitrate=N/A speed=0.0318x    
video:149kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing  

我不明白为什么“frame=”行重复了 4 次。在第 4 次重复中,fps 显示为 0.5,我会将其解释为每 2 秒一帧,而不是我指定的 30FPS。

具体问题:

任何人都可以向我解释这个 ffmpeg 输出的含义,以及为什么拍摄每张图像需要 2 秒,而不是接近 1/30 秒?

任何人都可以向我解释如何在更短的时间内捕获图像吗?

我是否应该为每个 ffmpeg 调用生成一个单独的线程,以便它们 运行 异步而不是串行?或者这在实践中不会真正节省时间吗?

实际结果

  Input #0, video4linux2,v4l2, from '/dev/video0':
  Duration: N/A, start: 6004.168748, bitrate: N/A
    Stream #0:0: Video: mjpeg, yuvj422p(pc, bt470bg/unknown/unknown), 1920x1080, 30 fps, 30 tbr, 1000k tbn, 1000k tbc
Stream mapping:
  Stream #0:0 -> #0:0 (mjpeg (native) -> mjpeg (native))
Press [q] to stop, [?] for help
Output #0, image2, to '/tmp/video1.jpg':
  Metadata:
    encoder         : Lavf58.20.100
    Stream #0:0: Video: mjpeg, yuvj422p(pc), 1920x1080, q=2-31, 200 kb/s, 30 fps, 30 tbn, 30 tbc
    Metadata:
      encoder         : Lavc58.35.100 mjpeg
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: -1
frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    1 fps=0.5 q=8.3 Lsize=N/A time=00:00:00.06 bitrate=N/A speed=0.0318x    
video:149kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

Captured /dev/video0 image in: 3 seconds
Input #0, video4linux2,v4l2, from '/dev/video2':
  Duration: N/A, start: 6007.240871, bitrate: N/A
    Stream #0:0: Video: mjpeg, yuvj422p(pc, bt470bg/unknown/unknown), 1920x1080, 30 fps, 30 tbr, 1000k tbn, 1000k tbc
Stream mapping:
  Stream #0:0 -> #0:0 (mjpeg (native) -> mjpeg (native))
Press [q] to stop, [?] for help
Output #0, image2, to '/tmp/video2.jpg':
  Metadata:
    encoder         : Lavf58.20.100
    Stream #0:0: Video: mjpeg, yuvj422p(pc), 1920x1080, q=2-31, 200 kb/s, 30 fps, 30 tbn, 30 tbc
    Metadata:
      encoder         : Lavc58.35.100 mjpeg
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: -1
frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    0 fps=0.0 q=0.0 size=N/A time=00:00:00.00 bitrate=N/A speed=   0x    
frame=    1 fps=0.5 q=8.3 Lsize=N/A time=00:00:00.06 bitrate=N/A speed=0.0318x    
video:133kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

Captured /dev/video2 image in: 3 seconds
...

代码:

list_of_camera_ids = ["/dev/video1","/dev/video2", "/dev/video3", "/dev/video4",
                      "/dev/video5","/dev/video6", "/dev/video7", "/dev/video8",
                      "/dev/video9","/dev/video10", "/dev/video11", "/dev/video12",
                      "/dev/video13","/dev/video14", "/dev/video15", "/dev/video16",
                      "/dev/video17","/dev/video18"
                     ]
for this_camera_id in list_of_camera_ids:
    full_image_file_name = '/tmp/' + os.path.basename(this_camera_id) + 'jpg'
    image_capture_tic = time.perf_counter()
    
    run_cmd = subprocess.run([
                              '/usr/bin/ffmpeg', '-y', '-hide_banner',
                              '-f', 'video4linux2',
                              '-input_format',  'mjpeg',
                              '-framerate', '30',
                              '-i', this_camera_id,
                              '-frames', '1',
                              '-f', 'image2',
                              full_image_file_name
                             ],
                             universal_newlines=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE
                            )  
         print(run_cmd.stderr)
         image_capture_toc = time.perf_counter()       
         print(f"Captured {camera_id} image in: {image_capture_toc - image_capture_tic:0.0f} seconds")

附加数据: 为了回应 Mark Setchell 的回答,即需要更多信息来回答这个问题,我现在在这里详细说明所要求的信息:

相机:相机是 USB-3 相机,它们将自己标识为:

idVendor           0x0bda Realtek Semiconductor Corp.
idProduct          0x5829 

我试图为其中一台摄像机添加冗长的 lsusb 转储,但此 post 超过了 30000 个字符的限制

相机的连接方式: Pi 的 USB 3 端口连接到主 USB-3 7 端口集线器,带有 3 个支线 7 端口集线器(并非支线中的所有端口集线器被占用)。

摄像头分辨率:高清格式1920x1080

如果我只想要一张图像,为什么要设置帧速率?

我设置的帧速率看起来很奇怪,因为它指定了帧之间的时间,但您只需要一个帧。我这样做是因为我不知道如何从 FFMPEG 获取单个图像。这是我在网络上发现的 FFMPEG 命令选项的一个示例,我可以成功地捕获单个图像。我发现了无数 不起作用的选项! 我写这篇 post 是因为我的网络搜索没有产生适合我的示例。我希望比我见多识广的人能告诉我一个行之有效的方法!

为什么我按顺序而不是并行扫描相机?

我这样做只是为了首先让事情变得简单,并且列表上的循环看起来很简单而且 pythonic。我很清楚,以后我可能能够为每个 FFMPEG 调用生成一个单独的线程,并可能以这种方式获得并行加速。的确,我欢迎举个例子来说明如何做到这一点。

但无论如何,单张图像拍摄花费 3 秒似乎太长了。

为什么我只使用了您 Raspberry Pi 上 4 个内核中的一个?

我 post 编辑的示例代码只是我整个程序的一个片段。图像捕获目前在子线程中进行,而带有事件循环的 Window GUI 在主线程中 运行ning,因此用户输入在成像过程中不会被阻塞。

我对 Raspberry Pi 400 的核心知识不够了解,也不了解 Raspberry Pi OS(又名 Raspbian)如何管理线程到核心的分配,也不是 Python 是否可以或应该明确指示线程在特定内核中 运行ning。
我欢迎 Mark Setchell(或任何其他了解这些问题的人)推荐最佳实践并包含示例代码。

这里有不少问题:

  • 你不说你用的是什么相机
  • 它们是如何连接的(USB 2/3 端口)? USB 集线器?
  • 也不是分辨率
  • 你设置的帧率看起来很奇怪,因为它指定了帧之间的时间,但你只想要一个帧
  • 您正在按顺序而不是并行扫描相机
  • 您只使用了 Raspberry Pi
  • 上 4 个内核中的一个

可以使用v4l2-ctl直接抓帧:

v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=1 --stream-to=output.jpg

您输入的内容已经是 JPG 格式,因此它将按原样捕获。

有关详细信息,请参阅 v4l2-ctl --help-streaming

首先感谢https://whosebug.com/users/1109017/llogan 谁在下面的评论中提供了我需要的线索。

我把这个解决方案记录在这里,方便那些可能不看评论的人发现。

这是我修改后的程序:

list_of_camera_ids = ["/dev/video1","/dev/video2", "/dev/video3", "/dev/video4",
                      "/dev/video5","/dev/video6", "/dev/video7", "/dev/video8",
                      "/dev/video9","/dev/video10", "/dev/video11", "/dev/video12",
                      "/dev/video13","/dev/video14", "/dev/video15", "/dev/video16",
                      "/dev/video17","/dev/video18"
                     ]
for this_camera_id in list_of_camera_ids:
    full_image_file_name = '/tmp/' + os.path.basename(this_camera_id) + 'jpg'
    image_capture_tic = time.perf_counter()
    
    run_cmd = subprocess.run([
                              'v4l2-ctl','-d',
                              this_camera_id,
                              '--stream-mmap', 
                              '--stream-count=1',
                              '--stream-to=' +
                              full_image_file_name,"&"
                             ],
                             universal_newlines=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE
                            )  
         print(run_cmd.stderr)
         image_capture_toc = time.perf_counter()       
         print(f"Captured {camera_id} image in: {image_capture_toc - image_capture_tic:0.0f} seconds")

补充说明: 这段代码大大加快了速度!

使用我以前的方法,每张图像需要 3-4 秒才能捕获。在原始 post 所示的序列化循环中,18 张图像通常需要 45 到 60 秒才能完成。

使用我修改后的代码,使用 llogan 的建议,每个相机的捕获时间现在不到 1 秒。此外,只需通过在命令后附加“&”在后台生成每个摄像头,它们就会自动 运行 并行,现在 18 个摄像头的总时间约为 10 秒,因此每个摄像头的平均时间现在约为Raspberry Pi 400.

.55 秒

我怀疑我可能会因为使用简单的“&”方法进行并行化而产生一些额外的开销。如果我可以生成线程而不是完整的进程,也许其中一些可以进一步减少。但这是我还没有经验的性能调整级别。