はじめに
CMOSイメージセンサS-EYEはシリアル通信により画像が取得できるモジュールです。特にPCと接続すれば多彩な画像処理を行うことができ、接続も装備も簡単に済みます。
今回は、いままで行った画面表示等をもとに、実用的な人検知について実験してみます。言語は他のページと同じくC#、接続はBluetooth無線経由で行います。
※このページで紹介する内容はあくまでも一例です。個別の作成のご相談ご質問はお答えできませんのでご了承下さい。このページと同じ内容についてのご質問についてはロボット掲示板にてお願いいたします。
※以下の情報は2009年1月現在のものです。ご注意ください。
S-EYEについてはこちらを参照願います。
構成
構成その他は前S-EYE画像高速表示のページと同じですのでそちらを参照願います。浅草ギ研製Bluetooth無線と市販の安価なBluetooth無線を使ってPCと接続しています。
以前に説明したような内容は割愛していますので、プログラムなどの詳細は前のページを参照してください。
実際には、趣味で作っているロボットの頭部にS-EYEを搭載した状態で実験を行ってます。
(萌え軸装備)
参考書籍
ディジタル画像処理の基礎と応用 酒井幸市著 ISBN4-7898-3707-6
これだけです。すごく良いですこの本。このページごときの内容は全てこの一冊でこと足ります。というか、この本の20%も使ってないです。
人検知のアルゴリズムの検討その1
前回は動体検知を実験してみました。動体検知のアルゴリズムは非常に簡単で、画像を連続で取得し、前画像と今画像を比べて、ある程度以上値が離れていたらその部分が動いていると判定するものでした。このときは簡単に濃度だけの比較でしたので白黒画像を使いました。今回は人検知なので色も使った高度な認識になりそうです。
経験上、色を判定する場合はRGBやCMYなどの色形態ではなくHUE(ヒュー)、つまり色相で判定するのが良いと考えています。下はCMOS-EYE(別な製品)の色相の図です。
色相による色の表現はある色を基準とした角度によって表されます。たとえば、上のように赤を基準にした場合は反対色であるシアンが逆の角度になる、という表し方をします。これによれば、色の明暗にあまり左右されずに色の判定が行えます。
実際の測定環境では明かりが常に変化します。たとえば、同じ赤色でも昼間と夕方ではRGBやCMYの場合には値が大きく変わりますが、HUEの場合は赤は赤で、明暗が変わってもHUE値はあまり変わりません。
HUEを計算する場合は、RGBではなくYUVの方が計算しやすいので今回はS-EYEからの出力をYUVに選択します。
画像フォーマットついて
上記の理由からYUV。大きさは前回同様80x60にしてみます。S-EYEの場合は「モード5」が「YUV80x60」の出力になります。
YUV80x60の画像を取得してみる
さて、色はHUEで計算するとして、実際に人はどのように検知できるかは今のところまったくわかりません。実際に画像があった方が検討しやすいと思われるので、YUV80x60の画像を取ってみます。
前回のプログラムを改造して、とりあえず80x60YUV画像を表示させるプログラムを作りました。詳細はソースのコメントを見てください。
S-EYE_HumanDetect1.lzh (C#2008ExpressEditionのプロジェクト)
取得したのが下の画面です。
モニタではよくわからないので、フォトショップで画像部分を拡大したところがこれです。
(x4倍)
自動露出とAWB(オートホワイトバランス)機能は切ってます。AWBは撮影した色の偏りによって勝手に色がかわってしまうので色関係の検出時には切る方が良いです。顔の前にある光ってるようなものは液晶モニタが外光に反射しているところです。天気は晴れで、外からの光源によって顔の左右の明暗がはっきりわかれているという状況です。
人検知のアルゴリズムの検討その2
さて、色はHUEで計算するとして、その他に何が必要か上の写真で検討してみます。背景の本棚の色もどちらかというと人間の肌の色っぽいですので、このような色が多いと判定が難しいと思われます。人間は動くので、動体検知で動いた部分を人の可能性あり、としてその中で肌色があるかを検出するというのを考えました。
1)動体検知する
2)動体の範囲で肌色のHUEが存在したら人とする
Hueを計算するプログラム
動体検知は前回やったので、Hueをどのように計算するかを検討してみます。また、肌の色のHue値がなんなのかも確認します。
まず、S-EYEから送られてくるフォーマットであるYUV422について説明します。
<YUV422について>
Y(Y):輝度
U(Cb):青系色差 B-Y
V(Cr):赤系色差 R-Y
となっています。S-EYEのYUVはITU-R
BT.601という規格になっています。ググると詳細がわかりますがここでは概要だけ説明します。
Yはそのまま輝度です。出力は8ビット値で、白いほど値が高く黒いほど値が低くなります。
Cb、Crの色差情報は、「差」というだけあってマイナスの値も含みます。128が中心値になりますので、Cb=128のときに青系色差が0ということになります。Cb=127のとき−1、Cb=126のとき−2...というように考えます。(ITU-RBT601では色データの解像度が8ビットではないので厳密には違いますが、考え方だけ。)
−Yの値−
−Cb、Crの値−
「422」の方ですが、S-EYEからのデータは色データだけ1ピクセルづつ間引かれて出力されます。人間の目には輝度情報よりも色情報の変化に鈍感なので、色情報を多少間引いても見た目はあまり変わらないという特性を利用して、データ量を減らしています。「4ピクセルの中で、Yが4つ、Cbが2つ、Crが2つ」なので「YUV422」と呼ばれています。
<色相ついて>
色相を求める式は教科書などでは次のように書かれています。
HはHueです。CrをCbで割ったものをアークタンジェントしたものがHueになります。この結果は、Cbを基準とした場合のCbからの角度がHueになります。色のイメージは下の図のようになります。Crを基準とした角度によって色が判断されます。前出のCMOS−EYEの色相は、結果がわかりやすいように赤を基準に修正されたものになっていますが、単純に色相を計算するとこのようになります。
<色相を取得してみる>
ということで、取得した画像の中心部20x20の色相の平均を計算するプログラムを組んでみました。
S-EYE_HumanDetect2.lzh (C#2008ExpressEditionのプロジェクト)
プロジェクトは先のものを上書きしているので、先のプログラムで実験していた方は古いプログラムのフォルダごと削除してからこちらを試してみてください。
変更部分のポイントは次の部分。
画像データはYUVで送られてきます。80x60画素モードの場合、2ピクセルあたり4バイトになりますので1画面は160x60バイトのデータになります。横方向が2倍になっているので気をつけてください。上の「YUVの出力順と画素の関係」の図をよく見ればどういうデータ構造かわかるかと思います。
行163〜168が心臓部です。初めに、一番初めのピクセルのHueを計算して保存します。まず、行166までで色差データを相対値に変換しています。行167のAtan2(
)関数は、「第一引数を第二引数で割ったもののアークタンジェントを角度で出力する」という便利なものです。
atanも含む三角関数系の関数ライブラリは、大抵「ラジアン」で計算します。ということで
結果 割る パイ 掛ける 180
することで「度」に変換すると人間でもわかりやすい値になります。
このとき、位相が180度を超えるとマイナスの値になるので、マイナスになった場合は360を加算しています。行168の部分です。
行169〜181は単純に20x20の平均をとっているだけで、やっていることは1ピクセル目と同じです。
マイコンなどのプログラムの場合はアークタンジェントなんて非常に面倒なプログラムになるのですが、PCですとライブラリでAtan2のような関数があるので簡単ですね。ちなみに、このような関数が無い場合はatanをテーブル化して、CbとCrがプラス/マイナス/ゼロの各条件ごとに計算式が変わる、というようなことになって結構死ねます(経験者語る)。
実行結果は次のようになりました。
画像の右のテキストボックスの中に計算値が表示されています。
ほぼ、理論値(上の図)と同じ角度が取れました。
<人間の色相は?>
では人間の肌の色の色相はどうなるか確認してみます。(条件:夕方、室内)
手
手にライトあて
手(暗)
顔
顔遠く
顔(逆光)
顔(光でとばし)
肌色造形物(逆光)遊星からの物体Xです。
肌色造形物(順光)
深井中尉(印刷物)
深井中尉(暗)
ということで、大体赤〜黄を中心とした色相が得られました。造形物や印刷物の場合は環境光による影響もあまり無かったのですが、人間の場合は環境光による変化が激しかった。特に顔は35°〜225°と、広範囲の値が出てしまいました。
ライトでトバしたりして明るい方に変化させると緑系の色と判断してしまうことがわかりました。逆に暗い系に変化させると赤方向、つまり逆の角度に向かいます。ライトの色にもよるかもしれませんので、その辺りも考慮する必要がありそうです。
ここまでで考えられるのは、基本は120度ぐらい(赤と黄色の間)で検知し、対象が明るい場合は角度範囲を大きく(緑方向)、暗い場合は角度を小さく(赤、マゼンダ方向)すればよいかなあ、という感じでしょうか。明るいか暗いかはYの値だけで簡単に判定できます。
日本人=イエローモンキーといわれますが、実際には赤に近い色相でした。印刷では黄色系でした。白人だったら変わるかもしれません。黒人だったら間違いなく変わりそうな気がします。となりにオーストラリア人が引っ越してくる予定なので機会があったら実験してみます。
家族でも実験しましたが、私の値とほぼ同じでした。
人検知のプログラムその1
では実際に動体+色相でプログラムを組んでみます。これで良いかは別として...
プログラムは、画像表示の部分に、前回のYの値と比較する部分と、比較結果が値50を超えていたらHueを判定して100〜200の間ならそのピクセルを赤く描画する、という部分を追加しました。アルゴリズムは簡単なので説明を省略します。実際のプログラムはこちら
S-EYE_HumanDetect3.lzh (C#2008ExpressEditionのプロジェクト)
結果はこうなりました。
まず、静止状態だと動体検知の判定条件にひっかからないので、反応しない...そりゃそうだ。ということでビミョーです。
手を動かしてみると、前回肌色でなかった部分はきちんと検知しました。しかし、後ろの本棚も肌色に近いので検知してしまってます。
これなら、動体検知をしないほうがまだましかもしれません。
人検知のプログラムその2
動体検知をやめてみました。
うーん、この場合でも人は検知しますがその他の肌色系の物体も検知してしまいますね。
動体検知は入れないで、さらにアルゴリズムを追加するといけそうな気がします。
人検知のアルゴリズムの検討その3
上の結果で気が付いた点として、顔は丸にちかい形だということです。上の場合、赤で描画した領域をなんとか領域ごとにわけて、円形度を測定し、あるていど円にちかかったら顔である、というアルゴリズムを考えました。
まず、領域にわけるにはそれぞれの領域が連続していなくてはなりませんが、上の結果からもわかるとおり、ごましお雑音のような形になっているのできれいに領域ごとにわかれていません。これには「膨張」−>「収縮」で対応できると思います。
1)肌色の部分を抽出してニ値化(対象かそうでないかの2つの状態)
2)各領域にわかれたら境界線を検索して領域ごとに番号をつけます。(ラベリング)
3)その後、各番号ごとに彩度領域を検索して円形度を測定します。
これでやってみます。
ニ値化
肌色領域を判定した後、はカラー情報及び輝度情報が必要ありません。「対象かそうでないか」の2つの値だけに注目すればよいことになります。
膨張と収縮を含む以後の処理は、結果画面を何度もスキャンして加工するので、計算の負担を軽くするために、作業エリアを「対象かそうでないか」のニ値で表すことにします。
具体的には、画面サイズである80x60の配列を作り、対象でない場合は一番明るい色(255)、対象であった場合は一番暗い色(0)を描画することにします。モニタ上に結果を反映する場合は、Rだけ明るい色にするとその部分だけ赤になります。とりあえず結果は赤描画することにします。
ニ値化の際には、各データ構造に注意します。S−EYEから送られているデータは2ピクセル4バイト、ニ値化したデータは1ピクセル1バイト、モニタに描画するときは1ピクセル3バイトになります。
また、S−EYEからのYUVデータは色差がサブサンプリング(間引き)されているので実際にはとなり同士のピクセルは同じ値を使います。文章で書くとおそらく理解不可能だと思いますので、概念図を下に示します。
ということで、YUVでの色判定で二値化する場合は実際には横方向が1/2のデータ(この場合、実質は40x60のデータ判定になる)判定になります。ただし、ニ値化配列としては後で画面表示することも考慮して80x60で作っておき、色判定の結果を横方向に2倍(隣接を同じ結果に)してます。
実際にプログラムしたのが次の部分です。
一応、ループは80x60で行ってますが、x方向だけ2づつ進めています。行174の部分。実際に格納する場合は隣接2ピクセルを同じ値にしてます(行178−179と行183−184の部分)
結果はとりあえず80x60でworkという配列に格納してます。対象ありの場合は255、なしで0の2値で格納します。
膨張と収縮
まずは膨張と収縮です。1ピクセルごとに膨張させた後に、1ピクセル縮小すると、結果的にごましお雑音が消えることになります。
<膨張>
膨張のアルゴリズムは「周囲9ピクセルを検索し、1ピクセルでも対象があったら自分を対象とする」というものです。これを行うと、実質は1ピクセルづつ膨張することになり、ごましお雑音のようなドット抜けが埋まります。
ただし、画像の最外周ピクセルは周囲が無いので判定できないので、ここではとりあえず対象外としています。
膨張の部分のプログラムが次のようになります。
ここまでをプログラムしたのが次になります。
S-EYE_HumanDetect4.lzh (C#2008ExpressEditionのプロジェクト)
実際に動作させたところです。前よりもノイズが消えて、各領域が比較的まとまっています。
ここでふと思ったのですが、プログラムその2は昨日の夕方で、現在朝になっています。色あいが結構かわったわりにはニ値結果があまりかわらないので、肌色の判定条件をしぼっても顔を検知するのではないかと考えました。
そこで、肌色判定を100〜200度としていたところを100〜150度にしたところが次の画面です。
条件を絞っても顔は判定されてます。変わりに人以外の部分は領域が狭くなったので良い感じです。
<収縮>
収縮は膨張の逆で、「周囲に1つでも対象外があったら自分を対象外にする」というロジックで、結果として1ピクセル収縮になります。
ということで縮小を追加して実行しました。
ところが今度は、先ほどよりも明るくなってきて(朝6時−>8時に変化。晴れ)色合いがまったくかわってしまったために人を検知しなくなってしまいました。中心部のHueを見ると202度になってます。やはり明るくなると緑方向にシフトするようですね。明るさによる修正についてはとりあえず後回しにして、ここでは縮小を考えます。
ということで早朝と同じような環境にするためにカーテンを閉めました。
結果、こうなりました。目、鼻、口の部分は対象外になりますので、縮小すると顔に大きな穴が開いてしまい、あまり良くないという結果になりました。
縮小をやめて、膨張だけにもどしたところです。この方がまだマシな感じです。ということで縮小はやめました。
と、思ったら、曇ってきて、おそらく昨日及び今朝と同じ環境になったようで、縮小しても顔も検知できるようになりました。ということで縮小を入れたまま続行します。
人間の目は自働的に明るさを調整してしまうので変化がわかりませんが、外を見ると先ほどよりもかなり暗くなってきたようです。ということで客観的に明るさを測定する方法も交えて認識する必要があるような気がします。照度センサーとの組み合わせなどでしょうか。最近では可視光だけに反応するcds(のようなもの)も出ているようです。
検討をつづけます。上の画像ですと、もう一度縮小すればさらに領域数が減るのではと思いましたので縮小を2回実行してみます。
プログラムは、収縮と膨張のメソッド(関数)に引数をつけて、ワーク用配列を2つ渡して変換するように改良しました。これで収縮を連続で行ったり、ができます。行131,132が実際に収縮を指示しているところで、同じ処理を2回おこなってます。
結果、このようになりました。
この環境だと、収縮を繰り返すだけで顔だけ抜けそうですね。ということでもう一度収縮してみます。
あまりかわりませんでした。顔の形もくずれました。ということで現状ですと2回ぐらい収縮するとちょうどよいのではという感じです。
<明るさの検討>
現状でカーテンを開けてみました。
結果、このようになりました。反応ほとんどなし。
よく考えてみると、これは画像が飛びすぎです。カーテンをあけましたが外は曇り。もっと明るい状態でも普通に撮影できていた時もありました。AE(自動露出)やAWB(オートホワイトバランス)はPC側プログラム起動時に切ってますが、センサー自体が電源ON時にある程度は明るさを測定してセンサーの基準設定を決めるという気がしましたのでためしにセンサーボード側の電源をOFFしてみました。(電源を入れたのは早朝だったので明るめに調整されているのでは?と仮定した)
結果、上の環境とまったく同じにもかかわらず、明るさがちょうど良いように修正されました。ということで、S-EYEの電源ON時にある程度センサーの基準照度が設定されているようです。
ということで、「電源を切り入りすれば明るさによるHueの変化はなくなる。」という結論に達しました。よって、色あいの問題は解決です。
カーテンを開けた状態だと、外からの光で顔の半分が違った色合いになってしまうので、ここではカーテン閉め状態に戻します(カーテンを閉めて電源OFF−ONした。)
ラベリング
次はラベリングです。検知した領域に番号を付けて、領域ごとに分ける作業です。
とりあえずセンサーの電源を切り入りして環境を調整して撮影しました。
これを見ると、顔以外に本棚部分に数箇所の領域があることがわかります。現在は肌色の部分を”255”とした二値のデータだけになっていますが、後に円形度を測定するのに、それぞれの領域を区別しておく必要があります。
領域を区別する方法に、境界線追跡があります。
<境界線追跡>
ニ値化した画像の境界線を調べる場合、いろいろな方法がありますがここでは画面をスキャンする方法で実験してみます。
二値化した画像は通常は左上から右下にむかって順に検索していきます。この方法でスキャン中に対象が見つかった場合、初めにひっかかるのは領域の左上ということになります。スキャンは、検索を終了したところも検索すると上書きしてしまう恐れがあるので、元画像と結果画像の両方を使って判定します。元画像が対象で、結果画像で一回も境界線と判定されていないピクセルがあった場合、そこが領域の左上となります。
その後、その初めのピクセルを基点に時計回り又は反時計回りで周辺を検索し、対象があったらその部分をマークしていきます。検索方法もいろいろありますが、ここでは反時計回りに境界を検索する方法を考えます。
検索は、来た方向から向かって右手から反時計回りに対象があるかスキャンします。
スキャンは元のニ値化データをもとに、別な配列に記録します。たとえば、上の画像をこのパターンで検索する場合、まず右から来たのでケース1となり、次に検索するのは下になります。下も対象なので下をマークした上で、現在位置を下に移動します。
次は上から来たので、ケース3となります。ケース3は左−左下−下...の順に検索となります。この順番ですとまず左がひっかかります。
次はケース5となり...というようにして外周を検索できます。開始点にもどったら終了となります。
作ったプログラムが次の通りとなります。
S-EYE_HumanDetect5.lzh (C#2008ExpressEditionのプロジェクト)
以下、ポイントだけ説明します。
ここでは領域の左上か?を検索しています。左上が見つかったらdrawBorder8という関数で境界線を追跡しています。
行345ですが、もし、対象ピクセルが孤立していた場合は上で示したような反時計回りでの検索だと無限ループになる可能性がありますので、周辺4近傍(上下左右)を見て孤立していないかチェックしています。
これは実際に境界線追跡をする部分です。
方向は数値で表しています。
どちらの方向から来たか?次にどこを探索するか?はこの方向コードを使っています。詳細はプログラム中のコメントを参照願います。
ということで結果はこうなりました。
めでたく、肌色系の部分の境界を分けることができました。表示ではわかりずらいですが、実際にはwork5の80x60の配列には、各境界線が違った番号で書かれています。また、境界の長さはborderLenという変数に格納されています。境界線1番で描画した部分の長さはborderLen[1]に、というように境界ごとの長さも計測しています。長さは縦横方向は1、斜め方向はルート2(1.41)で計算しています。
拡大したところ。
<境界線の塗りつぶし>
円形度を計算するには面積と外周長が必要です。面積を求めやすくするために、境界線内部を同じ番号で塗りつぶしておきます。
今、境界線が書き終わったとします。元画像と結果描画のワークエリアは上の図のような感じになっています。これはワークエリア2つを重ねたイメージです。ピンクの部分が元画像(膨張収縮済み画像)、数字が境界線を番号で描画した結果とします。
注目するところは、「元画像が対象になっていて、結果画像に数字が書き込まれていない」(上の図だとピンク色で数字が無い)部分です。ここは「元画像が対象で、結果画像に描画が無い場合は、結果画像1つ左の数字を書き込む」というロジックで埋めることができます。
左上からスキャンしてくると、まず上の赤の部分が塗りつぶしていない最初の部分で判定されます。結果画像の1つ左には「1」が書かれているのでここを1にします。
次に塗りつぶしていないと判定されるのは上の赤の部分です。このときすでに1つ左は1が描画されているので、同じ論理で1つ左の値を自分の場所に書込みます。
という繰り返しで領域ごとに塗りつぶしが可能です。
プログラムは次のようになります。特に説明は不要かと。
円形度
気が付いたら暗くなってきました。センサーを切り入りして明るさ調整しました。
次は円形度です。円形度は形状の複雑さを示す度合いで、形の面積と外周長で計算できます。式は
この結果は1に近いほど円に近い形になります。
<面積の計算>
ここまでで外周長は記録されているので、面積を測定すればよいことになります。
面積は単純にピクセルをカウントするだけです。境界番号ごとにboderSという変数に面積を格納しました。
実際のプログラムは次のようになりました。
<円形度の計算>
ここまでくればデータがそろっているので後は計算するだけです。小数点演算になることに注意してください。
とりあえず、境界番号は100個以上にはならないだろうということで、境界番号100までを計算しています。この辺、もうちょっと良いロジックがあるような気がします。
最終的には一番円形の番号がわかればよいので、mostRoudNumberに1に近い境界番号が記録されるようになってます。そしてこの関数自体はそれを返します。
ということでできたプログラムが次の通りです。
S-EYE_HumanDetect.lzh (C#2008ExpressEditionのプロジェクト)
結果、このようになりました。
成功?でしょうか。
条件を変えて測定
今のところ、実際には人というよりも「画面中の肌色系の物体で、一番円形のもの」を検知するようになってます。ではいろいろと条件を変えて測定してみます。
横向きました。円形度が一番高くないようなので判定されません。ちなみに、ここで認識されている部分のように非常に小さい範囲ですと円形度の計算は本当はできません。小さいと円でなくても円に近い数値が出てしまう場合があるので、ある程度小さかったら外す、というロジックを入れた方がよいかも。
深井中尉(印刷)。顔の輪郭に黒線が入っているので、実写と違って肌部分に明確な区切りがついて円形になりづらいです。上は顔でなく首の部分を認識中。顔と首が明確に区切られてます。
物体Xと勝負。勝ちました。物体Xはツノがついているのが敗因のようです。
パーは形状が複雑なので認識しません。
グーで認識しました。
このように複雑な形だと、一番円形とはいえ円形度が低いです。円形度が0.4以下になってますね。(テキストボックス内に円形度を表示している。)
対象が小さいとこのように、見た目まったく円ではないにもかかわらず、円に近い数値が出てしまいます。
所感
時間があったので、今回のサンプルプログラムは結構いいところまで詰めたと思いますが如何でしたでしょうか?丸二日かけてしまいました。
これで顔認識といえるかどうかは別として、実際に画像処理でなにかをしようと思ったら、実際にシステムを構築し、環境を変えていろいろ実験しなくてはならないことがわかるかと思います。今回は上手くいっているようですが環境をできるだけ同じ(カーテン閉めっぱなし)にしたもので、これがロボット大会会場や、屋外になったらまったく違うロジックになるかもしれません。
もっと違うロジックで人検知を実験してみたい気もしますが、今回はここまでで。
話は変わりますが、実験用のロボは身体制御と画像と2つのBluetooth回線でやりとりしてます(PC側は1つ)ので頭からBT20Eが2個突き出してます。決してツインテールロボが作りたかったわけでは...
200年1月29日
|