カメラでまばたきが反応しない・誤検出する原因 — 照明・メガネ・フレームレート・顔の大きさ

まばたき検出
📌 準備: OpenCV の環境構築がまだの方はこちら → C++ 環境構築ガイド / C#(OpenCvSharp)セットアップ

カメラでまばたきが反応しない・誤検出する原因 — 照明・メガネ・フレームレート・顔の大きさ

カメラでまばたきを検出する仕組みは、作るのも使うのも手軽になりました。その一方で「目を閉じても反応しない」「何もしていないのに反応する」というのは、よくある質問です。

原因はたいてい、アルゴリズムよりも撮影条件にあります。本記事では、まばたき検出を4つの段に分けて、どの段で失敗しているかを切り分ける方法をまとめます。後半に、撮影条件を数値で確かめるスクリプトを載せました。


1. まばたき検出は4段でできている

段 やること ここで失敗すると
① 顔を見つける 画像の中から顔の位置を探す 何をしても反応しない
② 目の位置を決める 顔の中から目の領域・輪郭を特定する 反応が不安定・左右が入れ替わる
③ 開閉を判定する 各フレームで目が開いているか閉じているかを決める 閉じても反応しない/開いていても反応する
④ 時間方向にまとめる 閉→開の切り替わりを1回と数える 1回を2回と数える・短いまばたきを取りこぼす

「反応しない」と一口に言っても、①で顔を見失っているのか、③で開閉の差が出ていないのかで、対策がまったく違います。まず、どの段で止まっているかを確かめます。


2. 症状から原因を探す

症状 疑う段 よくある原因
まったく反応しない ① 逆光・顔が暗い・顔が画面からはみ出している・高解像度のまま顔検出している
ときどき反応しなくなる ①② 顔の向き・手や髪で顔が隠れる・照明の明滅
目を閉じても反応しないことが多い ③④ 顔が小さく写っている・フレームレートが低い・しきい値が合っていない
何もしていないのに反応する ③ メガネへの映り込み・下を向いた(伏し目)・笑って目が細くなった
1回のまばたきで2回反応する ④ 閉じている途中で判定が一瞬「開」に戻った
メガネをかけると精度が落ちる ②③ レンズの反射・フレームが目の輪郭にかかる

3. 原因ごとの確かめ方と対策

3-1. 逆光・暗さ(①③)

窓を背にして座ると、カメラは明るい背景に露出を合わせ、顔が暗く写ります。顔検出の確信度が下がるほか、目元の明暗差が小さくなって開閉の判定も難しくなります。

  • 確かめ方: 顔の領域と背景の平均の明るさを比べる。背景の方がはっきり明るければ逆光
  • 対策: 窓を背にしない。顔の正面から照明を当てる(モニターの明るさだけに頼らない)

当社の手元の検証動画(PC 内蔵カメラ・窓のある部屋)では、顔の明るさ 97 に対して背景が 157 でした(0〜255)。この動画は、次の 3-2 の対策をするまで顔検出がほぼ全フレームで外れていました。

3-2. 高解像度のまま顔検出している(①)

意外な落とし穴です。OpenCV の顔検出モデル YuNet は、1280×960 の原寸のまま入力すると、上の逆光の動画でスコアが 0.894 になり、既定のしきい値 0.9 にわずかに届かず、顔を見落としました。幅640に縮めてから検出すると 0.943 になり、全フレームで検出できました。

  • 対策: 顔検出は縮小した画像で行い、得られた座標を元の大きさに戻す。目の解析は原寸で行う

3-3. 顔が小さく写っている(③)

顔が小さいと目は数十ピクセルしかなく、瞼の上下の動きが数ピクセルの差になります。

当社の検証では、顔の幅 169px(画面幅の26%)の動画は、目視ではっきり目を閉じているのに、目の縦横比(EAR)がほとんど変化しませんでした。顔は全フレームで検出できていたので、①ではなく③の失敗です。顔の幅が 300〜400px あった動画では、同じ方法で正しく数えられました(詳細は OpenCV でまばたきを検出する(EAR 方式)と、その限界)。

  • 対策: カメラに近づく・解像度を上げる。目安として顔の幅 250px 以上(300px あれば安定)

3-4. フレームレートが低い(④)

まばたきは数百ミリ秒以下の短い動作です。9fps では1フレームが約111ms あり、閉じている瞬間が1フレームしか写らない、あるいは写らずに終わることがあります。「2フレーム以上閉じていたら1回」のような条件を付けていると、そのほとんどを取りこぼします。

もう1つ見落としやすいのが、部屋が暗いとカメラが自動で fps を落とすことです。設定は30fpsでも、露光時間を伸ばすために実際は15fps程度しか届いていない、ということがあります。

  • 確かめ方: 設定値ではなく、1秒間に実際に届いたフレーム数を数える
  • 対策: 照明を明るくする(露光時間が短くなり fps が戻る)。必要以上に高い解像度を選ばない

3-5. メガネ(②③)

レンズにモニターや照明が映り込むと、目元の明暗が乱れて開閉を誤判定します。太いフレームが目の輪郭にかかると、目の位置の推定もずれます。

  • 対策: 照明やモニターの角度を変えて映り込みを避ける。カメラを少し下から見上げる位置にすると、映り込みが減ることがある

3-6. しきい値が合っていない(③)

「目の縦横比が 0.2 を下回ったら閉眼」のような固定のしきい値は、人によって合いません。目を開いているときの値に個人差があるためです。当社の検証では、固定値 0.21 で数えられたのは13回中2回でした。その人の直近の値を基準にした相対的なしきい値にすると、13回中12回まで改善しました。

3-7. 伏し目・笑顔(③)

下を向いて手元を見る、笑って目が細くなる——これらは「目が閉じかけている」状態と見分けにくく、誤検出の原因になります。

  • 対策: 用途に応じて、閉眼と判定する時間の下限・上限を設ける。顔の向きの推定と組み合わせ、下を向いている間は判定を止める

4. 撮影条件を確かめるスクリプト

上の項目のうち、fps・顔の大きさ・明るさ・逆光・顔検出のスコアは数値で確かめられます。カメラ(10秒間)または動画ファイルを入力すると、問題がありそうな項目を表示するスクリプトです。

# まばたき検出の「撮影条件」を数値で確かめる
#   実際の fps / 顔の大きさ / 顔の明るさ / 逆光 / 顔検出のスコア を測って、問題がありそうな項目を表示する
# 使い方: python diagnose_blink_conditions.py <video file | camera index> [秒数=10]
# 必要なもの: pip install "opencv-python>=4.10"、face_detection_yunet_2023mar.onnx(OpenCV Zoo)
import sys
import time
from statistics import median

import cv2
import numpy as np


def main():
    if len(sys.argv) < 2:
        print("usage: python diagnose_blink_conditions.py <video file | camera index> [seconds]")
        return
    src = sys.argv[1]
    seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 10.0
    is_camera = src.isdigit()
    cap = cv2.VideoCapture(int(src) if is_camera else src)
    if not cap.isOpened():
        print("cannot open", src)
        return

    detector = cv2.FaceDetectorYN.create("face_detection_yunet_2023mar.onnx", "", (320, 320), 0.5)
    stamps, widths, face_lum, bg_lum, scores, missed = [], [], [], [], [], 0
    frame_size = None
    start = time.perf_counter()
    while time.perf_counter() - start < seconds or not is_camera:
        ok, frame = cap.read()
        if not ok:
            break
        stamps.append(time.perf_counter())
        frame_size = (frame.shape[1], frame.shape[0])
        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)
        if faces is None:
            missed += 1
            continue
        x, y, w, h = (faces[0][:4] / scale).astype(int)
        x, y = max(x, 0), max(y, 0)
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        mask = np.zeros(gray.shape, np.uint8)
        mask[y:y + h, x:x + w] = 1
        face_lum.append(float(gray[mask == 1].mean()))
        bg_lum.append(float(gray[mask == 0].mean()))
        widths.append(w / frame.shape[1])
        scores.append(float(faces[0][-1]))
    n = len(stamps)
    if n == 0:
        print("フレームが読めませんでした")
        return

    if is_camera and n > 1:
        fps = (n - 1) / (stamps[-1] - stamps[0])     # カメラは実測(設定値ではなく、実際に届いた枚数)
    else:
        fps = cap.get(cv2.CAP_PROP_FPS)              # 動画ファイルは記録された値
    detected = n - missed
    print(f"解像度          : {frame_size[0]}x{frame_size[1]}")
    print(f"fps             : {fps:.1f}" + ("(実測)" if is_camera else "(ファイルの記録値)"))
    print(f"顔を検出できた  : {detected}/{n} フレーム({detected / n:.0%})")
    warnings = []
    if fps < 15:
        warnings.append("fps が低い: 閉眼が1フレームしか写らないことがある。照明を明るくする・解像度を下げる")
    if detected / n < 0.9:
        warnings.append("顔を見落とすフレームが多い: 顔の向き・逆光・画面からのはみ出しを確認")
    if detected:
        fw, fl, bl, sc = median(widths), median(face_lum), median(bg_lum), median(scores)
        print(f"顔の幅          : 画面幅の {fw:.0%}({fw * frame_size[0]:.0f}px)")
        print(f"顔の明るさ      : {fl:.0f} / 255(背景 {bl:.0f})")
        print(f"顔検出のスコア  : {sc:.2f}")
        if fw * frame_size[0] < 250:
            warnings.append("顔が小さい: 瞼の動きを解析しきれない。カメラに近づくか解像度を上げる")
        if fl < 60:
            warnings.append("顔が暗い: 正面から照明を当てる")
        if bl - fl > 40:
            warnings.append("逆光: 背景が顔より明るい。窓を背にしない・正面に照明を足す")
        if sc < 0.9:
            warnings.append("顔検出のスコアが低い: 既定のしきい値 0.9 では見落とす可能性がある")
    print()
    print("\n".join("⚠ " + w for w in warnings) if warnings else "問題は見つかりませんでした")


if __name__ == "__main__":
    main()

当社の検証動画3本で実行した結果です。

動画 fps 顔の幅 顔 / 背景の明るさ 指摘された項目
PC 内蔵カメラ・窓のある部屋 9 400px(31%) 97 / 157 fps が低い・逆光
スマートフォン 9 296px(55%) 125 / 95 fps が低い
PC カメラ・やや遠い 12 169px(26%) 112 / 148 fps が低い・顔が小さい

「顔が小さい」と指摘された3本目は、実際にまばたきを1回も数えられなかった動画です。


5. それでも反応しないとき

撮影条件を整えても安定しない場合は、③の開閉判定の方式そのものが条件に合っていない可能性があります。顔の特徴点から目の形を測る方式(EAR など)は、特徴点が瞼を追えない条件に弱いという性質があります。§3 の「顔が小さい」例(顔の幅169px)がその典型です。

目の画像そのものから開閉を分類する方式は、この弱点を持ちません。当社のまばたき検出SDKはこの方式で、目の画像をディープラーニングのモデルに入れ、左右の目の開閉を1フレームずつ判定します。同じ顔の幅169pxの動画で比べると、当社が試した EAR 方式の実装は4回中0回、当社SDKは4回すべてを数えました。当社の動作確認用の動画3本での結果で、条件によって変わります。(比較の詳細は OpenCV でまばたきを検出する(EAR 方式)と、その限界)。

意識したまばたきだけを操作に使いたい場合は、閉眼の長さで区別する設計が有効です。意識したまばたきと無意識のまばたきを見分ける にまとめました。


まとめ

  • まばたき検出は「顔 → 目の位置 → 開閉 → 時間方向」の4段。どの段で止まっているかを先に切り分ける
  • まったく反応しないなら①(逆光・暗さ・高解像度のままの顔検出)
  • 閉じても反応しないなら③④(顔が小さい・fps が低い・しきい値が合っていない)
  • 勝手に反応するなら③(メガネの映り込み・伏し目・笑顔)
  • fps は設定値ではなく実測で確かめる。暗い部屋ではカメラが自動で fps を落とす

👁 まばたき検出をすぐ試したい方へ

スワローインキュベートのまばたき検出は、目の画像から開閉を判定する方式で、一般的な可視光カメラと CPU だけで動作します。ブラウザ版の試用版アプリなら、インストール不要で30秒間の計測をその場で試せます(映像は外部に送信しません)。

→ まばたき検出SDKの詳細を見る
→ 試用版アプリ(ブラウザ版)で精度を試す(無料)

タイトルとURLをコピーしました