📸

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

2020/12/10に公開
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.3333Microsoft=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 遅い)

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

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

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


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

Discussion

mugiflymugifly

⇒ちなみにXSplit VCam使うと簡単に設定できます。OSの設定画面っぽいのですが、ここにたどり着く方法をVCam経由にする以外に知りません。

DirectShow が用意しているダイアログかと思います。
https://superuser.com/questions/1287366/open-webcam-settings-dialog-in-windows

このダイアログを表示するためだけのフリーウェアもありました。
https://tksoft.work/archives/3861

こーのいけこーのいけ

あー、なるほど。XSplit VCamは自分で(多分DirectShowなりを使って)カメラ開いてるから、そのダイアログが出せるんですね。Chromeもその気になれば出せるんでしょうけど、実装されていない、と。

別アプリになってても排他ロックされてるから開けないんじゃ・・・と思ったけど、Chromeとかは設定初期化したりしないから大丈夫なのかも。USBパケットキャプチャと合わせて深堀りすれば色々分かりそうです。

情報ありがとうございました!

こーのいけこーのいけ

今確認したところ、Chromeのカメラ選択デフォルトは「設定」で「カメラ」で検索して出てくるところでカメラデバイスを選択すればデフォルトになりますね。
前はちゃんと動いてなかったような覚えがあったので書かなかったのですが、これでカメラ選択機能作らなくても何とかなる(公開サービスでは作った方がいいです)。

こーのいけこーのいけ
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_SETTINGS, 1)

別のことで CAP_PROP_* のこと調べてたのですが、これでダイアログ開きつつ画像処理させることが出来ますね。
キャプチャバックエンドが変わるのであれこれ細かなとこの挙動が変わりそうですが…