はじめに
2020/1/19(土)に開催された第56回 コンピュータビジョン勉強会@関東の発表内容をまとめました。
当日の資料はこちらから見れます。
ソースコードは以下のGithub上に公開しています。
https://github.com/33taro/gaze_cv
虹彩検出の手順について
虹彩検出は大学の頃、研究していたテーマなので、進化したOpenCVならお手軽にできなかなぁと思い実施しています。
手順としては次の通りです。
1.カメラ画像から人物の顔と顔のランドマーク検出
2.顔のランドマークから目領域を切り出し
3.目領域を2値化して虹彩領域を抽出
4.抽出した虹彩領域から虹彩検出
カメラ画像から人物の顔と顔のランドマーク検出
以前、別の記事で紹介した顔のランドマーク検出を利用して、虹彩(黒目の部分)を検出してみました。
詳細はそちらを参照してください。
顔のランドマークから目領域を切り出し
上記の記事でも紹介していますが、顔のランドマークはこちらにある学習済みモデルを利用しています。
そのため目領域は右目が「No.37~42」、左目が「No.43~48」となります。
こちらは実際のソースコードでは「tracking_system」ディレクトリの「eye_region_manager.py」に記載しています。
def detect_eye_region(self, face_landmark):
"""
ランドマークから目領域の座標を取得
:param face_landmark:
"""
# 右目切り出し
self._right_eye_region = {'top_x': face_landmark[36][0], 'bottom_x': face_landmark[39][0],
'top_y': face_landmark[37][1]
if face_landmark[37][1] < face_landmark[38][1] else face_landmark[38][1],
'bottom_y': face_landmark[41][1]
if face_landmark[41][1] > face_landmark[40][1] else face_landmark[40][1]}
# 左目切り出し
self._left_eye_region = {'top_x': face_landmark[42][0], 'bottom_x': face_landmark[45][0],
'top_y': face_landmark[43][1]
if face_landmark[43][1] < face_landmark[45][1] else face_landmark[45][1],
'bottom_y': face_landmark[47][1]
if face_landmark[47][1] > face_landmark[46][1] else face_landmark[46][1]}
※配列は0番から始まるため、0~67となり番号がひとつずれます。
※目領域の上下y座標は、より長くなるように取得しています。
目領域を2値化して虹彩領域を抽出
目領域の2値化にはPタイル法を用いました。
これは2値化したい領域が画像領域の何割を占めるか割合で指定する手法です。
これにより明るさに左右されず虹彩が取得できました。
(経験則から虹彩は目領域の4割にしています)
※2値化前にノイズ削除のためにガウシアンフィルタで平滑化しています。
Pタイル法はOpenCVに実装されていないため、自作しました、
「utility」ディレクトリの「image_utility.py」に記載しています。
# coding:utf-8
import cv2
def p_tile_threshold(img_gry, per):
"""
Pタイル法による2値化処理
:param img_gry: 2値化対象のグレースケール画像
:param per: 2値化対象が画像で占める割合
:return img_thr: 2値化した画像
"""
# ヒストグラム取得
img_hist = cv2.calcHist([img_gry], [0], None, [256], [0, 256])
# 2値化対象が画像で占める割合から画素数を計算
all_pic = img_gry.shape[0] * img_gry.shape[1]
pic_per = all_pic * per
# Pタイル法による2値化のしきい値計算
p_tile_thr = 0
pic_sum = 0
# 現在の輝度と輝度の合計(高い値順に足す)の計算
for hist in img_hist:
pic_sum += hist
# 輝度の合計が定めた割合を超えた場合処理終了
if pic_sum > pic_per:
break
p_tile_thr += 1
# Pタイル法によって取得したしきい値で2値化処理
ret, img_thr = cv2.threshold(img_gry, p_tile_thr, 255, cv2.THRESH_BINARY)
return img_thr
抽出した虹彩領域から虹彩検出
虹彩領域は取得できましたが、眉毛や瞼の影などがあり、虹彩だけをキレイに抽出できません。
そこで輪郭点追跡で黒色領域を取得し、各領域に足して外接円近似をし、半径が最大の円を虹彩としました。
※ただし外接円が大きすぎる場合、虹彩の候補から外します。
一連の2値化~虹彩検出は「tracking_system」ディレクトリの「eye_system_manager.py」に記載しています。
@staticmethod
def _detect_iris(eye_img):
# グレースケール化後、ガウシアンフィルタによる平滑化
eye_img_gry = cv2.cvtColor(eye_img, cv2.COLOR_BGR2GRAY)
eye_img_gau = cv2.GaussianBlur(eye_img_gry, (5, 5), 0)
# Pタイル法による2値化
eye_img_thr = p_tile_threshold(eye_img_gau, IRIS_PER)
cv2.rectangle(eye_img_thr, (0, 0), (eye_img_thr.shape[1] - 1, eye_img_thr.shape[0] - 1), (255, 255, 255), 1)
# 輪郭抽出
contours, hierarchy = cv2.findContours(eye_img_thr, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 輪郭から最小外接円により虹彩を求める
iris = {'center': (0, 0), 'radius': 0}
for i, cnt in enumerate(contours):
(x, y), radius = cv2.minEnclosingCircle(cnt)
center = (int(x), int(y))
radius = int(radius)
# 半径が大きすぎる場合、虹彩候補から除外
if eye_img_thr.shape[0] < radius*0.8:
# # 虹彩候補の描画
# cv2.circle(eye_img, center, radius, (255, 0, 0))
continue
# 最も半径が大きい円を虹彩と認定
if iris['radius'] < radius:
iris['center'] = center
iris['radius'] = radius
iris['num'] = i
return iris