Raspberry Piで指定したデバイスから画面キャプチャする(2021.7.25)

Summary

opencvではデバイス番号を指定してカメラを特定するが、デバイスの番号は実際に接続されているUSBカメラの状況によって変わるので、デバイス番号をハードコーディングすると、意図しないデバイスからの入力になってしまう。
複数のカメラデバイスがRaspberry Piに接続されている場合にはユーザに選択させるようにしたい。Raspberry Piではv4l2-ctlコマンドの出力を使う。

Macでのやりかたはこちら

Raspberry Piに接続されたカメラデバイスのidを特定したい

v4l2-ctlでデバイス情報を取得する

OpenCVではデバイス番号で指定したカメラをopenして使用します。 ところが、複数のカメラデバイスが存在すると0決め打ちでは思っているのと違うデバイスを掴んでしまうことがあります。

import cv2

cap = cv2.VideoCapture(0)

Raspberry Piではv4l2-ctlコマンドを使うと、接続されているデバイスの一覧を取得することができます。
コマンドラインからは以下のように実行でき、手元のRaspberry Pi環境では4つのデバイスが見つかります。しかし、video10以上は呼び出せません。また、同じデバイスでも番号の若い方だけが使えます。

$ v4l2-ctl --list-devices
bcm2835-codec-decode (platform:bcm2835-codec):
            /dev/video10
            /dev/video11
            /dev/video12

bcm2835-isp (platform:bcm2835-isp):
            /dev/video13
            /dev/video14
            /dev/video15
            /dev/video16

mmal service 16.1 (platform:bcm2835-v4l2-0):
            /dev/video0

UVC Camera (534d:2109): USB Vid (usb-3f980000.usb-1.4):
            /dev/video1
            /dev/video2

pythonからの外部コマンドの呼び出しには、subprocessモジュールを使用します。(v4l2-ctlコマンドはRaspbberry OSにデフォルトでインストールされています)
エスケープを意識しなくて良いようにshell=Trueを指定し、帰りの文字列をvideo_devicesに代入します。文字列はバイナリパックされているので、printするには.decode("utf-8")を指定して文字列に変換します。

その上で、以下のような条件を満たすものを取り出します。

実際のコード

具体的な処理は以下のような処理として記述した

  1. Check camera devices with the v412-ctl command

    v4l2-ctlの出力をretで受けて、utf-8の文字列に変換し、改行\nコードで分割します。最終的にはretにはv4l2-ctlの出力を行ごとに分割したリストが入ります。

  2. Parse the lines

    retのリストを1行ずつlに取り出して(for l in ret:)、空行でなく先頭がタブ\tでない行をデバイス名としてdevicesに記録
    先頭が\tで始まる行はデバイス番号(/dev/video?)として、最後のデバイス名のリストに追加(リストのリスト)
    結果としてdevicesは、[デバイス名, デバイスID, デバイスID, … ]のようなリストがデバイスの個数分だけ格納されたリストが入ったものになる。
    リスト内にデバイスIDが複数あっても、先頭のもの以外はopencvで開けないため、devices[i][1]が開ける可能性のあるデバイスID

  3. Sort by device ID

    devicesの中身を中身のリストの二番目の要素(使えるデバイスID)で昇順にsortする。
    この結果、デバイス名は/dev/videoXのXが小さい順に並ぶ。
    devices.sort(key=sortsecond)でkeyとして指定している sortsecond = lambda val: val[1]により二番目の要素でのsortを実現。

  4. List active device IDs

    devicesのリストを参照して、list_devicesに[デバイスID数字のみ, デバイスID文字列,デバイス名]のリストとして入れ直す。list_dev_idsには「デバイスID数字のみ」を入れる(その「デバイスID数字のみ」が存在するかどうか後で簡単に確認できるようにするため)
    /dev/video10以降のデバイスIDはopencvから開けないので無視する

import subprocess

command = "v4l2-ctl --list-devices"

# Check camera devices with the v4l2-ctl command
ret = subprocess.check_output(command, shell=True)
ret = ret.decode("utf-8")
ret = ret.split("\n")
#print(ret)

# Parse the lines
devices = []
num_devices = 0
for l in ret:
    if l!="" and l[0]!="\t":
        dev_name = l
        devices.append([dev_name])
        num_devices += 1
    elif l!="" and l[0]=="\t":
        devices[num_devices-1].append(l.replace("\t",""))

# Sort by device ID
sortsecond = lambda val: val[1]
devices.sort(key=sortsecond)

# List active device IDs
list_devices = []
list_dev_ids = []
print("Select a camera devie:")
for l in devices:
    dev_name = l[0]
    dev_ref = l[1]
    dev_id = int(dev_ref.replace("/dev/video",""))
    if dev_id < 10:
        list_devices.append([dev_id, dev_ref, dev_name])
        list_dev_ids.append(dev_id)
        print(" {:d}: {:<12s} {:s}".format(dev_id, dev_ref, dev_name))
num_active_devices = len(list_devices)

val = list_devices[0][0]    
if num_active_devices > 1:
    try:
        val = int(input())
    except ValueError:
        print("Wrong device id")
        val = list_devices[0][0]
if val not in list_dev_ids:
    print("Wrong device id")
    val = list_devices[0][0]

idx = list_dev_ids.index(val)
print("Use {:d}: {:<12s} {:s}".format(list_devices[idx][0], list_devices[idx][1], list_devices[idx][2]))

device_id = val

ここで、sortsecond = lambda val: val[1]をkeyに指定すると、リストのリストの2番目の要素を用いてsortすることになります。

Raspberry Piに接続されたカメラデバイスの解像度の確認と設定

継続調査中

v4ls-ctlでデバイスを指定して--list-format-extを実行すると詳細な情報が出力される

$v4ls-ctl --list-formats-ext --device 1

ここで得られる情報を使って、cv2.CAP_PROP_FOURCCなどを設定すると良さそうに見える。

# フォーマット・解像度・FPSの設定
#cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G'))
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('Y','U','Y','V'))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
cap.set(cv2.CAP_PROP_FPS, FPS)

# フォーマット・解像度・FPSの取得
fourcc = decode_fourcc(cap.get(cv2.CAP_PROP_FOURCC))
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = cap.get(cv2.CAP_PROP_FPS)
print("fourcc:{} fps:{} width:{} height:{}".format(fourcc, fps, width, height))

“v4ls-ctl –list-formats-ext –device x”の出力例

$v4ls-ctl --list-formats-ext --device 1
ioctl: VIDIOC_ENUM_FMT
    Type: Video Capture

    [0]: 'MJPG' (Motion-JPEG, compressed)
        Size: Discrete 1920x1080
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.040s (25.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 1600x1200
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.040s (25.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 1360x768
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.040s (25.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 1280x1024
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.040s (25.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 1280x960
            Interval: Discrete 0.020s (50.000 fps)
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 1280x720
            Interval: Discrete 0.017s (60.000 fps)
            Interval: Discrete 0.020s (50.000 fps)
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
        Size: Discrete 1024x768
            Interval: Discrete 0.017s (60.000 fps)
            Interval: Discrete 0.020s (50.000 fps)
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
        Size: Discrete 800x600
            Interval: Discrete 0.017s (60.000 fps)
            Interval: Discrete 0.020s (50.000 fps)
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
        Size: Discrete 720x576
            Interval: Discrete 0.017s (60.000 fps)
            Interval: Discrete 0.020s (50.000 fps)
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
        Size: Discrete 720x480
            Interval: Discrete 0.017s (60.000 fps)
            Interval: Discrete 0.020s (50.000 fps)
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
        Size: Discrete 640x480
            Interval: Discrete 0.017s (60.000 fps)
            Interval: Discrete 0.020s (50.000 fps)
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
    [1]: 'YUYV' (YUYV 4:2:2)
        Size: Discrete 1920x1080
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 1600x1200
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 1360x768
            Interval: Discrete 0.125s (8.000 fps)
        Size: Discrete 1280x1024
            Interval: Discrete 0.125s (8.000 fps)
        Size: Discrete 1280x960
            Interval: Discrete 0.125s (8.000 fps)
        Size: Discrete 1280x720
            Interval: Discrete 0.100s (10.000 fps)
        Size: Discrete 1024x768
            Interval: Discrete 0.100s (10.000 fps)
        Size: Discrete 800x600
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 720x576
            Interval: Discrete 0.040s (25.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 720x480
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)
        Size: Discrete 640x480
            Interval: Discrete 0.033s (30.000 fps)
            Interval: Discrete 0.050s (20.000 fps)
            Interval: Discrete 0.100s (10.000 fps)
            Interval: Discrete 0.200s (5.000 fps)

$v4ls-ctl --list-formats-ext --device 0
ioctl: VIDIOC_ENUM_FMT
    Type: Video Capture

    [0]: 'YU12' (Planar YUV 4:2:0)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [1]: 'YUYV' (YUYV 4:2:2)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [2]: 'RGB3' (24-bit RGB 8-8-8)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [3]: 'JPEG' (JFIF JPEG, compressed)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [4]: 'H264' (H.264, compressed)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [5]: 'MJPG' (Motion-JPEG, compressed)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [6]: 'YVYU' (YVYU 4:2:2)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [7]: 'VYUY' (VYUY 4:2:2)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [8]: 'UYVY' (UYVY 4:2:2)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [9]: 'NV12' (Y/CbCr 4:2:0)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [10]: 'BGR3' (24-bit BGR 8-8-8)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [11]: 'YV12' (Planar YVU 4:2:0)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [12]: 'NV21' (Y/CrCb 4:2:0)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2
    [13]: 'RX24' (32-bit XBGR 8-8-8-8)
        Size: Stepwise 32x32 - 2592x1944 with step 2/2

Raspberry Piでデバイスを指定して画像キャプチャするPythonスクリプト

完成したスクリプトは次のようになります。 起動すると、ビデオデバイスを調査し2つ以上のデバイスが見つかったら、ユーザにデバイス番号を入力させます。デバイスが1つしかなければ自動で選定し、1つも見つからなければエラー終了します。
終了は’q’、’s’でその瞬間の画面をキャプチャーして./photo.jpgに保存します。

ソースコードはこちら(video_capture_pi_test.py


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# =======================================================
#
#  select a video device on Raspberry Pi using v4l2-ctl
#
#  video_capture_pi_test.py
#  coded by Noboru Harada (noboru@ieee.org)
#
#  Changes:
#  2021/07/25: First version
#  2021/07/27: Fixed a bug picking an idx
# =======================================================

import sys
import os
import cv2
import subprocess


command = "v4l2-ctl --list-devices"

# Check camera devices with the v4l2-ctl command
ret = subprocess.check_output(command, shell=True)
ret = ret.decode("utf-8")
ret = ret.split("\n")
#print(ret)

# Parse the lines
devices = []
num_devices = 0
for l in ret:
    if l!="" and l[0]!="\t":
        dev_name = l
        devices.append([dev_name])
        num_devices += 1
    elif l!="" and l[0]=="\t":
        devices[num_devices-1].append(l.replace("\t",""))

# Sort by device ID
sortsecond = lambda val: val[1]
devices.sort(key=sortsecond)

# List active device IDs
list_devices = []
list_dev_ids = []
print("Select a camera devie:")
for l in devices:
    dev_name = l[0]
    dev_ref = l[1]
    dev_id = int(dev_ref.replace("/dev/video",""))
    if dev_id < 10:
        list_devices.append([dev_id, dev_ref, dev_name])
        list_dev_ids.append(dev_id)
        print(" {:d}: {:<12s} {:s}".format(dev_id, dev_ref, dev_name))
num_active_devices = len(list_devices)

val = list_devices[0][0]    
if num_active_devices > 1:
    try:
        val = int(input())
    except ValueError:
        print("Wrong device id")
        val = list_devices[0][0]
if val not in list_dev_ids:
    print("Wrong device id")
    val = list_devices[0][0]

idx = list_dev_ids.index(val)
print("Use {:d}: {:<12s} {:s}".format(list_devices[idx][0], list_devices[idx][1], list_devices[idx][2]))

device_id = val

# select a camera resolution
res = [
    [1280, 720,  "0: 1280x720  (HD)"],
    [720,  480,  "1: 720x480   (SD)"],
    [1920, 1080, "2: 1920x1080 (Full HD)"]
    ]
print("Select resolution:")
print(" %s" % res[0][2])
print(" %s" % res[1][2])
print(" %s" % res[2][2])

res_val = 0
try: 
    res_val = int(input())
except ValueError:
    print("Wrong resolution settings") 
    print("Use default resolution setting") 
    
if res_val > 3 or res_val < 0:
    print("Use default resolution setting") 
    res_val = 0
res_width,res_height,res_str = res[res_val]

print ("Capture with [ %s ]" % (res_str))
print("Type 'q' to quit capturing / Type 's' for screen shot")

cap = cv2.VideoCapture(device_id)

# set video size
cap.set(cv2.CAP_PROP_FPS, 60)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, res_width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, res_height)

while True:
    # Capture frame-by-frame
    ret, frame = cap.read()

    # show the frame
    cv2.imshow("Type 'q' to quit capturing / Type 's' for screen shot", frame)

    key = cv2.waitKey(1) & 0xFF
    
    if key == ord('q'):
        break
    if key == ord('s'):
        filename = "./photo.jpg"
        cv2.imwrite(filename,frame)

# terminate resources
cap.release()
cv2.destroyAllWindows()

実行例

実行すると、デバイス番号を聞かれるので数字で入力。(デバイスが複数ある場合のみ)