Macで指定したデバイスから画面キャプチャする(2021.7.4, 2021.7.23追記)

Summary

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

キャプチャーする画面サイズを選択できるようにした(2021.7.23追記)

Macのカメラデバイスのidを特定したい

Swift scriptでデバイス情報を取得する

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

import cv2

cap = cv2.VideoCapture(0)

いろいろとググって調べたところ、Macでswiftスクリプトを使うとデバイスの情報が取得できるようです。こちらを参考にしました。この記事で紹介されていたavfcam_list.swiftというスクリプトを実行すると、デバイスの情報がjson形式の文字列として取得できます。
コマンドラインからは以下のように実行でき、手元のMac環境では2つのデバイスが見つかります。 ここでBuild-inカメラは2番目に表示されるので、デバイスIDは1となり、0ではアクセスできないことがわかります。

$ swift ./avfcam_list.swift 
{
  "SPCameraDataType" : [
    {
      "_name" : "mmhmm Camera",
      "manufacturer" : "mmhmm, inc.",
      "spcamera_unique-id" : "mmhmmCameraDevice",
      "spcamera_model-id" : "mmhmmCameraModel"
    },
    {
      "_name" : "FaceTime HD Camera (Built-in)",
      "manufacturer" : "Apple Inc.",
      "spcamera_unique-id" : "0x8020000005ac8514",
      "spcamera_model-id" : "UVC Camera VendorID_1452 ProductID_34068"
    }
  ]
}

これができれば、pythonからこのスクリプトを外部コマンドとして呼び出せばよいことになります。外部コマンドの呼び出しには、subprocessモジュールを使用します。(下記の例ではパスは固定としていますが、完成版では実行しているpythonスクリプトと同じ場所のswiftスクリプトを実行します)
エスケープを意識しなくて良いようにshell=Trueを指定し、帰りの文字列をvideo_devicesに代入します。文字列はバイナリパックされているので、printするには.decode("utf-8")を指定して文字列に変換します。文字列をjson形式の辞書として読み込むには、json.loads()を使用します。python 3.7より前の辞書は順序を保存しないため、collectionsからOrderedDictを importして使用します。json.loads()object_pairs_hook=OrderedDictを指定することで、OrderedDict形式になります。

import subprocess
import json
from collections import OrderedDict

# Check camera devices with a swift script
camera_devices = subprocess.check_output("swift ./avfcam_list.swift", shell=True)
json_dict = json.loads(camera_devices, object_pairs_hook=OrderedDict)

camera_devices = camera_devices.decode("utf-8")
print(camera_devices)

json形式からOrderedDict形式への変換と読み込みの注意点

json_dictを表示すると以下のように、json_dict['SPCameraDataType']の要素が、各デバイスの情報を値として持つOrderdDictのリストになっていることがわかります。この例ではデバイスが2つあるので、リストの要素はデバイスの情報を保持しているOrderedDict2つです。

# print(json_dict)の結果
OrderedDict([
('SPCameraDataType', [
    OrderedDict([('_name', 'mmhmm Camera'), ('manufacturer', 'mmhmm, inc.'),    ('spcamera_unique-id', 'mmhmmCameraDevice'), ('spcamera_model-id',  'mmhmmCameraModel')]), 
    OrderedDict([('_name', 'FaceTime HD Camera (Built-in)'), ('manufacturer', 'Apple Inc.'),    ('spcamera_unique-id', '0x8020000005ac8514'), ('spcamera_model-id', 'UVC Camera     VendorID_1452 ProductID_34068')])
])])

#json_str = json.dumps(json_dict)
#print(json_str)の結果
{"SPCameraDataType": [
    {"_name": "mmhmm Camera", "manufacturer": "mmhmm, inc.", "spcamera_unique-id":  "mmhmmCameraDevice", "spcamera_model-id": "mmhmmCameraModel"}, 
    {"_name": "FaceTime HD Camera (Built-in)", "manufacturer": "Apple Inc.",    "spcamera_unique-id": "0x8020000005ac8514", "spcamera_model-id": "UVC Camera
     VendorID_1452 ProductID_34068"}
]}

ここまでわかれば、num_devices = len(json_dict['SPCameraDataType'])として要素数を取得し、添字を使ってそれぞれの辞書にアクセすれば良いことになります。

num_devices = len(json_dict['SPCameraDataType'])
val = 0
if num_devices > 1:
    print("Select a camera devie:")
    for s in range(num_devices):
        name = json_dict['SPCameraDataType'][s].get("_name")
        print(" %d: %s" % (s, name))

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

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

ソースコードはこちら(video_capture_test.py
avfcam_list.swift)をスクリプトと同じ場所に置いてください。)

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

# =======================================================
#  opencvを使ってカメラから画像をキャプチャするスクリプト
#
#  video_capture_test.py
#  coded by Noboru Harada (noboru@ieee.org)
#
#  Changes:
#  2021/07/04: First version
# =======================================================

import sys
import os
import subprocess
import json
from collections import OrderedDict
import numpy as np
import cv2

# Get path for the swift script (supporse to be in the same location with this python script)
script_path = os.path.dirname(os.path.abspath(__file__))
script_path = "swift " + script_path + "/avfcam_list.swift"
print(script_path)

# Check camera devices with a swift script
camera_devices = subprocess.check_output(script_path, shell=True)
json_dict = json.loads(camera_devices, object_pairs_hook=OrderedDict)

camera_devices = camera_devices.decode("utf-8")
print(camera_devices)

# see how the OrderdDict looks like
#json_str = json.dumps(json_dict)
#print(json_str)
#print(json_dict)
#print(json_dict.keys())
#print(json_dict['SPCameraDataType'])

if json_dict.get('SPCameraDataType') == None:
    print("No camera device found")
    sys.exit()

num_devices = len(json_dict['SPCameraDataType'])
if num_devices == 0:
    print("No camera device found")
    sys.exit()

val = 0
if num_devices > 1:
    print("Select a camera devie:")
    for s in range(num_devices):
        name = json_dict['SPCameraDataType'][s].get("_name")
        print(" %d: %s" % (s, name))
    try:
        val = int(input())
    except ValueError:
        print("Wrong device id")
        val = 0

if val > num_devices-1 or val < 0:
        print("Wrong device id")
        val = 0

device_id = val
device_name = json_dict['SPCameraDataType'][device_id].get("_name")
print(" Use Camera device %d: %s" % (val, device_name))
print("Type 'q' to quit capturing")

cap = cv2.VideoCapture(device_id)

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

    # convert color frame to gray image
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # show the frame
    cv2.imshow("Type 'q' to quit capturing", gray)
    #cv2.imshow("Type 'q' to quit capturing", frame)

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

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

実行例

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

command line

フレームが表示される