# Project export: EyeLock

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Your Digital Driving Copilot
- Devpost: https://devpost.com/software/eyelock
- GitHub: https://github.com/sienaro/EyeLock
- Video: https://www.youtube.com/embed/yKs5GAHvHk0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Siena Rojas (1 commits)

## Devpost submission (written by the team)

### Inspiration

Personally, we have known people involved in extremely dangerous accidents with fatigued drivers, especially late at night or early in the morning. This is not an isolated experience. In fact, research shows that fatigued driving is responsible for 13% of all commercial motor vehicle accidents (Federal Motor Carrier Safety Administration). Additionally, up to 65% of truck drivers admit to driving while drowsy, with 50% actually falling asleep behind the wheel (FMCSA). This is an inhibition that slows reflexes and decision-making abilities, often compared to the dangers of drunk driving. EyeLock combats this widespread problem with a cost-efficient, accessible, and scientifically-backed approach that not only reduces liabilities for commercial driving companies, but also prioritizes driver health.

### What it does

EyeLock targets three primary fatigue-driven dangers: eye strain, falling asleep at the wheel, and unsafe posture for airbags. Long, repetitive distance-driving can be damaging to drivers’ long-term eye health (similarly to staring at a screen too long), as average blinking rate falls from 15-20 blinks/second to below 8-10 blinks/second, failing to replenish the eye’s natural tear film. EyeLock uses eye-tracking technology to remind drivers to blink at a natural rate when it detects dangerously low blinking frequencies. Falling asleep behind the wheel is one of the most dangerous risks of drowsy driving: looking away from the road for just two seconds doubles the risk of a crash, while 80% of crashes involve a driver looking away for just three seconds. EyeLock’s computer vision recognizes when a driver is nodding off, blinking for abnormally long, or has drooping eyelids, and plays pulsing frequencies proven to combat grogginess in a gentle but rapid fashion. Another danger of fatigued driving is slouching closer to the wheel, which is damaging to a driver’s long-term spinal health, as well as potentially fatal in the event of airbag deployment. EyeLock calibrates to a user’s naturally comfortable sitting position (at least 10-12 inches away from the wheel) to alert them of prolonged periods of leaning forward, being sure to ignore brief or trivial position changes. Lastly, EyeLock constantly tracks and warns against distracted eye movements that indicate a driver is looking away from the road or at an electronic device, being careful to ignore natural and healthy glances elsewhere. Our product achieves its safety features in high- or low-light environments without distracting visual alerts, overwhelming overlapping notices, or overly-sensitive excess warnings.

### How we built it

We built the backend with Python, using OpenCV for video processing and MediaPipe for facial feature identification. We also utilized the Python Tkinter library for the UI. To account for blink frequency, we used a sliding window average over 15 second intervals. Blink detection was performed by calculating the Eye Aspect Ratio (EAR) to utilize the position of eyelids to qualify blinks. We also calculated head yaw and pitch by measuring z-coordinates of the cheeks and nose-to-chin distance to detect nodding off and distracted vision. Distance from the steering wheel was calculated by comparing the distance between the driver’s eyes compared to their original sitting position.

### Challenges we ran into

Initially, we were trying to detect distracted driving by noting eye location, but we updated this to rely on head position, since we could set a more accurate threshold for how much the driver was looking away, helping avoid false positives and negatives.

### Accomplishments we're proud of

Having never worked with computer vision in a project before, we are proud to have learned about the libraries used for these tasks, specifically eye and movement tracking, which are now ubiquitous technologies. We are also proud to have found a realistic and applicable solution to a prevalent problem, especially under such short time constraints.

### What's next

Looking forward, we hope to optimize EyeLock’s hardware for seamless integration into the car’s interface, as well as add additional sensors and haptic alerts to the steering wheel and seat to increase driver alertness. We also hope to track long term driver statistics and performance to personalize their alert sensitivity and suggest the safest driving hours and habits for them.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 2 recognized source files, 22 KB.
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (2 of 2)

```
ui_app.py
webcam_test.py
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Final Version
- Initial commit: EyeLock tracker initial implementation

## Key source files (fetched from GitHub, selected and truncated for size)

### webcam_test.py

```python
import cv2
import time
import numpy as np
import tkinter as tk
from PIL import Image, ImageTk
import mediapipe as mp
from collections import deque

# ---- Config ----
CONSEC_FRAMES = 2          # Consecutive frames for a blink
WINDOW_SECONDS = 60        # Rolling window for blink rate
CALIBRATION_TIME = 60      # Calibration period in seconds

# ---- MediaPipe setup ----
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
    static_image_mode=False,
    max_num_faces=1,
    refine_landmarks=True,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5
)

LEFT_EYE = [33, 160, 158, 133, 153, 144]
RIGHT_EYE = [362, 385, 387, 263, 373, 380]

def eye_aspect_ratio(landmarks, eye_idx, w, h):
    pts = []
    for i in eye_idx:
        x = int(landmarks[i].x * w)
        y = int(landmarks[i].y * h)
        pts.append((x, y))

    p1, p2, p3, p4, p5, p6 = pts
    A = np.linalg.norm(np.array(p2) - np.array(p6))
    B = np.linalg.norm(np.array(p3) - np.array(p5))
    C = np.linalg.norm(np.array(p1) - np.array(p4))
    return (A + B) / (2.0 * C + 1e-6), pts

cap = cv2.VideoCapture(0)

class App:
    def __init__(self, window):
        self.window = window
        self.window.title("BreakTime")
        self.window.geometry("400x650")

        # --- UI Elements ---
        self.label_title = tk.Label(window, text="BreakTime", font=("Helvetica", 24, "bold"))
        self.label_title.pack(pady=10)

        self.status_label = tk.Label(window, text="Initializing...", font=("Helvetica", 12), fg="blue")
        self.status_label.pack()

        self.focus_label = tk.Label(window, text="Calibration Pending", font=("Helvetica", 14, "italic"))
        self.focus_label.pack(pady=10)

        self.blink_label = tk.Label(window, text="Blink rate: -- / min", font=("Helvetica", 12))
        self.blink_label.pack(pady=5)

        # Camera Feed Label
        self.cam_label = tk.Label(window)
        self.cam_label.pack(pady=20)

        # Controls
        btn_frame = tk.Frame(window)
        btn_frame.pack(pady=10)
        
        self.recal_btn = tk.Button(btn_frame, text="Recalibrate", width=12, command=self.reset_calibration)
        self.recal_btn.grid(row=0, column=0, padx=5)

        # --- Logic & State ---
        self.running = True
        self.frames_below = 0
        self.blink_timestamps = deque()
        
        # Calibration variables
        self.reset_calibration()
        
        # Start main loop
        self.update_cam()

    def reset_calibration(self):
        """Resets the data to start a new 60-second calibration."""
        self.start_time = time.time()
        self.calibration_data = []
        self.calibrated_threshold = None
        self.status_label.config(text="Calibration starting...", fg="orange")

    def update_cam(self):
        if not self.running:
            return

        ret, frame = cap.read()
        if ret:
            frame = cv2.flip(frame, 1)
            h, w, _ = frame.shape
            rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            results = face_mesh.process(rgb)

            ear = None
            elapsed = time.time() - self.start_time

            if results.multi_face_landmarks:
                landmarks = results.multi_face_landmarks[0].landmark
                left_ear, left_pts = eye_aspect_ratio(landmarks, LEFT_EYE, w, h)
                right_ear, right_pts = eye_aspect_ratio(landmarks, RIGHT_EYE, w, h)
                ear = (left_ear + right_ear) / 2

                # Draw eye landmarks for visual feedback
                for (x, y) in left_pts + right_pts:
                    cv2.circle(frame, (x, y), 2, (0, 255, 0), -1)

                # --- PHASE 1: CALIBRATING ---
                if elapsed < CALIBRATION_TIME:
                    self.calibration_data.append(ear)
                    remaining = int(CALIBRATION_TIME - elapsed)
                    self.status_label.config(text=f"Calibrating: {remaining}s remaining...")
                
                # --- PHASE 2: PROCESS CALIBRATION ---
                elif self.calibrated_threshold is None:
                    if len(self.calibration_data) > 0:
                        avg_open_ear = np.mean(self.calibration_data)
                        # Threshold is set to 75% of your natural open-eye average
                        self.calibrated_threshold = avg_open_ear * 0.75
                        self.status_label.config(text="Monitoring Active", fg="green")
                    else:
                        self.status_label.config(text="Calibration Error: No face detected", fg="red")

                # --- PHASE 3: ACTIVE MONITORING ---
                else:
                    if ear < self.calibrated_threshold:
                        self.frames_below += 1
                    else:
                        if self.frames_below >= CONSEC_FRAMES:
                            self.blink_timestamps.append(time.time())
                        self.frames_below = 0

            # Rolling window blink rate calculation
            now = time.time()
            while self.blink_timestamps and now - self.blink_timestamps[0] > WINDOW_SECONDS:
                self.blink_timestamps.popleft()

            if self.calibrated_threshold:
                blink_rate = len(self.blink_timestamps) * (60.0 / WINDOW_SECONDS)
                self.blink_label.config(text=f"Blink rate: {blink_rate:.1f} / min")

                # Focus Heuristic
                if blink_rate < 6:
                    self.focus_label.config(text="Focus: Staring / Screen Strain", fg="red")
                elif blink_rate > 25:
                    self.focus_label.config(text="Focus: Fatigue Detected", fg="orange")
                else:
                    self.focus_label.config(text="Focus: Healthy", fg="green")

            # Mini Preview (PiP)
            target_w = 320
            scale = target_w / w
            target_h = int(h * scale)
            frame_small = cv2.resize(frame, (target_w, target_h), i
[truncated — 578 more characters]
```

### ui_app.py

```python
import cv2
import time
import numpy as np
import tkinter as tk
from PIL import Image, ImageTk
import mediapipe as mp
from collections import deque
import os
import threading
import sounddevice as sd

# ---- Configs ----
CONSEC_FRAMES = 2          
CALIBRATION_TIME = 15      
DISTANCE_SENSITIVITY = 1.15 
PERSISTENCE_LIMIT = 30     
LONG_BLINK_THRESHOLD = 0.4  
SLEEP_THRESHOLD = 1.5       
ABSENCE_THRESHOLD = 2.0     
LOW_BLINK_THRESHOLD = 8.0  
SOUND_COOLDOWN = 5.0       
TONE_FREQ = 500            
SAMPLE_RATE = 44100

PITCH_DOWN_LIMIT = 45       
PITCH_UP_LIMIT = -35        
YAW_LIMIT = 45              

# ---- MediaPipe setup ----
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
    static_image_mode=False,
    max_num_faces=1,
    refine_landmarks=True,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5
)

LEFT_EYE = [33, 160, 158, 133, 153, 144]
RIGHT_EYE = [362, 385, 387, 263, 373, 380]
L_CENTER, R_CENTER = 468, 473 

def get_head_orientation(landmarks, w, h):
    nose, chin = landmarks[1], landmarks[152]
    dy = (chin.y - nose.y) * h
    dz = (chin.z - nose.z) * h 
    pitch = np.degrees(np.arctan2(dy, dz)) - 90 
    left_cheek, right_cheek = landmarks[234], landmarks[454]
    yaw = (left_cheek.z - right_cheek.z) * 1000 
    return pitch, yaw

def get_eye_distance(landmarks, w, h):
    p1 = np.array([landmarks[L_CENTER].x * w, landmarks[L_CENTER].y * h])
    p2 = np.array([landmarks[R_CENTER].x * w, landmarks[R_CENTER].y * h])
    return np.linalg.norm(p1 - p2)

def eye_aspect_ratio(landmarks, eye_idx, w, h):
    pts = np.array([(landmarks[i].x * w, landmarks[i].y * h) for i in eye_idx])
    A = np.linalg.norm(pts[1] - pts[5])
    B = np.linalg.norm(pts[2] - pts[4])
    C = np.linalg.norm(pts[0] - pts[3])
    return (A + B) / (2.0 * C + 1e-6)

cap = cv2.VideoCapture(0)

class App:
    def __init__(self, window):
        self.window = window
        self.window.title("EyeLock AI")
        self.window.configure(bg="#121212")
        self.window.state('zoomed') 
        
        self.running = True
        self.calibrated_dist = None
        self.start_time = None
        self.session_start = time.time()
        self.blink_timestamps = deque()
        self.ear_history = deque(maxlen=100)
        self.frames_below = 0
        self.displayed_rate = 0.0
        self.calib_blink_count = 0
        self.last_sound_time = 0
        self.last_tone_time = 0
        self.too_close_start_time = None 
        self.eye_closure_start = None
        self.face_lost_start = None

        self.container = tk.Frame(self.window, bg="#121212")
        self.container.pack(fill="both", expand=True)
        self.container.grid_rowconfigure(0, weight=1)
        self.container.grid_columnconfigure(0, weight=1)

        self.frames = {}
        for F in (StartPage, InstructionPage, MainDashboard):
            page_name = F.__name__
            frame = F(parent=self.container, controller=self)
            self.frames[page_name] = frame
            frame.grid(row=0, column=0, sticky="nsew")

        self.show_frame("StartPage")
        self.update_cam()

    def show_frame(self, page_name):
        self.frames[page_name].tkraise()
        if page_name == "MainDashboard":
            self.reset_calibration_logic()

    def reset_calibration_logic(self):
        self.start_time = time.time()
        self.calibrated_dist = None
        self.calib_blink_count = 0
        self.blink_timestamps.clear()
        self.ear_history.clear()
        self.displayed_rate = 0.0
        self.face_lost_start = None
        self.eye_closure_start = None
        self.too_close_start_time = None

    def play_voice_alert(self, message):
        now = time.time()
        if now - self.last_sound_time > SOUND_COOLDOWN:
            threading.Thread(target=lambda: os.system(f"say {message}"), daemon=True).start()
            self.last_sound_time = now

    def play_sleep_tone(self):
        now = time.time()
        if now - self.last_tone_time > 0.4:
            def audio_thread():
                t = np.linspace(0, 0.2, int(SAMPLE_RATE * 0.2), False)
                tone = np.sin(TONE_FREQ * t * 2 * np.pi)
                sd.play((tone * 0.5).astype(np.float32), SAMPLE_RATE)
                sd.wait()
            threading.Thread(target=audio_thread, daemon=True).start()
            self.last_tone_time = now

    def update_cam(self):
        if not self.running: return
        ret, frame = cap.read()
        if not ret: 
            self.window.after(10, self.update_cam)
            return
        
        now = time.time()
        frame = cv2.flip(frame, 1)
        h, w, _ = frame.shape
        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        results = face_mesh.process(rgb)
        db = self.frames["MainDashboard"]

        session_elapsed = int(now - self.session_start)
        db.timer_label.config(text=f"Focus Time: {session_elapsed // 60:02d}:{session_elapsed % 60:02d}")

        if results.multi_face_landmarks:
            landmarks = results.multi_face_landmarks[0].landmark
            pitch, yaw = get_head_orientation(landmarks, w, h)
            
            # --- MONITORING INACTIVE UNTIL CALIBRATION STARTS ---
            if self.start_time:
                for idx in LEFT_EYE + RIGHT_EYE:
                    px, py = int(landmarks[idx].x * w), int(landmarks[idx].y * h)
                    cv2.circle(frame, (px, py), 2, (0, 255, 65), -1)

                ear = (eye_aspect_ratio(landmarks, LEFT_EYE, w, h) + eye_aspect_ratio(landmarks, RIGHT_EYE, w, h)) / 2
                self.ear_history.append(ear)
                curr_dist = get_eye_distance(landmarks, w, h)
                base_thresh = 0.22

                if ear < base_thresh:
                    if self.eye_closure_start is None: self.eye_closure_start = now
                    self.frames_below += 1
                else:
                    if self.eye_closure_start:
                        dur = now - self.eye_closure_sta
[truncated — 10196 more characters]
```