意識したまばたきと無意識のまばたきを見分ける — 閉眼時間でハンズフリー入力を作る
手を使わずに機器を操作したい——両手がふさがる作業中、手袋をしている現場、手指を動かしにくい方の操作補助、あるいはゲーム。そうした場面で「まばたきをクリックの代わりに使う」という発想は自然です。
ただし、まばたきを入力に使うには1つ大きな問題があります。人は意識しなくても、1分間に十数回まばたきをしていることです(安静時の平均 17回/分という報告があります:Bentivoglio ら, 1997)。まばたきをそのまま入力にすると、数秒おきに誤作動します。
本記事では、閉眼時間で意識したまばたきと無意識のまばたきを分ける設計を、動くコードと実測データで説明します。
1. 方式の候補
| 方式 | 操作 | 誤作動 | 操作する人の負担 |
|---|---|---|---|
| まばたき1回 | 普通にまばたき | 非常に多い(無意識のまばたきと区別できない) | 小 |
| 2回連続(ダブルブリンク) | 短い間隔で2回 | 中(無意識でも続けて2回することがある) | 中 |
| 長めに閉じる | 一定時間以上、目を閉じる | 少ない | 小〜中 |
| 片目だけ閉じる(ウインク) | 左右どちらかだけ | 少ない | 大(できない人がいる) |
本記事では「長めに閉じる」を採ります。無意識のまばたきは短く、意識して閉じると長くなる、という差を使います。片目の動作は誤作動に強い一方で、ウインクが苦手な人には使えません。
当社が公開しているブラウザゲーム「まばたきシューティング」も同じ考え方で、約0.35秒以上閉じたときだけ発射し、ふだんの短いまばたきでは発射しない設計にしています。
2. 設計で決めること
| 項目 | 本記事の値 | 考え方 |
|---|---|---|
入力とみなす閉眼時間 HOLD_S |
0.35 秒 | 短すぎると無意識のまばたきで誤作動し、長すぎると操作が疲れる |
| 発火のタイミング | 閉眼が HOLD_S に達した瞬間 |
目を開けるのを待たないので反応が速い |
入力ではないとみなす上限 MAX_S |
2.0 秒 | これ以上閉じ続けたら「目を休めている」「眠っている」とみなし、入力として数えない |
| 開閉の判定 | 目の縦横比(EAR)+相対しきい値 | 詳細は OpenCV でまばたきを検出する(EAR 方式)と、その限界 |
発火のタイミングには2つの選び方があります。
| タイミング | 利点 | 欠点 |
|---|---|---|
閉眼が HOLD_S に達した瞬間(本記事) |
反応が速い。操作した手ごたえがある | 長く閉じた場合も、いったん発火してしまう |
目を開けた瞬間に、閉眼時間が HOLD_S〜MAX_S なら発火 |
長い閉眼を確実に除外できる | 目を開けるまで反応しない(体感で遅い) |
ゲームや選択操作のように反応の速さが大事なら前者、取り消しのきかない操作(送信・決定など)なら後者が安全です。
3. コード(Python)
開閉の判定は、EAR 方式の記事と同じ YuNet + FacemarkLBF です。閉眼が続いたフレーム数から時間を出し、HOLD_S に達したら入力を発火します。
# 意識したまばたきだけを「入力」として拾う(閉眼時間で区別する)
# 開閉の判定は EAR 方式(YuNet で顔検出 → FacemarkLBF で68点 → 目の縦横比 → 直近の中央値×0.75 を下回ったら閉)
# 閉眼が HOLD_S 秒に達した瞬間に「入力」を1回発火する(目を開けるのを待たない=反応が速い)
# HOLD_S に届かずに開いたものは「ふだんのまばたき」として数えるだけで、入力にしない
# MAX_S 秒を超えて閉じ続けた場合は、入力ではなく「目を閉じて休んでいる」とみなして取り消す
# pip install "opencv-contrib-python>=4.10,<5" ← cv2.face は contrib 版にだけ入っている
import sys
from collections import deque
from statistics import median
import cv2
import numpy as np
def eye_aspect_ratio(p, s):
"""目の6点から EAR を求める(Soukupová & Čech, 2016)"""
d = lambda a, b: float(np.linalg.norm(p[a] - p[b]))
return (d(s + 1, s + 5) + d(s + 2, s + 4)) / (2.0 * d(s, s + 3))
HOLD_S = 0.35 # これ以上閉じたら意識したまばたき(入力)
MAX_S = 2.0 # これ以上閉じ続けたら入力ではない(目を閉じて休んでいる・居眠り)
def main():
if len(sys.argv) < 2:
print("usage: python blink_input.py <video file | camera index>")
return
src = sys.argv[1]
cap = cv2.VideoCapture(int(src) if src.isdigit() else src)
if not cap.isOpened():
print("cannot open", src)
return
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
ratio = 0.75 # 中央値の何割を下回ったら閉眼とみなすか
history = deque(maxlen=round(fps * 3)) # 直近約3秒分の「開いている時」の EAR
detector = cv2.FaceDetectorYN.create("face_detection_yunet_2023mar.onnx", "", (320, 320))
facemark = cv2.face.createFacemarkLBF()
facemark.loadModel("lbfmodel.yaml")
frame_no = natural = inputs = 0
closed_frames = 0 # 連続して閉じているフレーム数
fired = False # この閉眼で入力を発火したか(False: まだ / True: 発火済み / None: 休眼として取り消し済み)
durations = [] # ふだんのまばたきの閉眼時間(秒)
while True:
ok, frame = cap.read()
if not ok:
break
# 大きいフレームのままだと YuNet のスコアが下がって顔を見落とす → 幅640に縮めて検出し、座標を戻す
scale = 640.0 / frame.shape[1] if frame.shape[1] > 640 else 1.0
small = cv2.resize(frame, None, fx=scale, fy=scale)
detector.setInputSize((small.shape[1], small.shape[0]))
_, faces = detector.detect(small)
ear = -1.0
if faces is not None:
box = np.round(faces[0][:4] / scale).astype(np.int32).reshape(1, 4)
ok, landmarks = facemark.fit(frame, box)
if ok and landmarks[0].shape[1] == 68:
p = landmarks[0][0]
ear = (eye_aspect_ratio(p, 36) + eye_aspect_ratio(p, 42)) / 2.0
if ear > 0:
now_closed = len(history) > 0 and ear < median(history) * ratio
if now_closed:
closed_frames += 1
held = closed_frames / fps
if fired is False and held >= HOLD_S:
fired = True
inputs += 1
print(f"INPUT #{inputs} at {frame_no / fps:.2f} s")
if fired is True and held > MAX_S:
print(f" ↳ {MAX_S} 秒を超えて閉じているため、入力ではなく休眼とみなす")
inputs -= 1
fired = None # この閉眼の間はもう発火しない
elif closed_frames:
if fired is False:
natural += 1
durations.append(closed_frames / fps)
closed_frames, fired = 0, False
if not now_closed: # 基準は「開いている時」の値だけで作る(長く閉じても基準が下がらない)
history.append(ear)
frame_no += 1
print(f"frames={frame_no} fps={fps} 入力={inputs} ふだんのまばたき={natural}")
if durations:
print("ふだんのまばたきの閉眼時間(秒):", ", ".join(f"{d:.2f}" for d in durations))
if __name__ == "__main__":
main()
必要なもの:
pip install "opencv-contrib-python>=4.10,<5"
face_detection_yunet_2023mar.onnx(OpenCV Zoo)、lbfmodel.yaml(Facemark の学習済みモデル)
実行例:
python blink_input.py 0 ← カメラ
python blink_input.py sample.avi
MAX_S を超えたときは、発火済みの入力を数のうえで取り消しているだけです。実際のアプリで「取り消せない操作」に使う場合は、§2 の「目を開けた瞬間に発火」に切り替えてください。
4. 実測: フレームレートが低いと、しきい値が効かない
意識して長く閉じる指示をしていない動画(まばたきを繰り返しているだけの動画・9fps)で、このコードを動かしました。
| 動画 | 短い閉眼として数えた回数 | 入力と判定された回数 | 短い閉眼の閉眼時間(秒) |
|---|---|---|---|
| PC 内蔵カメラ(14.4秒) | 14 | 1 | 0.11〜0.33 |
| スマートフォン(6.3秒) | 5 | 0 | 0.11〜0.33 |
長く閉じる操作はしていない動画ですが、1回が入力と判定されました(目視では、ほかのまばたきと同じ程度の短いまばたきでした)。また、多くの閉眼時間が 0.11・0.22・0.33 秒と、111ms 刻みでしか出ていません。
原因はフレームレートです。9fps では1フレームが約111ms なので、
- 閉眼時間は「閉じていたフレーム数 × 111ms」でしか測れない。実際は 0.25 秒程度のまばたきでも、フレームの境目のかかり方しだいで2フレーム(0.22秒)にも3フレーム(0.33秒)にも見え、閉じかけのフレームが閉と判定されればさらに1フレーム長くなる
- しきい値 0.35 秒は「3.15フレーム」なので、実際には「4フレーム以上」の判定になり、1フレームの違いで入力になるかどうかが決まる
閉眼時間で区別する方式は、時間を細かく測れることが前提です。30fps なら1フレームは約33ms で、0.35 秒は約10フレーム分になり、1フレームのずれの影響は1割程度に収まります。
| fps | 1フレーム | 0.35 秒は何フレームか | 1フレームのずれの影響 |
|---|---|---|---|
| 9 | 111 ms | 3.2 | 大きい(判定が揺れる) |
| 15 | 67 ms | 5.3 | 中 |
| 30 | 33 ms | 10.5 | 小さい |
入力に使うなら、実際に 30fps 前後が出ていることを確認してください。部屋が暗いとカメラは自動で fps を落とします(確かめ方は カメラでまばたきが反応しない・誤検出する原因)。
5. しきい値を人に合わせる
0.35 秒は出発点で、最適な値は人によって違います。実際に組み込むときは、次の手順で合わせるのが現実的です。
- ふだんのまばたきを30秒ほど記録し、閉眼時間の分布を見る(上のコードが最後に一覧を出します)
- ふだんのまばたきの最長値より十分長い値(目安として最長値の1.5〜2倍)を
HOLD_Sにする - 実際に操作してもらい、「押したつもりなのに反応しない」と「押していないのに反応した」の回数で微調整する
操作に慣れると、意識した閉眼は短くなっていく傾向があります。長く使うアプリでは、しきい値を設定画面から変えられるようにしておくと親切です。
6. 使う場面ごとの注意
| 場面 | 注意点 |
|---|---|
| ゲーム | 反応の速さが大事。発火は HOLD_S 到達時。連打を想定し、入力後すぐに次を受け付ける |
| 選択・決定の操作 | 誤作動の影響が大きいので、発火は目を開けた時に。決定前に確認の一手を挟む |
| 長時間の作業 | 疲れてくると無意識の閉眼が長くなる。MAX_S で休眼を除外し、しきい値も見直せるように |
| 操作補助(手指を動かしにくい方) | 本人のまばたきの特徴に合わせた調整が必須。調整の手順を用意する |
7. 閉眼時間を測るなら、開閉判定の安定性が前提
閉眼時間でまばたきを区別する設計は、「閉じている間ずっと閉眼と判定され続ける」ことが前提です。顔の特徴点から目の形を測る方式(EAR)では、閉じている途中で値が1フレームだけ戻り、1回の閉眼が2回に分かれることがあります。当社の検証でも、13回のまばたきのうち3回がこの形で分割されました。分割されると閉眼時間が短く測られ、意識したまばたきが HOLD_S に届かなくなります。
当社のまばたき検出SDKは、目の画像をディープラーニングのモデルに入れて開閉を判定する方式です。同じ動画では、分割による誤検出は0回でした。当社の動作確認用の動画3本での結果で、条件によって変わります。(比較の詳細)。下で紹介しているブラウザゲームも、このSDKで動いています。
まとめ
- まばたきを入力に使う最大の問題は、無意識のまばたき(安静時で平均 17回/分という報告)
- 「長めに閉じる」を入力にすると、誤作動を抑えつつ誰でも操作できる。本記事は
HOLD_S = 0.35秒 - 発火は「閉眼が
HOLD_Sに達した瞬間」(速い)か「目を開けた瞬間」(安全)かを用途で選ぶ - 長すぎる閉眼(
MAX_S超え)は休眼として入力から除外する - 9fps では閉眼時間が 111ms 刻みになり、しきい値が効かない(実測で無操作でも1回誤作動)。30fps 前後を確保する
🎮 まばたきで操作する体験を試してみる
この設計で作ったブラウザゲーム「まばたきシューティング」を無料で公開しています。インストール・会員登録は不要で、PC・スマートフォンのブラウザで遊べます(カメラ映像は端末内で処理し、外部に送信しません)。判定にはまばたき検出SDKと同じ技術を使っており、精度は試用版アプリ(ブラウザ版)でも確かめられます。

