[OpenCV] 複数のWebカメラを使用する場合、USBポートの番号からデバイスを識別するクラスを作ってみました

2020.07.23

1 はじめに

CX事業本部の平内(SIN)です。

OpenCVからVideoデバイスにアクセスする場合、デバイスIDを指定する方法しかありません。

DEVICE_ID = 0 # /dev/video0
cap = cv2.VideoCapture (DEVICE_ID)

しかし、Videoデバイスが複数接続された場合、このデバイスIDは、いつも同じとは限りません。 今回は、複数接続時に、安定して同じデバイスにアクセスするためのクラスを作成してみました。

2 環境

作業した環境は、RaspberryPiです。

Raspberry Piは、Model 4B(メモリ4G)で、OSは、本年5月の最新版(Raspbian GNU/Linux 10 (buster) 2020-05-27-raspios-buster-full-armhf.img です。

$ cat /proc/cpuinfo  | grep Revision
Revision	: c03112

$ lsb_release -a
No LSB modules are available.
Distributor ID:	Raspbian
Description:	Raspbian GNU/Linux 10 (buster)
Release:	10
Codename:	buster

$ uname -a
Linux raspberrypi 4.19.118-v7l+ #1311 SMP Mon Apr 27 14:26:42 BST 2020 armv7l GNU/Linux

3 デバイスID

最初に、デバイスIDについて確認します。以下、USBカメラを接続した場合、OSに認識されている様子です。 (※ USBカメラ以外の情報及び、ループバック(/dev/Video10,11,12)については、省略されています。)

接続されていない場合

$ lsusb
$ ls -la /dev/video*

1台目のカメラ(Logicool)を接続した場合

$ lsusb
Bus 001 Device 003: ID 046d:0892 Logitech, Inc. OrbiCam
$ ls -la /dev/video*
crw-rw---- 1 root video 81, 3 Jul 22 22:02 /dev/video0
crw-rw---- 1 root video 81, 4 Jul 22 22:02 /dev/video1

1台目のカメラ(Logicool)は、/dev/video0でアクセス可能です。

2台目のカメラ(Elecom)を追加した場合

$ lsusb
Bus 001 Device 003: ID 046d:0892 Logitech, Inc. OrbiCam
Bus 001 Device 004: ID 056e:701c Elecom Co., Ltd
$ ls -la /dev/video*
crw-rw---- 1 root video 81, 3 Jul 22 22:02 /dev/video0
crw-rw---- 1 root video 81, 4 Jul 22 22:02 /dev/video1
crw-rw---- 1 root video 81, 5 Jul 22 22:03 /dev/video2
crw-rw---- 1 root video 81, 6 Jul 22 22:03 /dev/video3

追加された2台目のカメラ(Elecom)は、/dev/video2でアクセス可能になります。

以下、Logicoolにアクセスしているコード

DEVICE_ID = 0 # /dev/video0(Logicool)
cap = cv2.VideoCapture (DEVICE_ID)

しかし、この状態でOSを再起動すると、同じコードで、Elecomの方にアクセスしてしまう場合があります。

これは、デバイスID(/dev/videoX)がOSに認識された順番で割り振られる事が原因です。

lsusbで確認すると、Device 00xの順番が入れ替わってしまっている事が確認できます。

$ lsusb
Bus 001 Device 004: ID 046d:0892 Logitech, Inc. OrbiCam
Bus 001 Device 003: ID 056e:701c Elecom Co., Ltd

4 USB機器、ポート情報

Linuxでは、/dev/videoXに登録される際に、USB機器の情報やポート番号が、シンボリックリンクとして追加されます。

$ ls -la /dev/v4l/by-id
lrwxrwxrwx 1 root root  12 Jul 22 23:11 usb-046d_HD_Pro_Webcam_C920_21E9E21F-video-index0 -> ../../video2
lrwxrwxrwx 1 root root  12 Jul 22 23:11 usb-046d_HD_Pro_Webcam_C920_21E9E21F-video-index1 -> ../../video3
lrwxrwxrwx 1 root root  12 Jul 22 23:11 usb-Sonix_Technology_Co.__Ltd._ELECOM_8MP_Webcam_SN0001-video-index0 -> ../../video0
lrwxrwxrwx 1 root root  12 Jul 22 23:11 usb-Sonix_Technology_Co.__Ltd._ELECOM_8MP_Webcam_SN0001-video-index1 -> ../../video1
$ ls -la /dev/v4l/by-path
lrwxrwxrwx 1 root root  12 Jul 22 23:11 platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0-video-index0 -> ../../video0
lrwxrwxrwx 1 root root  12 Jul 22 23:11 platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0-video-index1 -> ../../video1
lrwxrwxrwx 1 root root  12 Jul 22 23:11 platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.2:1.0-video-index0 -> ../../video2
lrwxrwxrwx 1 root root  12 Jul 22 23:11 platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.2:1.0-video-index1 -> ../../video3

この情報を確認すれば、機器の種類や、接続されているUSBポート番号が、どのデバイスIDに紐付いているかを確認することが出来ます。

下記は、接続したポートによってシンボリックリンクが変わるようです。

USBポート1に接続した場合 usb-0:1.1:1となっている

pi@raspberrypi:~ $ ls -la /dev/v4l/by-path
lrwxrwxrwx 1 root root  12 Jul 22 23:35 platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0-video-index0 -> ../../video0
lrwxrwxrwx 1 root root  12 Jul 22 23:35 platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.1:1.0-video-index1 -> ../../video1

USBポート4に接続した場合 usb-0:1.4.1:1となっている

pi@raspberrypi:~ $ ls -la /dev/v4l/by-path
lrwxrwxrwx 1 root root  12 Jul 22 23:30 platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.4:1.0-video-index0 -> ../../video0
lrwxrwxrwx 1 root root  12 Jul 22 23:30 platform-fd500000.pcie-pci-0000:01:00.0-usb-0:1.4:1.0-video-index1 -> ../../video1

5 デバイス一覧クラス

先のコマンドを使用して、ポート番号の情報とともに、デバイス情報を出力するクラスです。

import subprocess

class UsbVideoDevice():
    def __init__(self):
        self.__deviceList = []

        try:
            cmd = 'ls -la /dev/v4l/by-id'
            res = subprocess.check_output(cmd.split())
            by_id = res.decode()
        except:
            return

        try:
            cmd = 'ls -la /dev/v4l/by-path'
            res = subprocess.check_output(cmd.split())
            by_path = res.decode()
        except:
            return

        # デバイス名取得
        deviceNames = {}
        for line in by_id.split('\n'):
            if('../../video' in line):
                tmp = self.__split(line, ' ')
                if( "" in tmp):
                    tmp.remove("")
                name = tmp[8]
                deviceId = tmp[10].replace('../../video','')
                deviceNames[deviceId]=name

        # ポート番号取得
        for line in by_path.split('\n'):
            if('usb-0' in line):
                tmp = self.__split(line, '0-usb-0:1.')
                tmp = self.__split(tmp[1], ':')
                port = int(tmp[0])
                tmp = self.__split(tmp[1], '../../video')
                deviceId = int(tmp[1])
                if deviceId % 2 == 0:
                    name = deviceNames[str(deviceId)]
                    self.__deviceList.append((deviceId , port, name))
    
    def __split(self, str, val):
        tmp = str.split(val)
        if('' in tmp):
            tmp.remove('')
        return tmp

    # 認識しているVideoデバイスの一覧を表示する
    def disp(self):
        for (deviceId, port, name) in self.__deviceList:
            print("/dev/video{} port:{} {}".format(deviceId, port, name))

    # ポート番号(1..)を指定してVideoIDを取得する
    def getId(self, port):
        for (deviceId, p, _) in self.__deviceList:
            if(p == port):
                return deviceId
        return -1

6 実行例

クラスを使用しているコードです。

index.py

import cv2
from usbVideoDevice import UsbVideoDevice

usbVideoDevice = UsbVideoDevice()

print("情報一覧")
usbVideoDevice.disp()

print("\nポート番号とデバイスIDの一覧")
for port in range(4):
    deviceId = usbVideoDevice.getId(port + 1)
    if (deviceId != -1):
        print("PORT:{} /dev/video{}".format(port + 1, deviceId))

ポート1のみに接続されている場合

$ python3 index.py
情報一覧
/dev/video0 port:1 usb-Sonix_Technology_Co.__Ltd._ELECOM_8MP_Webcam_SN0001-video-index0

ポート番号とデバイスIDの一覧
PORT:1 /dev/video0

ポート2のみに接続されている場合

$ python3 index.py
情報一覧
/dev/video0 port:2 usb-Sonix_Technology_Co.__Ltd._ELECOM_8MP_Webcam_SN0001-video-index0

ポート番号とデバイスIDの一覧
PORT:2 /dev/video0

ポート2及び、3に接続されている場合

$ python3 index.py
情報一覧
/dev/video0 port:2 usb-Sonix_Technology_Co.__Ltd._ELECOM_8MP_Webcam_SN0001-video-index0
/dev/video2 port:3 usb-046d_HD_Pro_Webcam_C920_21E9E21F-video-index0

ポート番号とデバイスIDの一覧
PORT:2 /dev/video0
PORT:3 /dev/video2

そして、このクラスを利用して、ポート番号でOpenCVを初期化するコードは、以下のようになります

import cv2
from usbVideoDevice import UsbVideoDevice

usbVideoDevice = UsbVideoDevice()
PORT = 1
cap = cv2.VideoCapture (usbVideoDevice.getId(PORT))

これで、意識するべきは、カメラを接続するUSBポートの番号だけということになります。

7 最後に

今回は、変化するデバイスIDとUSBポートの番号を紐付けるクラスを作成してみました。 複数のVideoデバイスを利用する場合は、このような仕組みが必須かも知れません。