1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
| """ PERCLOS (Percentage of Eyelid Closure) 标准实现 基于 NHTSA 验证的 psychophysiological signal (1990s research) EU ADDW 标准采用 80% 闭眼阈值 """
import numpy as np from typing import Tuple
class PERCLOSCalculator: """ PERCLOS 计算 - 欧盟 ADDW 标准实现 定义: PERCLOS = 闭眼时间百分比 (眼睑开度 < 80% 判定为闭眼) EU ADDW 标准: - 计算窗口:60 秒滑动窗口 - 闭眼判定:眼睑开度 < 80% 持续 > 500ms - 二级警告:PERCLOS > 阈值 (OEM 声明, 通常 15-30%) 参考文件: - NHTSA DOT HS 808 504 (PERCLOS 原始研究) - ECE R152 ADDW Technical Requirements """ def __init__(self, closure_threshold: float = 0.8, min_closure_duration: float = 0.5, window_sec: float = 60.0, fps: int = 30): self.closure_threshold = closure_threshold self.min_closure_duration = min_closure_duration self.window_sec = window_sec self.fps = fps self.window_frames = int(window_sec * fps) def calculate(self, eye_openness: np.ndarray) -> dict: """ 计算 PERCLOS 值 Args: eye_openness: 眼睑开度序列 (0-1), shape=(N,) 1.0=完全睁眼, 0.0=完全闭眼 Returns: dict: { 'perclos': float, # 0-100% 'closure_events': list, # 闭眼事件列表 'alert_level': str, # NONE/WARN/SERIOUS 'max_closure_duration': float, # 最长单次闭眼秒数 } """ n = len(eye_openness) is_closed = eye_openness < self.closure_threshold closure_events = [] in_closure = False start = 0 for i, closed in enumerate(is_closed): if closed and not in_closure: in_closure = True start = i elif not closed and in_closure: duration = (i - start) / self.fps if duration >= self.min_closure_duration: closure_events.append({ 'start_frame': start, 'end_frame': i, 'duration_sec': duration, }) in_closure = False if in_closure: duration = (n - start) / self.fps if duration >= self.min_closure_duration: closure_events.append({ 'start_frame': start, 'end_frame': n, 'duration_sec': duration, }) if n < self.window_frames: total_closure = sum(e['duration_sec'] for e in closure_events) perclos = (total_closure / (n / self.fps)) * 100 if n > 0 else 0 else: window_perclos = [] for i in range(n - self.window_frames): window = is_closed[i:i + self.window_frames] window_perclos.append(np.sum(window) / self.window_frames * 100) perclos = np.max(window_perclos) if window_perclos else 0 max_dur = max([e['duration_sec'] for e in closure_events], default=0) if perclos > 30: alert = 'SERIOUS' elif perclos > 15: alert = 'WARN' else: alert = 'NONE' return { 'perclos': round(perclos, 2), 'closure_events': closure_events, 'alert_level': alert, 'max_closure_duration': round(max_dur, 2), }
if __name__ == "__main__": calc = PERCLOSCalculator(fps=30) t = np.arange(1800) / 30.0 normal = np.random.normal(0.9, 0.05, 1200) sleepy = np.concatenate([ np.random.normal(0.85, 0.08, 180), np.full(90, 0.3), np.random.normal(0.85, 0.08, 90), np.full(120, 0.2), np.random.normal(0.85, 0.08, 120), ]) eye_data = np.clip(np.concatenate([normal, sleepy]), 0, 1) result = calc.calculate(eye_data) print(f"PERCLOS: {result['perclos']:.2f}%") print(f"Alert Level: {result['alert_level']}") print(f"Closure Events: {len(result['closure_events'])}") print(f"Max Closure: {result['max_closure_duration']:.2f}s") for e in result['closure_events']: print(f" - {e['duration_sec']:.2f}s @ frame {e['start_frame']}")
|