0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonとOpenCVで始めるカメラ制御入門:USBカメラの最短撮影からV4L2・libcamera・産業用カメラ・FPGAまで

0
Posted at

1. はじめに

PythonとOpenCVを使えば、USBカメラから画像を1枚取得して保存するだけなら、数行のコードで実現できます。

import cv2

camera = cv2.VideoCapture(0)
ret, frame = camera.read()

if ret:
    cv2.imwrite("image.jpg", frame)

camera.release()

しかし、このコードを見て、

カメラ制御とは、意外に簡単なのだ

と考えるのは、半分正しく、半分は誤解です。

簡単なのは、PythonやOpenCVがカメラのすべてを直接制御しているからではありません。USBカメラ、OS、デバイスドライバ、通信規格、画像処理回路などが、複雑な処理をあらかじめ引き受けてくれているためです。

元記事は2020年に公開され、2021年に更新されたもので、OpenCVとscheduleを使って画像を定期保存する最短例に重点が置かれていました。基本的な考え方は現在も有効ですが、現代的な開発では、エラー処理、環境分離、ログ、デバイス識別、再接続、保存容量、バックエンド、サービス化なども最初から意識した方が安全です。(Qiita)

本記事では、次の順序でカメラ制御を学びます。

  1. USBカメラから画像を1枚取得する
  2. なぜUSBカメラは簡単に使えるのかを理解する
  3. 安定した定期撮影プログラムを作る
  4. OpenCVの背後にあるOS・ドライバ・APIを理解する
  5. 一眼カメラ、産業用カメラ、MIPI CSI-2カメラとの違いを知る
  6. 高速撮像でFPGAや専用ミドルウェアが必要になる理由を理解する

単なるサンプルコードではなく、カメラ開発全体を見渡すための入口を目指します。


2. カメラ画像がPythonに届くまで

まず、次の構造を理解することが重要です。

Pythonの次に、いきなりカメラが接続されているわけではありません。

たとえばLinuxでUSBカメラを使う場合、典型的には次の経路を通ります。

Python
  ↓
OpenCV VideoCapture
  ↓
V4L2
  ↓
Linux uvcvideoドライバ
  ↓
USB Video Class通信
  ↓
USBカメラ

OpenCVの公式ドキュメントでも、カメラの設定値は次のような多層構造を通過すると説明されています。

VideoCapture
  → API backend
  → Operating System
  → Device Driver
  → Device Hardware

そのため、OpenCVで設定した解像度、フレームレート、露光時間などが、必ずしも指定どおりに反映されるとは限りません。(OpenCVドキュメント)


3. なぜUSBカメラは簡単なのか

3.1 UVCという共通規格がある

一般的なWebカメラの多くは、UVC、すなわちUSB Video Classという共通規格に従っています。

UVCでは、カメラがホストPCに対して、

  • 対応する解像度
  • 対応するフレームレート
  • ピクセル形式
  • MJPEGなどの圧縮形式
  • 明るさ
  • コントラスト
  • 露光
  • フォーカス

などの情報を、ある程度共通した方法で通知・制御できるように定めています。USB-IFはUVC 1.5の仕様書一式を公開しています。(USB-IF)

つまり、OSから見ると、

これはメーカー独自の謎の装置ではなく、UVC規格に従ったビデオ装置である

と認識できます。

Linuxでは一般的なUVCカメラをuvcvideoドライバが扱い、標準的なV4L2デバイスとしてユーザー空間へ公開します。メーカー独自機能についても、UVC Extension UnitをV4L2のコントロールへ対応づける仕組みがあります。(Linux Kernel Archives)


3.2 OSに汎用ドライバが用意されている

UVC対応カメラであれば、多くの場合、ユーザーがカーネルドライバを書く必要はありません。

OS側には、あらかじめ次のようなカメラ基盤があります。

OS 主なカメラAPI
Linux V4L2
Windows Media Foundation、DirectShow
macOS AVFoundation

OpenCVのVideoCaptureは、これらのAPIをバックエンドとして利用します。OpenCVはCAP_V4L2CAP_MSMFCAP_DSHOWCAP_AVFOUNDATIONなどのバックエンドを選択できます。(OpenCVドキュメント)

したがって、PythonプログラムはUSB通信のパケット構造やDMA、割り込み処理を意識せずに、

ret, frame = camera.read()

と書くだけで、画像をNumPy配列として受け取れます。


3.3 カメラ内部で画像処理が済んでいる

USB Webカメラは、単なるイメージセンサではありません。

多くの製品ではカメラ内部で、

  • センサの読み出し
  • Bayer配列からRGBまたはYUVへの変換
  • ホワイトバランス
  • 自動露光
  • ノイズ低減
  • 色補正
  • ガンマ補正
  • JPEGまたはMJPEG圧縮

などが実行されています。

PCには、すでに比較的扱いやすいYUYVやMJPEGなどの映像が届きます。

そのため、USBカメラが簡単なのは、

複雑なカメラシステムが簡単になったのではなく、複雑な部分がカメラ内部、OS、ドライバに隠されている

からです。


4. 学習ステップ0:開発環境を作る

以前はAnaconda環境を前提とする例も多く見られましたが、小さなカメラ制御プログラムであれば、標準のvenvで十分です。

mkdir camera-capture
cd camera-capture

python3 -m venv .venv
source .venv/bin/activate

Windows PowerShellでは次のようにします。

python -m venv .venv
.venv\Scripts\Activate.ps1

OpenCVをインストールします。

python -m pip install --upgrade pip
python -m pip install opencv-python

インストールを確認します。

python -c "import cv2; print(cv2.__version__)"

PyPIでは、通常のデスクトップ環境向けにopencv-python、GUIを必要としないサーバー向けにopencv-python-headlessが提供されています。同じ環境へ複数種類のOpenCVパッケージを同時に入れると、いずれもcv2名前空間を使うため衝突する可能性があります。(PyPI)

画面表示を行わず、画像を保存するだけのサーバーでは、次でも構いません。

python -m pip install opencv-python-headless

5. 学習ステップ1:USBカメラから画像を1枚取得する

まずは、エラー処理を含めた最小コードを作ります。

#!/usr/bin/env python3

from pathlib import Path

import cv2


def main() -> None:
    device_id = 0
    output_path = Path("image.jpg")

    camera = cv2.VideoCapture(device_id)

    if not camera.isOpened():
        raise RuntimeError(
            f"カメラを開けませんでした: device_id={device_id}"
        )

    try:
        ret, frame = camera.read()

        if not ret or frame is None or frame.size == 0:
            raise RuntimeError("カメラから画像を取得できませんでした")

        if not cv2.imwrite(str(output_path), frame):
            raise RuntimeError(
                f"画像を保存できませんでした: {output_path}"
            )

        print(f"saved: {output_path.resolve()}")

    finally:
        camera.release()


if __name__ == "__main__":
    main()

OpenCVのread()は、フレームの取得とデコードをまとめて実行します。画像を取得できなかった場合はFalseを返し、画像は空になります。したがって、retと画像の両方を確認することが重要です。(OpenCVドキュメント)

実行します。

python oneshot.py

一例ですが、下記のようにPC内蔵のカメラがあれば、写真が撮れてるはずです。

image.jpg


6. device_id=0とは何か

次のコードの0は、カメラそのものの固有番号ではありません。

camera = cv2.VideoCapture(0)

これは、OSまたはOpenCVバックエンドから見た「0番目のビデオデバイス」です。

たとえば、

  • 内蔵カメラ
  • USBカメラ
  • 仮想カメラ
  • HDMIキャプチャ
  • 赤外線カメラ

などが複数存在すると、番号が変わることがあります。

LinuxではV4L2デバイスが、一般に次のようなデバイスファイルとして公開されます。

/dev/video0
/dev/video1
/dev/video2

V4L2の公式仕様でも、映像取得デバイスは通常/dev/video0などのキャラクタデバイスとしてアクセスされると説明されています。(Linuxカーネルドキュメント)

Linuxでは次のコマンドで確認できます。

ls -l /dev/video*

v4l-utilsが入っていれば、より詳しく確認できます。

v4l2-ctl --list-devices

対応する解像度、ピクセル形式、フレームレートを調べるには、次を実行します。

v4l2-ctl \
    --device=/dev/video0 \
    --list-formats-ext

USBの接続順で番号が変わる環境では、次のような永続的なシンボリックリンクを使う方が安全です。

ls -l /dev/v4l/by-id/

Pythonからもパスを直接指定できます。

camera = cv2.VideoCapture(
    "/dev/v4l/by-id/usb-xxxxxxxx-video-index0",
    cv2.CAP_V4L2,
)

7. 実際に使われているバックエンドを確認する

OpenCVにバックエンドを自動選択させた場合、環境によって動作が異なることがあります。

camera = cv2.VideoCapture(0)

if not camera.isOpened():
    raise RuntimeError("カメラを開けませんでした")

print("backend:", camera.getBackendName())

LinuxでV4L2を明示する場合は次のようにします。

camera = cv2.VideoCapture(0, cv2.CAP_V4L2)

Windowsでは次の候補があります。

camera = cv2.VideoCapture(0, cv2.CAP_MSMF)
camera = cv2.VideoCapture(0, cv2.CAP_DSHOW)

macOSでは次のように指定できます。

camera = cv2.VideoCapture(0, cv2.CAP_AVFOUNDATION)

自動選択で問題なく動く間は、cv2.CAP_ANYのままで構いません。カメラが開けない、設定が反映されない、遅延が大きいといった問題が発生したときに、バックエンドを明示します。


8. 解像度とフレームレートを設定する

次のように、解像度とフレームレートを要求できます。

camera.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
camera.set(cv2.CAP_PROP_FPS, 30)

ただし、set()は「要求」であり、必ず採用されるとは限りません。

設定後は、実際の値を読み戻します。

width = camera.get(cv2.CAP_PROP_FRAME_WIDTH)
height = camera.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = camera.get(cv2.CAP_PROP_FPS)

print(f"size: {width:.0f} x {height:.0f}")
print(f"fps: {fps:.2f}")

OpenCVのドキュメントにも、プロパティ値はバックエンド、OS、ドライバ、ハードウェアを経由するため、実際に使用される値と異なる可能性があると明記されています。(OpenCVドキュメント)

高解像度・高フレームレートを使う場合、カメラがMJPEGをサポートしていれば、次を指定することで利用可能になることがあります。

fourcc = cv2.VideoWriter_fourcc(*"MJPG")
camera.set(cv2.CAP_PROP_FOURCC, fourcc)

これもカメラとバックエンドに依存するため、設定後の確認が必要です。


9. 最初の数フレームを捨てる理由

カメラを開いた直後は、自動露光やホワイトバランスが安定していない場合があります。

そのため、最初の数フレームを読み捨ててから保存すると、安定した画像を得やすくなります。

for _ in range(10):
    ret, frame = camera.read()

ret, frame = camera.read()

厳密な測光や色評価を行う場合には、自動露光や自動ホワイトバランスを無効化し、露光時間、ゲイン、照明条件を固定する必要があります。

単に「画像が撮れた」ことと、「定量的な計測に使える画像が得られた」ことは別です。


10. 学習ステップ2:一定時間ごとに画像を保存する

数秒から数十秒ごとに撮影するだけなら、外部のスケジューラライブラリは必須ではありません。

Python標準ライブラリのtime.monotonic()を使うと、OSの時計合わせの影響を受けにくい周期処理を作れます。

以下は、実運用を少し意識した定期撮影プログラムです。

#!/usr/bin/env python3

from __future__ import annotations

import argparse
import logging
import os
import time
from datetime import datetime
from pathlib import Path
from typing import Union

import cv2


CameraSource = Union[int, str]


BACKENDS = {
    "auto": cv2.CAP_ANY,
    "v4l2": cv2.CAP_V4L2,
    "msmf": cv2.CAP_MSMF,
    "dshow": cv2.CAP_DSHOW,
    "avfoundation": cv2.CAP_AVFOUNDATION,
}


def parse_camera_source(value: str) -> CameraSource:
    """数字ならカメラ番号、それ以外ならパスまたはURLとして扱う。"""
    try:
        return int(value)
    except ValueError:
        return value


def open_camera(
    source: CameraSource,
    backend_name: str,
    width: int,
    height: int,
    fps: float,
    fourcc: str | None,
) -> cv2.VideoCapture:
    backend = BACKENDS[backend_name]
    camera = cv2.VideoCapture(source, backend)

    if not camera.isOpened():
        raise RuntimeError(
            f"カメラを開けませんでした: "
            f"source={source}, backend={backend_name}"
        )

    if fourcc:
        if len(fourcc) != 4:
            raise ValueError("--fourccは4文字で指定してください")

        camera.set(
            cv2.CAP_PROP_FOURCC,
            cv2.VideoWriter_fourcc(*fourcc),
        )

    if width > 0:
        camera.set(cv2.CAP_PROP_FRAME_WIDTH, width)

    if height > 0:
        camera.set(cv2.CAP_PROP_FRAME_HEIGHT, height)

    if fps > 0:
        camera.set(cv2.CAP_PROP_FPS, fps)

    actual_width = camera.get(cv2.CAP_PROP_FRAME_WIDTH)
    actual_height = camera.get(cv2.CAP_PROP_FRAME_HEIGHT)
    actual_fps = camera.get(cv2.CAP_PROP_FPS)

    logging.info("backend: %s", camera.getBackendName())
    logging.info(
        "actual format: %.0f x %.0f, %.2f fps",
        actual_width,
        actual_height,
        actual_fps,
    )

    return camera


def warm_up_camera(
    camera: cv2.VideoCapture,
    frame_count: int,
) -> None:
    for _ in range(frame_count):
        ret, _ = camera.read()

        if not ret:
            time.sleep(0.05)


def read_frame(camera: cv2.VideoCapture):
    ret, frame = camera.read()

    if not ret or frame is None or frame.size == 0:
        raise RuntimeError("フレームを取得できませんでした")

    return frame


def save_jpeg_atomic(
    path: Path,
    frame,
    quality: int,
) -> None:
    """
    JPEGを一時ファイルへ書き、最後に置き換える。

    書き込み途中でプログラムが停止した場合に、
    完成していないJPEGが正規ファイル名で残ることを防ぐ。
    """
    success, encoded = cv2.imencode(
        ".jpg",
        frame,
        [cv2.IMWRITE_JPEG_QUALITY, quality],
    )

    if not success:
        raise RuntimeError("JPEGへの変換に失敗しました")

    path.parent.mkdir(parents=True, exist_ok=True)

    temporary_path = path.with_name(
        f".{path.name}.tmp"
    )

    temporary_path.write_bytes(encoded.tobytes())
    os.replace(temporary_path, path)


def build_output_path(output_dir: Path) -> Path:
    timestamp = datetime.now().astimezone().strftime(
        "%Y%m%dT%H%M%S.%f%z"
    )

    return output_dir / f"image_{timestamp}.jpg"


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="OpenCVを使ってカメラ画像を定期保存する"
    )

    parser.add_argument(
        "--device",
        default="0",
        help=(
            "カメラ番号、デバイスパス、"
            "またはストリームURL。既定値: 0"
        ),
    )

    parser.add_argument(
        "--backend",
        choices=BACKENDS,
        default="auto",
        help="OpenCVのカメラバックエンド",
    )

    parser.add_argument(
        "--output-dir",
        type=Path,
        default=Path("images"),
        help="画像の保存先",
    )

    parser.add_argument(
        "--interval",
        type=float,
        default=10.0,
        help="撮影間隔。単位は秒",
    )

    parser.add_argument(
        "--count",
        type=int,
        default=0,
        help="撮影枚数。0ならCtrl+Cまで継続",
    )

    parser.add_argument(
        "--width",
        type=int,
        default=0,
        help="要求する画像幅。0なら変更しない",
    )

    parser.add_argument(
        "--height",
        type=int,
        default=0,
        help="要求する画像高さ。0なら変更しない",
    )

    parser.add_argument(
        "--fps",
        type=float,
        default=0.0,
        help="要求するフレームレート。0なら変更しない",
    )

    parser.add_argument(
        "--fourcc",
        default=None,
        help="MJPGなどのFOURCC。4文字で指定",
    )

    parser.add_argument(
        "--warmup-frames",
        type=int,
        default=10,
        help="起動直後に読み捨てるフレーム数",
    )

    parser.add_argument(
        "--jpeg-quality",
        type=int,
        default=95,
        choices=range(1, 101),
        metavar="[1-100]",
        help="JPEG品質",
    )

    return parser


def main() -> None:
    args = build_parser().parse_args()

    if args.interval <= 0:
        raise ValueError("--intervalは0より大きくしてください")

    if args.count < 0:
        raise ValueError("--countは0以上にしてください")

    logging.basicConfig(
        level=logging.INFO,
        format=(
            "%(asctime)s "
            "%(levelname)s "
            "%(message)s"
        ),
    )

    source = parse_camera_source(args.device)

    camera = open_camera(
        source=source,
        backend_name=args.backend,
        width=args.width,
        height=args.height,
        fps=args.fps,
        fourcc=args.fourcc,
    )

    captured_count = 0
    consecutive_failures = 0

    try:
        warm_up_camera(
            camera,
            args.warmup_frames,
        )

        next_deadline = time.monotonic()

        while args.count == 0 or captured_count < args.count:
            now = time.monotonic()

            if now < next_deadline:
                time.sleep(next_deadline - now)

            try:
                frame = read_frame(camera)
                output_path = build_output_path(
                    args.output_dir
                )

                save_jpeg_atomic(
                    output_path,
                    frame,
                    args.jpeg_quality,
                )

                captured_count += 1
                consecutive_failures = 0

                logging.info(
                    "saved %d: %s",
                    captured_count,
                    output_path,
                )

            except RuntimeError as error:
                consecutive_failures += 1

                logging.error(
                    "capture failed (%d): %s",
                    consecutive_failures,
                    error,
                )

                if consecutive_failures >= 3:
                    logging.warning(
                        "カメラを再接続します"
                    )

                    camera.release()
                    time.sleep(1.0)

                    camera = open_camera(
                        source=source,
                        backend_name=args.backend,
                        width=args.width,
                        height=args.height,
                        fps=args.fps,
                        fourcc=args.fourcc,
                    )

                    warm_up_camera(
                        camera,
                        args.warmup_frames,
                    )

                    consecutive_failures = 0

            next_deadline += args.interval

            current_time = time.monotonic()

            if next_deadline < current_time:
                skipped = (
                    int(
                        (current_time - next_deadline)
                        // args.interval
                    )
                    + 1
                )

                next_deadline += skipped * args.interval

                logging.warning(
                    "処理が撮影周期に間に合わなかったため、"
                    "%d回分をスキップしました",
                    skipped,
                )

    except KeyboardInterrupt:
        logging.info("Ctrl+Cを受け取ったため終了します")

    finally:
        camera.release()
        logging.info("camera released")


if __name__ == "__main__":
    main()

11. 定期撮影プログラムの使い方

10秒ごとに撮影します。

python capture_interval.py \
    --device 0 \
    --interval 10

1920×1080、30 fps、MJPEGを要求します。

python capture_interval.py \
    --device 0 \
    --width 1920 \
    --height 1080 \
    --fps 30 \
    --fourcc MJPG \
    --interval 10

100枚撮影して終了します。

python capture_interval.py \
    --device 0 \
    --interval 10 \
    --count 100

LinuxでV4L2を明示します。

python capture_interval.py \
    --device /dev/video0 \
    --backend v4l2 \
    --interval 10

12. なぜtime.sleep(10)だけでは不十分なのか

次のコードでも、一見すると10秒ごとに撮影できます。

while True:
    capture()
    time.sleep(10)

しかし、撮影と保存に0.3秒かかった場合、実際の周期は約10.3秒になります。

撮影・保存 0.3秒
待機      10.0秒
合計      10.3秒

これを長時間続けると、少しずつ撮影時刻がずれていきます。

一方、先ほどのコードは、

next_deadline += interval

として、基準時刻に対して次の撮影時刻を計算しています。

また、処理が撮影周期より遅くなった場合には、遅れている撮影を一気に連続実行せず、その回をスキップします。

計測システムでは、

遅れた処理を取り戻そうとして大量の処理を連続実行する

よりも、

何回遅れたかをログへ残し、次の正規周期へ戻る

方が安全な場合があります。


13. PythonのループとOSスケジューラを使い分ける

定期処理には、いくつかの方法があります。

目的 適した方法
数秒ごとの撮影 Python内のループ
1分から数時間ごとの撮影 cron、systemd timer、タスクスケジューラ
常時起動する観測システム systemdサービスなど
複雑なジョブ管理 ワークフロー管理ツール
厳密なハードウェア同期 外部トリガ、FPGA、リアルタイム制御

数秒ごとの撮影をcronなどで行うと、毎回カメラを開き直すことになります。

その場合、

  • カメラ初期化時間
  • 自動露光の再収束
  • USB認識の遅延
  • デバイス競合

などが発生します。

短い周期では、カメラを開いたままPythonプロセスを常駐させる方が自然です。

一方、1時間に1回だけ撮影する場合は、常駐プロセスを持たず、OSのスケジューラから1回撮影プログラムを起動する設計も合理的です。


14. ディスク容量を必ず見積もる

定期撮影では、コードよりも先にディスクが問題になることがあります。

1枚のファイルサイズを$S$、撮影間隔を$T$秒、観測時間を$D$日とすると、必要容量は概算で、

V
=
S
\times
\frac{86400}{T}
\times
D

です。

たとえば、1枚1 MBの画像を10秒ごとに保存すると、1日あたりの画像数は、

N
=
\frac{86400}{10}
=
8640

です。

したがって、1日あたり約8.64 GBになります。

30日間では約259 GBです。

保存容量だけでなく、

  • ファイル数
  • inode数
  • バックアップ時間
  • 転送時間
  • JPEG圧縮時間
  • 破損ファイルの検出
  • 古いデータの削除方針

も設計する必要があります。

長期観測では、日付ごとのディレクトリに分ける方法も有効です。

images/
├── 2026-06-25/
├── 2026-06-26/
└── 2026-06-27/

15. データ取得と画像処理を分離する

カメラ制御プログラムへ、画像解析、AI推論、ネットワーク転送、GUI表示などをすべて詰め込むと、撮影が不安定になりやすくなります。

基本的には、次のように分離します。

取得処理に最も重要なのは、

  • フレームを失わないこと
  • 取得時刻を正しく記録すること
  • 障害を検出すること
  • データを壊さず保存すること

です。

AI推論が一時的に遅くなっても、カメラ取得まで止まらない設計が望まれます。

動画処理では、GStreamerのようなパイプラインフレームワークを使うと、取得、変換、圧縮、保存、配信を個別の要素として構成できます。GStreamerはカメラ取得や編集を含むメディア処理用のアプリケーション開発基盤を提供しています。(gstreamer.freedesktop.org)

LinuxのV4L2カメラは、たとえば次のように確認できます。

gst-launch-1.0 \
    v4l2src device=/dev/video0 \
    ! videoconvert \
    ! autovideosink

GStreamerのv4l2srcは、WebカメラなどのV4L2デバイスから映像を取得する要素です。(gstreamer.freedesktop.org)


16. ここから先が「一般のカメラ」の難しさ

「USBカメラ以外の一般のカメラ」といっても、実際にはいくつかの種類があります。

種類 主な接続・規格 難易度
USB Webカメラ UVC
一眼・ミラーレス PTP、メーカー独自SDK
産業用USBカメラ USB3 Vision、GenICam
産業用Ethernetカメラ GigE Vision、GenICam 中〜高
Raspberry Pi系カメラ MIPI CSI-2、libcamera 中〜高
センサモジュール単体 MIPI CSI-2、I2C
Camera Link、CoaXPress フレームグラバ
独自高速カメラ FPGA、PCIe、専用回路 非常に高

この違いは、カメラの価格だけでは決まりません。

重要なのは、

どこまでが完成したカメラ製品で、どこからを自分で実装しなければならないか

です。


17. 一眼・ミラーレスカメラ

一眼カメラやミラーレスカメラは、USBで接続できても、必ずしもUVCカメラとして動作するわけではありません。

写真撮影用カメラでは、

  • 静止画撮影
  • シャッター制御
  • 絞り制御
  • ISO感度
  • 保存画像の転送
  • ライブビュー
  • バルブ撮影
  • フォーカス制御

などを、PTPまたはメーカー独自プロトコルで扱います。

LinuxやmacOSでは、対応機種であればgphoto2またはlibgphoto2を利用できます。libgphoto2は多数の静止画カメラに対応していますが、Webカメラを扱うためのライブラリではありません。(gphoto.org)

例として、接続されたカメラを確認します。

gphoto2 --auto-detect

静止画を撮影してPCへ保存します。

gphoto2 \
    --capture-image-and-download

ただし、対応する機能はカメラごとに異なります。

ある機種では静止画撮影まで可能でも、別の機種では画像転送だけ、あるいはライブビューだけということがあります。専用SDKが必要になる場合もあります。

つまり、

USBで接続できること

と、

OpenCVでWebカメラのように読めること

は同じではありません。


18. 産業用カメラ

産業用カメラでは、

  • 外部トリガ
  • 正確なタイムスタンプ
  • 複数カメラ同期
  • 長時間露光
  • 12 bit、14 bit、16 bit画像
  • Bayer RAW
  • ROI読み出し
  • ゲインの定量設定
  • フレーム欠落検出
  • カメラ内部の温度監視

などが必要になります。

この世界では、USB3 Vision、GigE Vision、GenICamなどの規格がよく使われます。

GenICamは、GigE Vision、USB3 Vision、Camera Linkなど、異なる通信方式のカメラに対して共通のプログラミングインターフェースを提供することを目的としています。(EMVA)

USB3 VisionはUSB 3.xを基盤としたマシンビジョン用規格であり、GigE VisionはEthernetを使うカメラインターフェース規格です。(Automate)

ただし、OpenCVが直接すべてを扱えるとは限りません。

一般には、

Pythonアプリケーション
  ↓
メーカーSDKまたはGenICam対応ライブラリ
  ↓
GenTL Transport Layer
  ↓
USB3 Vision / GigE Vision
  ↓
産業用カメラ

という構造になります。

Linuxでは、Aravisというオープンソースライブラリが、USB3 VisionとGigE Visionのカメラに対応しています。AravisのArvCameraはGenICamの複雑さを隠したカメラ制御APIを提供します。(aravisproject.github.io)

産業用カメラでは、OpenCVは「カメラ制御」よりも、取得後の画像処理に使われることが多くなります。


19. MIPI CSI-2カメラとlibcamera

Raspberry Piや組み込みSoCへ接続するカメラモジュールは、USBカメラとは構造が異なります。

典型的には、

イメージセンサ
  ↓ MIPI CSI-2
CSI-2受信回路
  ↓
ISP
  ↓
メモリ
  ↓
ユーザー空間

となります。

センサから届くのは、JPEG画像ではなく、RAW8、RAW10、RAW12などの生データであることがあります。

その場合、必要になる処理は次のとおりです。

  • センサへの電源供給
  • クロック供給
  • リセット制御
  • I2Cによるレジスタ設定
  • MIPI CSI-2受信
  • Bayer配列の展開
  • 欠陥画素補正
  • ブラックレベル補正
  • デモザイク
  • ホワイトバランス
  • 色変換
  • 自動露光
  • 自動フォーカス
  • DMAバッファ管理

Linuxでは、複雑なカメラパイプラインを扱うために、V4L2、Media Controller、V4L2 sub-deviceなどが使用されます。複雑なデバイスでは、センサ、CSI-2受信回路、ISPなどが個別のsub-deviceとして表現されます。(Linuxカーネルドキュメント)

libcameraは、こうした複雑なLinuxカメラ構成と画像制御アルゴリズムを抽象化し、アプリケーションから一貫した方法でカメラを利用できるようにするカメラスタックです。(libcamera)

つまり、MIPI CSI-2カメラでは、

OpenCVからカメラを開く

より前に、

Linuxがセンサ、CSI-2受信回路、ISP、バッファを正しく結びつけられる状態を作る

必要があります。


20. 専用デバイスドライバは必ず必要なのか

結論としては、カメラによります。

専用ドライバを書かなくてよい場合

  • UVC対応USBカメラ
  • OS標準APIで認識できるカメラ
  • メーカーSDKが提供されているカメラ
  • GenICam対応ライブラリで制御できる産業用カメラ
  • 既存のlibcamera対応センサ

この場合、アプリケーション開発者がカーネルドライバを直接書く必要はありません。

専用ドライバが必要になりやすい場合

  • OSが知らない独自USBデバイス
  • 新しいMIPI CSI-2イメージセンサ
  • 独自PCIeカメラボード
  • FPGAで作った独自フレームグラバ
  • 独自DMAエンジン
  • 特殊な同期・割り込み機構を持つ装置

デバイスドライバは、単に「画像を読むコード」ではありません。

一般には次の役割があります。

  • ハードウェアの検出
  • 電源、クロック、リセット制御
  • レジスタ読み書き
  • DMAバッファの管理
  • 割り込み処理
  • エラー処理
  • ユーザー空間APIの提供
  • ストリーム開始・停止
  • タイムスタンプ管理
  • ハードウェア状態の取得

したがって、専用ドライバ開発は、Pythonプログラミングとは異なる領域です。


21. 高速カメラでは、なぜFPGAが必要になるのか

カメラのデータ量は、概算で次の式から見積もれます。

R
=
W
\times
H
\times
B
\times
F

ここで、

  • $W$:横方向の画素数
  • $H$:縦方向の画素数
  • $B$:1画素あたりのバイト数
  • $F$:フレームレート

です。

1920×1080、RGB 8 bit、30 fpsの非圧縮画像では、

R
=
1920
\times
1080
\times
3
\times
30

となり、約187 MB/sです。

4K、16 bit、数百fps、複数カメラとなると、一般的なPython処理より前に、

  • 入力帯域
  • メモリ帯域
  • PCIe帯域
  • バッファ容量
  • コピー回数
  • レイテンシ
  • 同期精度

が問題になります。


22. FPGAが担当する処理

高速撮像システムでは、次のようなパイプラインをFPGAに実装することがあります。

FPGA側では、たとえば次を実装します。

  • MIPI CSI-2やLVDSの受信
  • パケット解析
  • RAW10、RAW12などの展開
  • ラインバッファ
  • デモザイク
  • 欠陥画素補正
  • ダーク補正
  • ROI切り出し
  • 画素加算
  • 閾値判定
  • 圧縮
  • タイムスタンプ
  • 外部トリガ同期
  • DMA転送
  • フレーム欠落検出

AMDのMIPI CSI-2 RX Subsystemでは、CSI-2カメラから受信したデータを処理し、AXI4-Stream形式の画素データとして後段へ出力します。(AMD ドキュメント)

またVitis Visionは、OpenCVに類似した画像処理機能をFPGAやAI Engine向けに最適化したライブラリを提供しています。(xilinx.github.io)


23. FPGAだけ作れば終わりではない

高速カメラ開発では、FPGAのHDLを書くだけではシステムになりません。

必要になるのは、たとえば次の全体です。

イメージセンサ
  +
FPGAロジック
  +
組み込みファームウェア
  +
DMA
  +
Linuxデバイスドライバ
  +
ユーザー空間ライブラリ
  +
PythonまたはC++ API
  +
監視・ログ機構

このうち、FPGAロジックとアプリケーションの間をつなぐ部分は、広い意味でミドルウェアと呼べます。

ミドルウェアには、

  • レジスタ制御API
  • バッファ管理
  • ストリーム制御
  • メタデータ
  • タイムスタンプ
  • エラー通知
  • カメラ設定
  • 画像形式変換
  • デバイス列挙
  • 複数カメラ同期

などが必要です。

高速撮像では、単純な「画像を読む関数」よりも、データが途切れず流れる仕組み全体を設計することになります。


24. FPGAを使うべきかの判断

高速だからといって、必ずFPGAが必要なわけではありません。

まずは次の順番で検討するとよいでしょう。

OpenCV
  ↓
V4L2 / GStreamer
  ↓
ハードウェアエンコーダ
  ↓
GPU / NPU
  ↓
専用フレームグラバ
  ↓
FPGA

FPGAが有力になるのは、次のような場合です。

  • CPUへ送る前にデータを減らしたい
  • マイクロ秒以下の決定論的処理が必要
  • 複数カメラを厳密に同期したい
  • 画素ごとの処理を連続ストリームで実行したい
  • CPUやGPUへ送るには帯域が大きすぎる
  • 特殊なセンサインターフェースを受信したい
  • 外部トリガと画像を正確に対応づけたい

一方、

  • 数fpsから30 fps程度
  • JPEGまたはMJPEGで取得可能
  • 数十ミリ秒の遅延を許容
  • 単一カメラ
  • 保存後に解析すればよい

という用途では、USBカメラとOpenCVで十分なことが多いでしょう。


25. 現代的な開発ステップ

カメラシステムは、最初から大規模に作るべきではありません。

次の順番で段階的に進めます。

Step 1:1枚だけ撮る

確認することは、

  • カメラを開けるか
  • 画像を取得できるか
  • 保存できるか

です。

ここではGUIもAIも不要です。


Step 2:カメラの能力を調べる

確認することは、

  • 解像度
  • フレームレート
  • ピクセル形式
  • 圧縮形式
  • 露光
  • ゲイン
  • フォーカス
  • 実際のバックエンド

です。

Linuxではv4l2-ctl、libcamera環境ではcamなどを使って、まずカメラ単体を調査します。libcameraのcamユーティリティは、カメラ一覧の表示やフレーム取得に利用できます。(libcamera)


Step 3:定期取得を安定化する

追加するものは、

  • エラー処理
  • ログ
  • タイムスタンプ
  • 再接続
  • ディスク容量監視
  • 正常終了処理
  • 設定の読み戻し

です。


Step 4:取得と処理を分ける

たとえば、

capture.py
process.py
monitor.py

に分けます。

取得プログラムは、画像を確実に得ることへ集中させます。


Step 5:長期運転に対応する

必要になるのは、

  • systemdなどによる自動起動
  • プロセス監視
  • ログローテーション
  • 古い画像の削除
  • 温度・USBエラーの監視
  • カメラ切断からの復旧
  • 動作確認用のヘルスチェック

です。


Step 6:性能限界を測定する

次を測ります。

  • 実効フレームレート
  • フレーム欠落数
  • 保存時間
  • CPU使用率
  • メモリ使用量
  • USB帯域
  • エンドツーエンド遅延
  • タイムスタンプ精度

「動く」と「要求性能を満たす」は別です。


Step 7:必要な技術へ進む

目的に応じて、

  • GStreamer
  • V4L2
  • libcamera
  • gphoto2
  • GenICam
  • USB3 Vision
  • GigE Vision
  • メーカーSDK
  • Linuxドライバ
  • FPGA
  • PCIe DMA

へ進みます。


26. プロジェクト構成も整理する

少し長期的に使うなら、次のような構成にします。

camera-project/
├── README.md
├── pyproject.toml
├── config/
│   └── camera.toml
├── src/
│   └── camera_capture/
│       ├── __init__.py
│       ├── capture.py
│       ├── device.py
│       └── storage.py
├── tests/
│   ├── test_storage.py
│   └── test_timestamp.py
├── data/
│   ├── raw/
│   └── processed/
└── logs/

ハードウェアが必要な部分と、ハードウェアなしでテストできる部分を分離することも重要です。

たとえば、

  • ファイル名生成
  • ディスク容量計算
  • 設定ファイルの読み込み
  • JPEG保存
  • ログ解析

は、実カメラなしでもテストできます。

一方、実カメラを使う試験は、hardware-in-the-loop testとして分けて実施します。


27. よくある問題

カメラを開けない

確認することは、

  • カメラが接続されているか
  • 別のアプリがカメラを使用していないか
  • OSのカメラ権限が許可されているか
  • Linuxでデバイスのアクセス権があるか
  • カメラ番号が変わっていないか
  • バックエンドが適切か

です。

Linuxでは次を確認します。

ls -l /dev/video*
groups

必要に応じてvideoグループへの所属を確認します。


画像が暗い、最初だけ白い

自動露光が安定していない可能性があります。

  • 数フレーム読み捨てる
  • 数秒待つ
  • 露光を固定する
  • ゲインを固定する
  • 照明を安定させる

などを試します。


解像度やfpsが設定どおりにならない

カメラが、その解像度とフレームレートの組み合わせに対応していない可能性があります。

v4l2-ctl \
    --device=/dev/video0 \
    --list-formats-ext

で確認します。


映像が数秒遅れる

内部バッファへ古いフレームが蓄積している可能性があります。

リアルタイム性を優先する場合は、

  • 取得スレッドを分離する
  • 最新フレームだけを残す
  • GStreamerでdropを設定する
  • バッファ数を制限する
  • ハードウェアタイムスタンプを使う

などを検討します。


カメラを抜き差しすると止まる

実運用では、USB切断は必ず起こり得るものとして設計します。

  • 連続失敗回数を数える
  • カメラをrelease()する
  • 数秒待つ
  • 再度開く
  • 復旧したことをログへ残す

といった再接続処理が必要です。


28. セキュリティとプライバシー

カメラ画像には、人物、研究資料、装置画面、位置情報などが写り込む可能性があります。

最低限、次を確認します。

  • 不必要な範囲を撮影しない
  • 保存先のアクセス権を制限する
  • 外部へ無認証で映像配信しない
  • 保存期間を決める
  • カメラの動作中であることを明示する
  • 個人情報を含む画像を不用意に共有しない
  • 実行のためだけに常時root権限を使わない

技術的に撮影できることと、撮影してよいことは別問題です。


29. まとめ

USBカメラをOpenCVから使うだけなら、必要なコードは数行です。

しかし、その簡単さは、

  • UVCという共通規格
  • OSの汎用デバイスドライバ
  • V4L2、Media Foundation、AVFoundation
  • カメラ内部のISP
  • OpenCVのバックエンド

によって支えられています。

学習の入口としては、USBカメラが最適です。

USBカメラ
  ↓
OpenCV
  ↓
定期撮影
  ↓
エラー処理と長期運転
  ↓
V4L2 / GStreamer
  ↓
libcamera / GenICam
  ↓
デバイスドライバ
  ↓
FPGAと高速撮像

最初は、1枚の画像を確実に保存するだけで構いません。

その後、

その1枚は、どの規格、どのドライバ、どのバッファ、どの画像処理を通って届いたのか

を少しずつ掘り下げていくことで、単なるPythonスクリプトから、本格的な画像計測・組み込みカメラ・高速データ収集システムへ進むことができます。

関連記事

やや古いですが、

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?