閉眼時間から眠気を推定する — PERCLOS の考え方とカメラでの実装
運転中の居眠り、夜勤の見守り、長時間の監視作業——「眠くなってきた」ことをカメラで捉えたい場面は少なくありません。そのときによく使われる指標が PERCLOS(パークロス) です。
本記事では、PERCLOS の定義と由来、なぜ「まばたきの回数」ではなく「閉じている時間の割合」を見るのかを説明し、カメラの開閉判定から PERCLOS に近い値を計算するコードを示します。
本記事は、眠気の推定に使われる指標とその実装についての技術的な解説です。医学的な診断や、安全を保証する仕組みについて述べるものではありません。
1. PERCLOS とは
PERCLOS は PERcentage of eyelid CLOSure の略で、一定時間のうち、目が閉じている時間の割合です。
- 1994 年に Wierwille らが、ドライビングシミュレーターでの研究で眠気の指標として提案しました
- 1998 年には Dinges らが米国運輸省(NHTSA / FHWA)の報告書で、睡眠不足の状態での注意力の低下(反応課題での見逃し)と照らし合わせて検証し、評価した眠気検知の指標・技術の中で最も信頼性・妥当性が高いと報告しています
代表的な定義は P80 で、「1分間のうち、瞼が80%以上閉じている時間の割合」です。定義上、短いまばたきは含めず、ゆっくりした閉眼を捉える指標です。ほかに70%を基準にする P70 などもあり、研究によって定義が異なる点には注意が必要です。
2. なぜ「回数」ではなく「閉じている時間の割合」なのか
眠くなると、まばたきの様子は次のように変わります。
| 状態 | まばたきの様子 |
|---|---|
| 覚醒している | 素早く閉じて素早く開く。閉じている時間は短い |
| 眠くなってきた | 閉じるのも開くのもゆっくりになり、閉じている時間が長くなる |
| 強い眠気 | 数秒間目を閉じたままになる(いわゆる「うとうと」) |
まばたきの回数は、会話や読書などの課題で数倍変わります(安静時 17・会話 26・読書 4.5 回/分という報告があります:Bentivoglio ら, 1997)。回数だけを見ると、眠気なのか課題の違いなのかが区別しにくくなります。
閉じている時間の割合なら、「ゆっくりしたまばたき」と「目を閉じたまま」の両方が値を押し上げるので、眠気の変化を捉えやすくなります。
3. カメラで計算する
3-1. 何を測るか
厳密な P80 には「瞼がどれだけ閉じているか(開き具合)」が必要です。一般的なカメラの解析では、次の2通りの近似がよく使われます。
| 方法 | 得られるもの | P80 との関係 |
|---|---|---|
| 目の縦横比(EAR)などの連続値 | 開き具合に比例する値 | 開眼時の値を100%として、閉眼の度合いを推定できる |
| 開閉の2値判定 | 各フレームで「開」か「閉」か | 「閉と判定された時間の割合」。閉と判定する基準しだいで P80 にも P70 にも近くなる |
本記事のコードは後者、開閉の2値判定から閉眼割合を出す方法です。厳密な P80 ではないので、結果は「PERCLOS の考え方に基づく閉眼割合」として扱ってください。
3-2. コード(Python)
1フレーム1行の開閉判定(eyeStatusLeft / eyeStatusRight が 1: 閉 / 2: 開 / -1: 判定不能)の CSV から、窓(既定60秒)ごとの閉眼割合を出します。
# 1フレーム1行の開閉判定 CSV から、1分ごとの「閉眼している時間の割合」(PERCLOS の考え方)を出す
# 使い方: python perclos.py result.csv <fps> [窓の長さ(秒)=60] [短いまばたきを除く上限(秒)=0]
# result.csv : eyeStatusLeft / eyeStatusRight(1: 閉 / 2: 開 / -1: 判定不能)の列を持つ CSV
# fps : 記録した映像のフレームレート(フレーム番号 ÷ fps で時間を出す)
# 短いまばたきを除く上限 : 例えば 0.5 を指定すると、0.5 秒未満の閉眼(ふだんのまばたき)を数えない
import csv
import sys
CLOSED, OPEN = 1, 2
def main():
if len(sys.argv) < 3:
print("usage: python perclos.py result.csv <fps> [window_s=60] [ignore_blinks_shorter_than_s=0]")
return
path, fps = sys.argv[1], float(sys.argv[2])
window_s = float(sys.argv[3]) if len(sys.argv) > 3 else 60.0
ignore_s = float(sys.argv[4]) if len(sys.argv) > 4 else 0.0
with open(path, newline="", encoding="utf-8-sig") as f:
status = [] # 1フレームごとに True(閉) / False(開) / None(判定不能)
for r in csv.DictReader(f):
left, right = int(r["eyeStatusLeft"]), int(r["eyeStatusRight"])
if left in (CLOSED, OPEN) and right in (CLOSED, OPEN):
status.append(left == CLOSED and right == CLOSED)
else:
status.append(None)
# 短い閉眼(ふだんのまばたき)を「開」として扱う
if ignore_s > 0:
i = 0
while i < len(status):
if status[i]:
j = i
while j < len(status) and status[j]:
j += 1
if (j - i) / fps < ignore_s:
for k in range(i, j):
status[k] = False
i = j
else:
i += 1
per_window = int(round(window_s * fps))
print(f"窓 {window_s:.0f} 秒ごとの閉眼割合" + (f"({ignore_s} 秒未満の閉眼は除外)" if ignore_s else ""))
for w in range(0, len(status), per_window):
chunk = status[w:w + per_window]
valid = [s for s in chunk if s is not None]
if len(valid) < len(chunk) * 0.8:
print(f" {w / fps:7.0f}s〜: 判定できたフレームが {len(valid)}/{len(chunk)} と少ないため算出しない")
continue
ratio = sum(valid) / len(valid)
bar = "#" * int(ratio * 50)
print(f" {w / fps:7.0f}s〜: {ratio:6.1%} {bar}")
if __name__ == "__main__":
main()
テスト用に作った4分間のデータ(30fps・1分ごとに長い閉眼を増やしたもの)での実行例です。
$ python perclos.py test.csv 30
窓 60 秒ごとの閉眼割合
0s〜: 3.3% #
60s〜: 7.7% ###
120s〜: 6.3% ###
180s〜: 14.9% #######
$ python perclos.py test.csv 30 60 0.5 ← 0.5 秒未満の閉眼(ふだんのまばたき)を除く
窓 60 秒ごとの閉眼割合(0.5 秒未満の閉眼は除外)
0s〜: 0.0%
60s〜: 5.0% ##
120s〜: 2.5% #
180s〜: 12.5% ######
ふだんのまばたきを除くと、最初の1分(長い閉眼なし)は 0% になり、長い閉眼だけが値に残ることが分かります。
4. 実装上の注意
4-1. 判定できないフレームを「開」にしない
顔が横を向いた、手で顔が隠れた——そうしたフレームを「開」として数えると、閉眼割合が実際より低く出ます。うとうとして頭が傾いたときこそ判定できないフレームが増えるので、なおさら危険です。上のコードは判定不能を分母から外し、判定できたフレームが8割未満の窓は値を出さないようにしています。
4-2. フレームレート
閉眼割合は時間の比なので、回数ほどフレームレートに敏感ではありません。ただし短いまばたきを除く処理(例: 0.5秒未満)は時間の長さで判定するため、15fps 以上、できれば 30fps を確保してください。暗い車内や夜間は、カメラが自動で fps を落とすことがあります。
4-3. 夜間・車内の照明
一般的な可視光カメラは暗所に弱く、夜間の車内では顔が写らないことがあります。市販のドライバーモニタリングでは、近赤外線の照明とカメラを使うのが一般的です。可視光カメラで作る場合は、使う時間帯と明るさを前提として明記し、その範囲で評価してください。
4-4. しきい値は「その人の普段」から
閉眼割合が何%になったら警告するかは、用途と人によって違います。覚醒しているときの値を最初の数分で測って基準にし、そこからの増え方を見る設計が現実的です。
4-5. 用途ごとの責任範囲
PERCLOS は眠気の「推定」に使う指標で、居眠りを確実に防ぐものではありません。車両の安全機能のように人命にかかわる用途では、指標の選定だけでなく、対象とする環境での検証や、関係する規格・ガイドライン(車載なら機能安全の ISO 26262 や、各地域のドライバーモニタリングに関する規制など)への対応が別途必要です。
5. まばたきの他の指標と組み合わせる
PERCLOS は「長い閉眼」をよく捉えますが、眠気の初期に現れる変化は捉えにくいことがあります。実用上は、次の指標と組み合わせることが多いです。
| 指標 | 捉えるもの |
|---|---|
| 閉眼時間の平均・最長 | 1回ごとのまばたきが「ゆっくり」になる変化 |
| まばたきの間隔のばらつき | まばたきのリズムの乱れ |
| 顔の向き・うなずき | 頭が落ちる・傾く動き |
閉眼時間を細かく測る方法は まばたき(瞬目)を動画から計測する、開閉判定の作り方は OpenCV でまばたきを検出する(EAR 方式)と、その限界 にまとめています。
6. PERCLOS の精度は、開閉判定の精度で決まる
PERCLOS は「閉じていたフレームの割合」なので、開閉判定が閉眼を見落とせば、その分だけ低く出ます。顔の特徴点から目の形を測る方式(EAR)は、顔が小さく写ると特徴点が瞼を追えないことがあります。当社が試した実装では、顔の幅169pxの動画で閉じた目を「開いている」と判定し続け、4回のまばたきを1回も捉えられませんでした。車内のカメラのように、顔が画面の一部にしか写らない配置では特に注意が必要です。
当社のまばたき検出SDKは、目の画像をディープラーニングのモデルに入れ、左右の目の開閉を1フレームずつ判定します。同じ動画で4回すべてを捉えました(動画 C は PC の Web カメラで撮ったもので、車内の映像ではありません)。当社の動作確認用の動画3本での結果で、条件によって変わります。処理は CPU のみで1フレーム約20ms(Apple M2・入力 1280×960・当社実測)です。車載向けの CPU では、速度を別途確認してください(比較の詳細)。
まとめ
- PERCLOS は「一定時間のうち目が閉じている時間の割合」。代表的な P80 は「1分間のうち瞼が80%以上閉じている時間の割合」
- 1994年に提案され、1998年の米国運輸省の報告で、評価した眠気検知の指標の中で最も信頼性・妥当性が高いとされた
- まばたきの回数は課題で大きく変わるが、閉眼割合は「ゆっくりしたまばたき」と「閉じたまま」を捉える
- カメラの開閉判定から近い値を出せる。判定不能を開にしない・fps を確保・基準はその人の普段
- 人命にかかわる用途では、対象環境での検証と規格への対応が別途必要
👁 閉眼時間を測る仕組みを組み込みたい方へ
スワローインキュベートのまばたき検出SDKは、一般的な可視光カメラの映像から左右の目の開閉をフレームごとに判定します。GPU 不要・CPU のみで動作し、Windows / Linux / iOS / Android、C++ / C# に対応。1ヶ月無料の試用版があります。

