視線追跡は、赤外線の目による反射を使って角膜の曲率中心を求める方法が長く主流でした。これは、1度くらいまでの高精度な推定が得られるという利点があり、一方、1mくらいまでの距離での撮影と使い方が限定され、外界だと赤外線ノイズに邪魔されるという欠点があります。
一方で、赤外線を使わずに、外見だけで視線を求める流れもありました。これは、カメラから被写体までの距離制約が小さく、利用シナリオが多いという利点があり、一方、得られる精度がチャレンジという欠点があります。
後者で、ニューラルネットで目の外見と視線を直接対応学習させる流れもありますが、学習データの整備という問題があります。
一方、後者の外見から攻めるアプローチには、眼球モデルを設定し、他方で黒目(虹彩)の中心をとらえ、視線を計算する方法もあります。そのとき、黒目の中心を求める方法として、以下のような方法がありました。
1.黒目あるいは瞳の境界を検出して、楕円で近似し、楕円のゆがみを踏まえて、法線を求める。
2.黒目の中心を求めて、他方で眼球中心の推定を行い、両者から視線を求める。黒目の中心は、
2-1.黒目(虹彩)の境界や瞳の境界が円形であることを利用して、観察された複数の円形から中心を推定する。
2-2.明暗のGradientに注目し、Gradientベクトルの方向に無限長の線を引いて、線の密度のもっとも濃い点を、黒目の中心とする。
ところが、以上のような、これまでに公開された有名な方法を見ると、どうも、どれも、黒目が明確に見えていることが前提ですね。実用までケアしないリサーチなら構わないかもしれないが、これじゃ、実際、だめじゃん。東洋人の細目だと、黒目あるいは瞳の円・楕円の一部は瞼で隠れていることが多い。そのため、既存の有名な方法はどれも使えません。
しかし、2-2の方法を少し変形して、Gradientの負の方向(白目の明るいほうから黒目の暗いほう)に虹彩の直径(~半径)の生体的統計(虹彩の大きさはかなり一定であることが知られている)の長さ程度の線を引き、その密度の中心を求めます。その変形だと細目でも、横方向のGradientが効いて、いい感じで黒目の中心が得られます。
特許出願のクレームの一つにするか迷った末に、小粒なテクなので、実施形態で明示するにとどめました。が、黒目中心を検出する方法として、東洋人の目でも安定して有効です。以下にPythonコードを載せます。似たようなことを目指している方のために、ここに残しておきます。ご自由にご利用ください。
...
gvcnt = get_gvLines(img, mask)
maxIdx = np.unravel_index(gvcnt.argmax(), gvcnt.shape)
...
def get_gvLines(img, mask):
# variant of Kothari and Mitchell method,
# in "ACCURATE EYE CENTRE LOCALISATION BY MEANS OF GRADIENTS"
GRADIENT_MIN = 100
GRADIENT_MAX = 9999
h, w = img.shape[:2]
img_pyr = cv2.pyrDown(img) #結構、重いので画像を1/4へスケールダウン
mask_pyr = cv2.pyrDown(mask) #まぶたや目頭、目尻からのノイズを避けるため、できるだけ検査領域をしぼりこんでおくためのマスク
h, w = img_pyr.shape[:2]
gx = cv2.Sobel(img_pyr, cv2.CV_32FC1, 1, 0) # x derivative
gy = cv2.Sobel(img_pyr, cv2.CV_32FC1, 0, 1) # y derivative
gvcnt = np.zeros((h, w), dtype=np.uint8) # count points with black background
for i, value in np.ndenumerate(mask_pyr):
v = np.array([gx[i], gy[i]])
n = np.linalg.norm(v)
if mask_pyr[i] == 0:
if GRADIENT_MIN < n < GRADIENT_MAX:
v = (v / n) * IRIS_SIZE_IN_PIXELS / 2
pstart = (int(i[1] - v[0] * 0.3), int(i[0] - v[1] * 0.3))
pend = (int(i[1] - v[0] * 0.6), int(i[0] - v[1] * 0.6))
# About Bresenham class:
# Pypedia version (bresenham.py) is slow but accurate
# Python package version https://pypi.python.org/pypi/bresenham/0.1 is
# super fast. But it is inaccurate or requires other parameters
line = bresenham.bresenhamline(np.array([pstart]), np.array([pend]), 2)
for l in line:
if l[1] < h and l[0] < w: gvcnt[l[1], l[0]] += 1
return cv2.pyrUp(gvcnt)
[2021/02/25追加]なお、このコードで、bresenhamを使っているところ、OpenCVのGrayScale Imageの全pixelが0のやつにに、線を重ね書きしていくやり方のほうがSimpleです。OpenCVの線引きも、結局、Bresenhamのアルゴリズムを使っていると思うので。