カメラ画像から虹彩検出をしてみた

# はじめに

2020/1/19(土)に開催された[第56回 コンピュータビジョン勉強会＠関東](https://kantocv.connpass.com/event/160053/)の発表内容をまとめました。
当日の資料は[こちら](https://www.slideshare.net/NaoyaTakeuchi/ss-221273392?ref=https://kantocv.connpass.com/event/160053/presentation/)から見れます。

ソースコードは以下のGithub上に公開しています。
https://github.com/33taro/gaze_cv


# 虹彩検出の手順について

虹彩検出は大学の頃、研究していたテーマなので、進化したOpenCVならお手軽にできなかなぁと思い実施しています。
手順としては次の通りです。

1.カメラ画像から人物の顔と顔のランドマーク検出
2.顔のランドマークから目領域を切り出し
3.目領域を2値化して虹彩領域を抽出
4.抽出した虹彩領域から虹彩検出

![虹彩検出手順.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224034/7ddcbc04-c505-0c11-61b7-6f4e5b60a620.png)


## カメラ画像から人物の顔と顔のランドマーク検出

以前、[別の記事](https://qiita.com/mimitaro/items/bbc58051104eafc1eb38)で紹介した顔のランドマーク検出を利用して、虹彩(黒目の部分)を検出してみました。
詳細はそちらを参照してください。


## 顔のランドマークから目領域を切り出し

[上記の記事](https://qiita.com/mimitaro/items/bbc58051104eafc1eb38)でも紹介していますが、顔のランドマークは[こちら](https://ibug.doc.ic.ac.uk/resources/facial-point-annotations/)にある学習済みモデルを利用しています。
そのため目領域は右目が「No.37～42」、左目が「No.43～48」となります。

![目領域.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224034/d4f4e815-b952-b9e4-8651-a7e7de2e1e01.png)

こちらは実際のソースコードでは「tracking_system」ディレクトリの「eye_region_manager.py」に記載しています。

```python: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値化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224034/6ec50e3e-9a12-c608-5ebe-dfc2db0788a8.png)
※2値化前にノイズ削除のためにガウシアンフィルタで平滑化しています。

Pタイル法はOpenCVに実装されていないため、自作しました、
「utility」ディレクトリの「image_utility.py」に記載しています。

```python: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
```


## 抽出した虹彩領域から虹彩検出

虹彩領域は取得できましたが、眉毛や瞼の影などがあり、虹彩だけをキレイに抽出できません。
そこで輪郭点追跡で黒色領域を取得し、各領域に足して外接円近似をし、半径が最大の円を虹彩としました。
※ただし外接円が大きすぎる場合、虹彩の候補から外します。

![虹彩の外接円近似.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224034/84f4a6cb-4c30-ae4d-4582-c7678602c6c8.png)

一連の2値化～虹彩検出は「tracking_system」ディレクトリの「eye_system_manager.py」に記載しています。

```python: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
```

## 虹彩の検出結果

左右の目の虹彩はしっかりと取得できています。
しかし上下については課題が残っています。
ただし人が上下を見るときは虹彩を動かすより顔を動かす場合が多いです。
(実際に目だけで上下を見ると疲れます)
なので顔のランドマークから、顔の向きを検出してなんとかできないかなぁ、と考えています。

# 参考リンク

## 輪郭点追跡

* **OpenCV-Pythonチュートリアル ～輪郭: 初めの一歩～**
http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html
OpenCVのチュートリアルです。基本的な使い方の参考にしました。

* **OpenCV - findContours で輪郭抽出する方法**
https://www.pynote.info/entry/opencv-findcontours
OpenCVの輪郭点追跡機能findContours()の詳細が紹介されています。
ソースコード記述の際の参考にしました。

* **【OpenCV; Python】findcontours関数のまとめ**
https://qiita.com/anyamaru/items/fd3d894966a98098376c
OpenCVの輪郭点追跡機能findContours()の各機能について図を用いて分かりやすく解説しています。
本技術の理解の参考にしました。

## 最小外接円

* **OpenCV-Pythonチュートリアル ～領域(輪郭)の特徴～**<br>
http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html<br>
OpenCVのチュートリアルです。基本的な使い方の参考にしました。

## Pタイル法

* **イメージングソリューション ～Pタイル法～**
https://imagingsolution.net/imaging/p-tile-method/
Pタイル法について分かりやすく解説しています。
Pタイル法の理解と実装をする上で参考にしました。

* **画像処理入門・C言語サンプル集 ～Pタイル法～**
http://pgsample.info/image/other/PercentileMethod.html
C言語によるPタイル法の実装です。
今回pythonでPタイル法のコードを書く際の参考にしました。