OpenCVがWebカメラであなたの顔を画像として取得するまでの仕組み

12 min read読了の目安(約11500字 4

この記事はWebRTC Meetup Online #2の資料を元に編集したOpenCV Advent Calendar 2020の10日目の記事です。

記事の組み立て直しをしてはいるものの、元がMarkdown形式で作成したスライドなためスライド風な構成になっているところがありますがご容赦ください。

はじめに

Webカメラと一言にいっても色々な機種があります。これらは一体どう違うのでしょう?
この記事に目を通せばその辺りを調べられる知識を得つつ、OpenCVからきちんと取り扱うことが出来るようになるでしょう。

No. メーカー 型番 特徴
1 ORite technology SC120 発掘された2003年製。UVCですらない(要ドライバなヤツ)12万画素?
2 Buffalo Inc. BSWHD06M ザ・安物
3 YouZipper HDC-264M H.264対応
4 YouZipper HDC-265M H.264/H.265対応2Kカメラ
5 PTZOptics PT-WEBCAM-80 電子PTZ搭載
6 J JOYACCESS aluminium web camera 1080P 美顔機能(AE/AWB?)設定項目多
7 ロジクール C525n AF搭載の普段使い720pのはずが…
8 Y&H ? 中華700円HDMI-USB変換
9 LIEWEG ? 安いので2個買った
10 LIEWEG ? 安いので2個買った
11 OBS Project OBS Studio 仮想カメラ
12 Snap Chat Snap Camera 仮想カメラ
13 SplitmediaLabs XSplit VCam 背景加工仮想カメラ
14 JETAKu WF-14 背景差し替えソフト付いてくるっていうから買ったら別有料ソフトって書いてあった
15 ELP 3.6mmレンズウェブカム-260fps 360p260fps/720p120fps/1080p60fps

全体像

全体としてはこんな感じです。レンズを通して顔の像を作るところは光学の世界、センサ面に結像された像を読み取る電子の世界、センサとPCの橋渡しをUSBで行う通信の世界、受け取ったフレームを処理するソフトウエアの世界、という流れで説明していきます。


物理(光学)の世界~一眼とWebカメラ(とスマホ)の違い~

一番大きな違いは設計思想そのものだと思います。レンズとセンサの大きさ、撮影設定などの柔軟性などに現れています。

  • 一眼レフ:でっかいレンズ × でっかいセンサつよい
  • Webカメラ:写ってればいいでしょレベル~産業用レベル
  • スマホ:目的ごとに複数のカメラモジュールを用意したりソフトで後処理したり。ともかく小さく薄く。

光学の世界で大事なことを一つだけ(機種選択の基準として)

ピント合わせ の方式はどうなっているか?

  • マニュアルフォーカス → 自分でリング回す:ピント合わせがつらい
  • 固定フォーカス    → 距離は大体一定だから特に問題ない
  • オートフォーカス   → 自動で合わせてくれる:楽だけど値段が高い

※露光時間や焦点とピント、絞りの話もしたいけど割愛
※電子の世界でもAWBとかゲイン調整、産業用カメラのキャリブレーション済みデータの話もしたいけど割愛

電子の世界の入り口

ベイヤフィルタについて

センサは色が分からない。 とにかく入ってきた光を電気信号に変換する素子です。
そのため、下図のようなフィルタを使って画素ごとに担当する色を分けます。
正確には図に書いてあるような色のフィルタではなく、図に書いてある色だけを通すフィルタです。

通常、4画素のうち2画素をGとしています(人間の目は輝度変化に対して敏感で、輝度はGの影響が強いから)。

ベイヤ変換(Raw現像の基礎)

Gのフィルタがかかってる画素にはR/Bの情報がないので、周囲2/4個のR/Bのフィルタがかかった画素の値を平均するなどして補間します。
より高度な補間方法(=計算に時間かかる)もあります。

RGB-YUV/YCbCr変換、YUY2/MJPG変換

例:RGB to YCbCr

Y =   0.297R + 0.587G + 0.114B
Cb = -0.169R - 0.331G + 0.500B
Cr =  0.500R - 0.419G - 0.081B

(係数はYCbCr/YUVやIT.xxxなどで色々バリエーションあり)

YUVに変換してUVを間引きすることでYUY2形式が得られます。YCbCrをさらに圧縮してJPEG形式が得られ、このJPEGを単純に繰り返し出力するのがMJPEG形式です。
H.264/H.265は時間軸方向の情報も使った圧縮方法ですが詳細は省略します。

式参考:JPEG・MPEG完全理解

ここまでのまとめ

光がセンサを通して画像データになりました(USBで送る準備が出来た)。ただし、1画素1画素がRGBを厳密に読み取っているわけではないです。

  • ベイヤー変換
  • YUY2間引き

により情報の欠落がありますし、MJPEGを使えば非可逆圧縮がかかっています。重要なのはこれらの圧縮手法では「人間が見たときの劣化が少ないこと」を重視していて、機械での画像処理を考慮したものではないことです。ただし画像処理でも輝度情報に頼ることは多いので、不適切とは限らないですが稀に注意が必要なことがあります。

通信の世界

UVC(USB Video Class)でUSB転送

最近のWebカメラは大抵UVCという仕様にのっとっていて、この仕様のおかげでデバイスドライバなしで利用することができます。

仕様:UVC v1.1 / UVC v1.5

※ほぼ読んでません。USBパケットキャプチャベースでの理解が中心です。

重要なことは、「デバイス側が自分の特性をPCに伝える」「PCはその情報を参照して設定値を送る」でしょう。
(特性:フォーマット、解像度、フレームレート、設定可能項目(露光時間や色合いなど)、その設定値の範囲)

パケットキャプチャ

USBにデータが流れるということはUSBPcapでパケットキャプチャすればWireSharkで通信内容が分かるということです。実際に見たのが上の図です。パケットキャプチャしてみるとすぐに気づきますが、Webカメラはソフト側の事情なんて一切考慮せず、決められたフレームレートで毎フレーム送ってきています。それをいい感じにアプリケーションに渡したり、設定変更を反映させるのはOS側の役目なのでしょう。

ソフトウエアの世界

いよいよ画像フレームがUSBを通してパソコン側に入ってきました。とはいえ、ここからは割と複雑になっていくので雑な解説になってしまいます。

OSでの処理(ざっくり)と見るべきパケット

Webカメラは接続した最初(あるいはパケットキャプチャ開始した最初)に自分の特性を伝えます。ここのUSBパケットをキャプチャすることで、カメラの大体の素性が分かります。

また、ブラウザやアプリが設定変更した場合は変更要求を送るようになっているので、このパケットを見れば実際のカメラの状態を追いかけることができます。

図中にも書いたように、デバイス側が「Format NのIndex Mは解像度がいくついくつ、フレーム間隔(fpsの逆数)はこの中から選べます」と最初に選択しているので、その中から選びます。

いよいよWebRTCとの関係へ

元がWebRTC Meetupでの発表なのでそちらではWebRTCでの利用について中心にしていました。この節はその名残です。

あるWebカメラが下記のような設定を持っているとき、様々な制約(WebRTCにおけるカメラオープンの引数)でどういう挙動になるかを解説しています。

getUserMediaしてWireSharkで見てみる

{
    video: {
        width: 1280,
        height: 720
    }
}


bFormatIndex: 1(MJPEG)
bFrameIndex: 2(1280x720)
dwFrameInterval: 333333(33.3333ms=30fps)

H.264サポートしてるカメラでもMJPEGで通信する

getUserMediaしてWireSharkで見てみる(2)

{
    video: {
        width: 640,
        height: 360
    }
}


bFormatIndex: 1(MJPEG)
bFrameIndex: 1(640x480)
dwFrameInterval: 333333(33.3333ms=30fps)

対応してない解像度はブラウザ側で対応する(元々左右をカットした)VGAで撮る→上下をカットしているので画角が狭くなっている(カメラにより挙動は異なる)

フレームレート

設定値は下図のように複数候補とデフォルトが1個ある。
単位は100マイクロ秒で333333で33.3333ミリ秒、30fps。666666で15fps。1000000で10fps。

getUserMediaしてWireSharkで見てみる(3)

{
    video: {
        width: 1280,
        height: 720,
        frameRate: 120
    }
}


bFormatIndex: 1(MJPEG)
bFrameIndex: 2(1280x720)
dwFrameInterval: 333333(30fps)

だが、ストリームの getVideoTracks()[0].getSettings() を見ると frameRate: 120 となってる

getUserMediaしてWireSharkで見てみる(4)

{
    video: {
        width: 1280,
        height: 720,
        frameRate: 8
    }
}


bFormatIndex: 1(MJPEG)
bFrameIndex: 2(1280x720)
dwFrameInterval: 1000000(10fps)

→このまま frameRate: 30 に変更しても設定変更リクエストが出ない(実際fps遅い)

複数タブ・ウインドウでの挙動

一つのブラウザで複数のウインドウ・タブで同じカメラをオープン出来ます。だからといって、カメラは複数の解像度・フレームレートで撮影出来るわけではないです。

→開いた順によって挙動が変化する(先のフレームレートもこれの亜種)

Aで1280x720でオープン→Bで640x480でオープン: Aは1280x720、Bは左右削った640x480、通信は1280x720
Bで640x480でオープン→Aで1280x720でオープン: AB共に左右削った640x480、通信も640x480

もうちょっとお手軽な調べ方

Chromeで chrome://media-internals を開いて Video Capture タブを開くと各カメラがサポートしている解像度とfps一覧が出ます。
※このやり方では「実際にどういう解像度でカメラと通信してるか」は不明

ここまでのまとめ

getUserMediaでカメラが撮影してMediaStreamに流れてくるまでの処理が分かった

  • カメラは決まった解像度・フレームレートしか出せない
  • それをブラウザが誤魔化している
  • そのせいでたまに変なことが起こる

おまけ: Webカメラが沢山あるとこういうときはどうなる?

デフォルトのカメラって何になる?

→ 分からん。さっぱり分からん。後から差すとデフォルトになるっぽい

同じ型番のカメラが2つある場合どうなる?

→ ブラウザだと列挙はされるが片方しか使えない(ストリームが開けずエラー)
→ OpenCVや各種ネイティブアプリでは普通に使えてるっぽい

解像度・フレームレート以外の設定項目

解像度やフレームレート以外の設定項目として、露光時間やピント合わせなどの撮影時の設定項目と、明るさや色合いなどの現像時に補正する項目があります。パン・チルトなどにも対応しているので、カメラ側に電動モータが組み込まれていればPC側から操作することができます。
※そういうカメラは3万円くらい~

どうやってこれらの値を設定するの?

videoTrack.applyConstraints({advanced: [{brightness: 255}]}) で設定可能

ただし対応ブラウザ/OSが限られます。

ブラウザ OS 対応 備考
Chrome Win 問題なし
Chrome Mac getCapabilities() でbrightnessなどが無い
Firefox Win getCapabilities() がない
Vivaldi Win 問題なし
- Linux チェックしてない

⇒ちなみにXSplit VCam使うと簡単に設定できます。OSの設定画面っぽいのですが、ここにたどり着く方法をVCam経由にする以外に知りません。(2020/12/28追記)OpenCVでの設定画面の出し方は後述します。

OpenCVはどう取り込んでるのか

いよいよOpenCVアドベントカレンダーとしての本題です。

まずは簡単なWebカメラ取り込み&表示プログラムを書いてみましょう。

import cv2


cap = cv2.VideoCapture(0)

print(cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FRAME_HEIGHT), cap.get(cv2.CAP_PROP_FPS))

ret, frame = cap.read()
if not ret:
    raise Exception("Capture Error")
print(frame.shape)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    cv2.imshow("camera", frame)

    key = cv2.waitKey(1)
    if key == ord("q"):
        break

cap.get()と取り込んだフレームのshapeの2つの方法で解像度を確認しています。単純に実行すると以下のようにVGA/30fpsで取り込んでいることが分かります。
これはUSBをパケットキャプチャすることでも確認が出来ます。また、パケットキャプチャで確認すると転送時のフォーマットはMJPEGになっています。

640.0 480.0 30.0
(480, 640, 3)

解像度変更の方法

ちょっと変えて撮影前に解像度を変更するようにしてみましょう。まずは今回使うカメラがサポートしている解像度です。

import cv2


cap = cv2.VideoCapture(0)

cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

print(cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FRAME_HEIGHT), cap.get(cv2.CAP_PROP_FPS))

ret, frame = cap.read()
if not ret:
    raise Exception("Capture Error")
print(frame.shape)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    cv2.imshow("camera", frame)

    key = cv2.waitKey(1)
    if key == ord("q"):
        break

意図通り、解像度が変わっているのが確認できます。

1920.0 1080.0 30.0
(1080, 1920, 3)

次はこのカメラでは対応していない解像度にしてみましょう。

cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1024)

おや、エラーになる訳ではなく近い解像度のものを選んで来ました。幅を合わせて高さは小さいものが選ばれましたね。1280x640としても同じく1280x720が選ばれるようです。

1280.0 720.0 30.0
(720, 1280, 3)

更に変更してみましょう。

cap.set(cv2.CAP_PROP_FRAME_WIDTH, 960)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

VGAになってしまいました。ちょっとアルゴリズムまでは分からないので「対応していない場合はいい感じに選んでくれる」と思っておきましょう。

640.0 480.0 30.0
(480, 640, 3)

まとめ & 書ききれなかったこまごましたこと

  • UVCが分かればOpenCVでの設定の仕方もわかってくる
  • VideoCaptureの引数の数字の順番は謎。カメラが増えると扱いきれなくなりがち
  • 欲しい解像度をカメラがちゃんとサポートしているか、確認しましょう
  • OpenCV(少なくともWindows)ではH.264/H.265は使えない(別途ソース調査の結果)
  • Logicool C525nはカタログスペック逆詐欺。VCam使えば顔追従まで出来る(別の話)

※執筆者はソースネクスト、SplitmediaLabs(XSplit VCamの開発元)、ヨドバシカメラ、ロジクールのいずれにも利害関係はありません。

明日は@wjs_fxfさんの「OpenCVをブラウザ上で動かすOpenCV.js Playgrondを作った話」です。OpenCV.jsの記事ということで楽しみにしています。

おまけ(2020/12/28追記): カメラごとの制御可能項目、対応解像度一覧

パケットキャプチャして調べた制御可能項目の一覧。アドベントカレンダー執筆後に買った右の2個のカメラが「私オートフォーカスできますよー」と嘘ついてるのが面白い。
いやいや、こんなボードカメラでM12マウントのレンズでレンズが回ったらびっくりするってば(笑)

対応解像度の一覧。面倒くさくてFPSまでは記載してませんが、FPSを見ても面白かったりします。
先のボードカメラについては「360pは260fps限定!」という漢らしすぎる割り切りっぷりなのが良いですね。
パケットキャプチャじゃなく chrome://media-internals からですが、MacBook ProのFaceTime HDがその逆でやたらFPS細かく刻んでる謎デバイスになっています。
記載してないけどFaceTime HDは360p非対応で地味に厄介なタイプですね。

おまけ2(2020/12/28追記):カメラ設定画面の出し方

バックエンドをDirectShowにして、CAP_PROP_SETTINGSに1を入れると設定画面が表示されます。普通のWebカメラの場合はOpenCVを呼び出しているプログラムを終了すると同時に設定ダイアログも閉じるが、独自ダイアログを呼び出しているLogicool製のWebカメラの場合はプログラムを終了してもなぜか設定ダイアログが閉じないです。そのため、その後でブラウザからLogicoolのWebカメラを開くと設定ダイアログが有効な状態で使うことが出来ます。

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_SETTINGS, 1)

今回のオチ

そもそものはじまりはコレを買ったこと

bg right:20% fit

「WebカメラでH.265!?」と思ったのでついつい買ってしまったものの、実際にH.265を使えるかどうかを全然把握していませんでした。
今回の方法でパケットキャプチャで調べてみたところ、Web会議などでは一切H.265(H.264も)を使っておらず、OpenCVですら使えないようでした。
頑張ってGstreamerでパイプライン書いたらH.264/H.265取り出せました。ところが、これが意外なことに残念画質で使い物にならなかったです。
今のところH.264を直接WebRTCに投げ込むのは難しいですね。Gstreamerなら可能かも?
背景ぼかしとかの加工処理が出来ないからあんまり意味ないかもしれないです。

Windows/gstreamerでの例:

gst-launch-1.0 -e ksvideosrc ^
 device-path="gst-monitorで出てくるdevice-path" ^
 ! video/x-raw,format=H264,width=1280,height=720 ^
 ! capssetter join=false replace=true caps=video/x-h264,stream-format=byte-stream,alignment=au,width=1280,height=720 ^
 ! h264parse ! queue ! mp4mux ! filesink location=capture.mp4