Open or closed? Eye-state detection with PyTorch

A computer vision model that tells whether an eye is open or closed from a small infrared image, tested on six people it never saw during training.

Data
MRL Eye Dataset: 84,898 infrared eye images from 37 people
Models
Small CNN from scratch vs. fine-tuned MobileNetV3-Small
Tools
Python, PyTorch, torchvision, scikit-learn, Pandas, ONNX
Result
97.2% accuracy on six new people, 98.5% of closed eyes caught

Problem

Drowsiness and impairment checks often start with one simple question: is the eye open or closed right now? A phone or car camera has to answer it quickly, for people it has never seen, with glasses, glare and bad lighting in the way.

Approach

  • Split the data by person, not by image: 26 people for training, 5 for validation and 6 for testing. Photos of the same person look alike, so a random split would let the model memorize faces and inflate the score.
  • Ran two experiments on the same split: a small CNN trained from scratch as a baseline, and MobileNetV3-Small, an ImageNet model built for phones, fine-tuned on the eye images.
  • Watched validation loss every epoch and kept the best checkpoint, stopping early once it stopped improving, to catch overfitting.
  • Measured accuracy, precision, recall, F1 and ROC AUC on the test people, then broke errors down by glasses, reflections, lighting and camera.
  • Exported the models to ONNX and timed single-image inference on a laptop CPU as a rough check on phone readiness.

The data

Eight infrared eye crops in two rows, open eyes on top and closed eyes below, for four conditions: no glasses, glasses, strong reflection and poor lighting.
Open (top) and closed (bottom) eyes. Each image is labelled with eye state, glasses, reflections, lighting quality and camera, which makes error analysis by condition possible.

Results on the six test people

Test results for both models
Metric (closed eye = positive)Small CNN, from scratchMobileNetV3-Small, fine-tuned
Accuracy96.7%97.2%
Recall (closed eyes caught)97.7%98.5%
Precision (closed calls that were right)95.8%96.0%
F196.7%97.2%
ROC AUC0.9950.995
Parameters0.29M1.52M
ONNX file size1.2 MB6.1 MB
One image on a laptop CPU1.6 ms26 ms

13,647 test images from six people who were not used for training or model selection. Speed was measured in PyTorch on a laptop CPU, one image at a time, so treat it as a rough comparison rather than phone performance.

Two line charts of training and validation loss by epoch. The small CNN's validation loss is lowest at epoch 2 and rises after it; the fine-tuned MobileNet's validation loss falls slowly and is lowest at epoch 8.
Training vs. validation loss. Dashed lines are training loss, solid lines are validation loss. The small CNN's validation loss jumped after epoch 2 while its training loss kept falling, a sign it was starting to fit the training people too closely, so early stopping kept epoch 2. The pretrained model started lower and improved steadily.

Where the model goes wrong

Horizontal bar chart of error rate by condition for both models. The fine-tuned model has clearly lower error with glasses (2.7% vs 3.9%) and strong reflections (2.4% vs 3.8%), and about the same without glasses (2.9% vs 3.0%).
Error rate by condition, test people only. One image can belong to several groups (glasses and poor lighting, for example), so the groups overlap. The test people had no images from the third camera in the dataset.
Ten eye images the fine-tuned model got wrong with high confidence, each labelled with the true state and the model's probability that the eye is open.
The most confident mistakes. Top row: labelled closed, predicted open. Bottom row: labelled open, predicted closed. Several of these look mislabelled or half closed, which suggests part of the remaining error sits in the labels rather than the model.

Pretraining helped most on the hard cases

Fine-tuning a pretrained model cut errors by about a third with glasses (3.9% to 2.7%) and strong reflections (3.8% to 2.4%), but barely changed them without glasses (3.0% to 2.9%).

The model leans toward "closed"

4.0% of open eyes were called closed, but only 1.5% of closed eyes were called open. For a drowsiness check that means more false alarms than misses. Moving the decision threshold trades one for the other, and the right balance depends on the product.

Some mistakes are in the labels

The most confident mistakes often look mislabelled or half closed. Relabelling a sample of the disagreements would show how much of the last 3% is model error and how much is label noise.

Bigger was not clearly better

The fine-tuned model was 0.5 points more accurate, but 5 times larger and much slower on CPU. On a phone, that trade-off needs measuring on the device with a mobile runtime before choosing a model.

Limitations and next steps

Six test people is a small group, and it matters: the five validation people were easier (99.0% accuracy) than the six test people (97.2%). The per-condition numbers rest on only a few people each and would move with a different split. Repeating the experiment with several person-level splits (grouped cross-validation) would give a more honest range.

These are still, pre-cropped eye images. A real product has to find the eye in a video frame first and then follow it over time, where blinks and eye movements become a time series. Adding temporal smoothing across frames is the natural next step, and the ONNX export is a starting point for testing on an actual phone.

Data: MRL Eye Dataset, Media Research Lab, VSB Technical University of Ostrava.

Back to projects

Contact

I'm looking for data and machine learning roles in Vancouver or remote. Send me an email or a message on LinkedIn.

LinkedIn GitHub