【Python OpenCV】複数のカメラデバイスを識別する方法3選

# はじめに

OpenCVで指定するデバイスIDとカメラデバイスの対応はUSBを抜き差ししたりPCを再起動したら変わることがあります。
本記事では、複数のカメラデバイスを使用する際にデバイスIDが変わっても同じデバイスにアクセスできるようにするための方法を3つ紹介します。

なお、使用OSは Windows 11 です。
Pythonのバージョンにこだわりがない方は@youichi_ioさんの記事が参考になると思います。(Python 3.7)

https://qiita.com/youichi_io/items/70446fb027b6e4c6efd6

# 方法1: カメラのプロパティ（デフォルト）の違いを利用する

```python
import cv2

def check_info(last_index):
    for i in range(last_index):
        try:
            cap = cv2.VideoCapture(i)
            if cap is None or not cap.isOpened():
                raise ConnectionError
            print(f"-*- DEVICE_ID: {i} -*-")
            fps = cap.get(cv2.CAP_PROP_FPS)
            contrast = cap.get(cv2.CAP_PROP_CONTRAST)
            saturation = cap.get(cv2.CAP_PROP_SATURATION)
            gamma = cap.get(cv2.CAP_PROP_GAMMA)
            print(f"FPS: {fps}")
            print(f"Contrast: {contrast}")
            print(f"Saturation: {saturation}")
            print(f"gamma: {gamma}")
        except ConnectionError:
            break
 
if __name__ == "__main__":
    check_info(10)
```

上記コードを実行すれば、カメラのプロパティを確認できます。
カメラのデフォルトのプロパティはデバイスごとに異なるはずなので、次のようなコードで特定のプロパティをもつカメラのデバイスIDを指定することができます。

## サンプルコード

```python
import cv2

# 指定したいデバイスのプロパティ情報
FPS = 30.0
CONTRAST = 32.0
SATURATION = 64.0
GAMMA = 100.0

class DeviceInfo:
    '''
    Attributes
    ----------
    fps: float
    contrast: float
    saturation: float
    gamma: float
    '''
    def __init__(self):
        self.fps = 0
        self.contrast = 0
        self.saturation = 0
        self.gamma = 0
    def get_info(self, id):
        cap = cv2.VideoCapture(id)
        if cap is None or not cap.isOpened():
            raise ConnectionError
        self.fps = cap.get(cv2.CAP_PROP_FPS)
        self.contrast = cap.get(cv2.CAP_PROP_CONTRAST)
        self.saturation = cap.get(cv2.CAP_PROP_SATURATION)
        self.gamma = cap.get(cv2.CAP_PROP_GAMMA)
    def identify(self, fps, contrast, saturation, gamma):
        if self.fps != fps:
            return False
        if self.contrast != contrast:
            return False
        if self.saturation != saturation:
            return False
        if self.gamma != gamma:
            return False
        return True

def main():
    DEVICE_ID = 0
    print(f"デフォルトのデバイスID: {DEVICE_ID}")
    for i in range(10):
        try:
            _deviceInfo = DeviceInfo()
            _deviceInfo.get_info(i)  # デバイスIDが i のカメラのプロパティ情報を取得
            is_identified = _deviceInfo.identify(FPS, CONTRAST, SATURATION, GAMMA)
            if is_identified:
                DEVICE_ID = i
                print(f"デバイス{i}: 全てのプロパティが一致しました")
            else:
                print(f"デバイス{i}: プロパティが一致しませんでした")
        except ConnectionError:
            break
    print(f"デバイスIDが {DEVICE_ID} に更新されました")

if __name__ == "__main__":
    main()
```

## メリット

- Python+OpenCVだけでコードが完結する
- OSに依存せず使えそう

## デメリット

- デバイス名がわからないので、どのデバイスを使ってるのか直感的にわかりにくい

# 方法2: C/C++でデバイス名をテキストファイルに出力してPythonで読み込む

[山本ワールドさんのホームページ](http://www.yamatyuu.net/index.html)を参考にしました。
C/C++でカメラのデバイス名とそのデバイスに対応している解像度とfpsをテキストファイルに出力する実行ファイルを公開してくださっているので、今回はそれをそのまま使わせてもらいます。
(ソースコードも公開してくださっているので、C/C++に理解のある方は本用途により適したコードを書いた方がスマートだと思います。)

今回は、Pythonしかよくわからない私のような方でもすぐ使える解決策を記しています。

1. こちらのサイトの下の方に実行ファイルのダウンロードリンクがあるので、ダウンロード
    - [DirectShowを使用してカメラ名と解像度の一覧を取得する](http://www.yamatyuu.net/computer/program/vc2013/directshow/camerainfo/index.html)
2. 作業用Pythonファイルと同じ場所にダウンロードした実行ファイル (camerainfo.exe) を置いて`subprocess.run()`
3. テキストファイル (camera.txt) がカレントディレクトリで作成される
4. テキストファイルを読み込んでカメラ名のみ抽出

## サンプルコード
```python
import os
import subprocess
import re
 
class CameraName:
    '''
    Attributes
    ----------
    name: str[]
        カメラのデバイス名をID順に格納
    '''
    def __init__(self):
        self.name = []
    def get(self):
        file_dir = os.path.dirname(__file__)
        txt_file = "camera.txt"
        subprocess.run(f"{file_dir}/camerainfo.exe")
        with open(txt_file) as f:
            self.name = []
            datalist = f.readlines()
            for data in datalist:
                repattern = re.compile(r'^\S')
                if repattern.match(data):
                    self.name.append(data.rstrip('\n'))

if __name__ == "__main__":
    _camera = CameraDevice()
    _camera.get()
    for i, name in enumerate(_camera.name):
        print(f"デバイス{i}: {name}")
```

## メリット

- デバイス名を取得できるので直感的に使える
- ついでに解像度とfpsの情報も取得できる

## デメリット

- 回りくどい

# 方法3: C/C++ & Pythonでデバイス名を取得するPYDファイルを生成する

以下のサイトを参考にしました。

- 参考サイト: [Listing Multiple Cameras for OpenCV-Python on Windows](https://www.dynamsoft.com/codepool/multiple-camera-opencv-python-windows.html)
- ソースコード: [https://github.com/yushulx/python-capture-device-list](https://github.com/yushulx/python-capture-device-list)

## 手順

### 前準備

- あらかじめビルド用のパッケージをインストールしておく
    - `pip install scikit-build cmake`
    - Anaconda の場合は `conda install -c conda-forge scikit-build` と `conda install cmake`

### ビルド

```bash
git clone https://github.com/yushulx/python-capture-device-list.git
cd python-capture-device-list
python setup.py build install
```

- ビルド時にアクティブなPythonのバージョンに合わせてPYDファイルが生成される
- 特にエラーがなければ `_skbuild\win_amd64-3.x\cmake-install\device` にPYDファイルが生成されているはず
- 生成されたPYDファイルがある device フォルダをまるごとコピーして作業用のPythonファイルと同じ場所にペースト
- あとはモジュールをインポートして `device.getDeviceList()` するだけ

## サンプルコード

```python
import device # モジュールをインポート

device_list = device.getDeviceList() # これだけで[デバイス名, 解像度]のリストが取得できる
for i, device in enumerate(device_list):
     print(f"デバイス{i}: {device[0]}")
```

## メリット

- デバイス名を取得できる
- 短いコードで済む

## デメリット

- ビルド用の外部ライブラリが必要 (ビルド時)
- Pythonのバージョンによって生成されるPYDファイルの中身が変わるので，環境が違うとインポートエラーになったりする

# それぞれどんな人におすすめか

**方法1**: OSを含め環境に依存したくない人におすすめ
**方法2**: WindowsユーザでPythonのバージョンに依存したくない人におすすめ
**方法3**: コードをできるだけシンプルにしたい人におすすめ

# おわりに
初投稿なので、わかりにくいところがあっても多めに見ていただけると幸いです。

>ちなみに、@youichi_ioさんが紹介されている記事でも本記事の **方法3** と同じようにPYDファイルを使用しています。 ([https://github.com/pvys/CV-camera-finder](https://github.com/pvys/CV-camera-finder) からダウンロードできる `pymf.pyd` を使用する)
私も参考にさせていただいたのですが、モジュールのインポートがうまくいかなかったため、Developer Command Prompt for VS で `dumpbin /dependents pymf.pyd` をしてみたところ、 `python37.dll` を参照していることがわかりました。
よって、おそらく Python 3.7 でないとモジュールのインポートがうまくいきません。
逆に、Python 3.7 でも問題ない場合は `pymf.pyd` を利用するのが最も簡単だと思います。