アイトラッカー作ってみた．

* この記事はOpenCV Advent Calendar 2020の13日目の記事です。
* 他の記事は[目次](https://qiita.com/advent-calendar/2020/opencv "mokuji")にまとめられています。

# 概要
私はこれまで、弱視であるがゆえに様々な不便を感じてきました。しかし、情報技術の発展に伴い、この弱視と情報技術を組み合わせることによって、逆に弱視であるがゆえに便利な生活が送れるのではないかと考えました。試作を重ねた結果、弱視の性質とOpenCVを活用することで安価にアイトラッカーを作成できました。よって本投稿では、弱視を活用するために作成したアイトラッカーについてまとめます。

# はじめに

### 弱視とは
世には弱視と呼ばれる人が一定数存在します。のび太君のような目の悪さは、眼球のピントのズレ等によって引き起こされるので、眼鏡をかけることによってある程度軽減されます。
しかし、弱視は視神経自体が細かったり、網膜がうまく機能しない等によって引き起こされるので、眼鏡等を使用しても視力はあまり改善しません。

私の場合、視界すべてにバイラテラルフィルタ([大変わかりやすい解説](https://qiita.com/micky_dtl/items/9a5e1c3fb0e8b1be5fd9 "sample"))がかかったように見えます。



この弱視のややこしいところは、片目だけ弱視になる場合がある点です。こうなってしまうと、左右の目に視力差が生じるため、距離推定がすこぶる苦手になってしまいます。また、見える画像全体にバイラテラルフィルタがかかったようになるので、人の顔の判別も不得意になります。

### ではどうするか
アイトラッカーで視線を追うことでどの物体を見ているか大まかに特定し、高性能なカメラを用いてその物体を撮影すれば解決できると考えました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/6cdf47ff-04e6-62f9-ded1-9f4ca7a13adf.png)

距離推定が苦手な場合は、注目している物体をアイトラッカーで特定し、二台のカメラを組み合わせて三角測量を行うことで対象物への距離が推定できます。人の顔を特定したい場合は、適当なニューラルネットと組み合わせれば十分に解決できると考えたのです。

### なぜわざわざ作るのか

既存のアイトラッカーは確かに高性能ですが、高価であります。私にそんな高価なデバイスを買う余裕はありません。また、視力が非常に低いという弱視の特性を使えば、大変安価にアイトラッカーを作成できると考えたからです。

# 作ってみる

## ハードウェア編
コードでいくら工夫してもデータが汚ければうまくいかないので、きれいなデータを得られるハードをつくってみます。画像中に不自然なところがいくつかあります。これは、気持ち程度のプライバシー保護であって、結果の捏造ではありません。

### 試作一号機

弱視は視力が弱い≒ほとんど見えていない->目の前にカメラを置いても問題ない

という飛躍した論理のもと、適当なWEBカメラとメガネを組み合わせて作ってみました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/7d65b195-6295-4c60-ab37-975b97976103.png" width="300">

つけてみた。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/7cba20ee-5173-9e24-71a5-3357bad33c0e.png" width="300">

文字通り目の前にカメラを設置してみました。しかし、弱視でほとんど見えていないといっても、左目は平衡感覚にはある程度寄与しているらしく、装着後に大変酔ってしまったのでこの構造が不適であることがわかりました。

### 試作二号機

一号機はフレームが貧弱だったため、目の前にカメラを設置するしかありませんでした。そのため、フレームをスポーツサングラスやプラスチックアームなど頑丈な素材で作ってみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/50543f98-69c7-3bcc-fb3b-8995a6263989.png" width="300">

つけてみた。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/04b4cf1c-cc38-f871-9d62-e16dc68bc699.png" width="300">

一号機より下にカメラをつけたので、酔いなどの問題も発生しませんでした。そのため、このシステムをもとにコードを書いていきます。

## コード編

黒目と視線方向は一致しているため、画像解析によって黒目の中心を検出することを目標とします。
なお、ハードウェア本体に照明を取り付けることによって、二値化処理のみで黒目を抜き出すことに成功しました。


### 円検出
ほとんどの場合黒目は円形であるので、円検出で黒目の方向を特定できると考えました。そこで、ハフ変換を用いた円検出を試しました。

```Python3
import cv2
import numpy as np

img = cv2.imread("frame.jpg")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 照明を当てることで明るさを一定にしたので、固定パラメータでも黒目の検出が可能になりました。
ret,th = cv2.threshold(gray,65, 255,cv2.THRESH_BINARY_INV)


medimg = cv2.medianBlur(img,5)

circles = cv2.HoughCircles(medimg,cv2.HOUGH_GRADIENT,1,20,
                            param1=200,param2=13,minRadius=0,maxRadius=0)

circles = np.uint16(np.around(circles))
for i in circles[0,:]:
    # 外側の円を書きます
    cv2.circle(img,(i[0],i[1]),i[2],(0,255,0),2)
    # 内側の円を書きます
    cv2.circle(img,(i[0],i[1]),2,(0,0,255),3)

cv2.imwrite('detected.jpg',img)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/0fe3954b-38dc-898d-1001-d22b6e7232e0.png)

上の画像をこの処理にかけると下の画像のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/b545a949-9e4a-57fb-3711-bf07f63cb36d.png)

このように、ある程度は検出できているように感じられます。赤色が検出された中心です。

しかし、この処理のままでは複数の円を検出できるので、複数の視線方向が検出されます。つまり、正しい視線方向を求めることができません。また、ハフ変換はアルゴリズム的に多少遅いので、リアルタイム処理が求められるアイトラッカーには不向きであると判断しました。


### 重心検出
先ほどの円検出には、黒目を円としてしか検出できないという致命的な欠点があります。

下図ような黒目は円として検出してもなんら問題はありませんが、

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/2b09e2c8-1fbf-7042-42b7-1f5439303d0b.png" width="200">


下図のように黒目が偏った際に、黒目を円として検出することは適切ではありません。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/0661282e-0837-18ee-96a3-777fd7ca44bd.png" width="200">


そこで、偏りに対して頑健であると考えられる重心を用いての検出を試しました。

```Python3
import cv2

frame = cv2.imread("frame.jpg")

gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

# 照明を当てることで明るさを一定にしたので、固定パラメータでも黒目の検出が可能になりました。
ret,th = cv2.threshold(gray,65, 200,cv2.THRESH_BINARY_INV)

# 最大の面積を持つ輪郭を探します
_, contours, _ = cv2.findContours(th, 1, 2)
cnt = contours[0]
for c in contours:
    if len(cnt) < len(c):
        print(c)
        cnt = c

# 最大の面積を持つ輪郭の重心を求めます
M = cv2.moments(cnt)
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])

cv2.circle(frame, (cx, cy), 5, (0, 255, 255), thickness=-1)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/4bf42f63-49cc-61cb-0565-46e467108e15.png)

上の画像をこの処理にかけると、下の図のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302978/5a4a7c8f-0ba1-c3d9-c2e1-9302a0e39aba.png)

このように、綺麗に検出できました。黄色の点が検出された中心です。

# まとめ
* 弱視の特徴を使うと、大変安価にアイトラッカーを作成できる。
* 重心を用いることで、広範囲にわたる視線方向が検知できる。

# 終わりに
本投稿では、私が自身の不自由を解決するために作成してきたアイトラッカーについてまとめてきました。大変しょぼいまとめで恐縮ですが、不便と思われがちな障害とコンピューター(そしてOpenCV!!!!)を組み合わせることで、安価に身体機能を拡張でき、むしろ便利な生活が送れる可能性があることを知っていただけたら幸いです。

明日(12/14)は、@UnaNancyOwen さんの「OpenCVのDNNモジュールで深度推定してみる」です。ご精読ありがとうございました。

# 資料
## 動作環境
* OpenCV 3.6 (手持ちのノートパソコンを壊したので断言はできません。ごめんなさい。)

## 費用
|      |  フレーム  |  アーム(カメラ台)  |  カメラ  |
| ---- | ---- | ---- | ---- |
|  試作一号機  |  100円(部屋に落ちていた)  |  2円(割り箸)  |  200円  |
|  試作二号機  |  30,000円(Oakleyのサングラス)  |  10円(LEGOのパーツ)  |  200円  |