File size: 4,252 Bytes
22a6915
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# data_preparation

Handles loading, splitting, scaling, and serving the collected dataset for training and evaluation.

## Links

- Participant consent form: [Consent document](https://drive.google.com/file/d/1g1Hc764ffljoKrjApD6nmWDCXJGYTR0j/view?usp=drive_link)
- Dataset (staff access): [Dataset folder](https://drive.google.com/drive/folders/1fwACM6i6uVGFkTlJKSlqVhizzgrHl_gY?usp=sharing)

## Data collection protocol

9 team members each recorded 5-10 minute webcam sessions using a purpose-built tool (`models/collect_features.py`). During recording:

- Participants simulated **focused** behaviour (reading, typing) and **unfocused** behaviour (looking at phone, turning away)
- Binary labels were annotated in real-time via key presses
- Sessions were recorded across different rooms, workspaces, and home offices using consumer webcams under varying lighting
- Real-time quality guidance warned if class balance fell outside 30-70% or if fewer than 10 state transitions occurred
- An automated post-collection quality report validated minimum duration (120s), sample count (3,000+ frames), balance, and transition frequency

All participants provided informed consent for their facial landmark data to be used within this coursework project. Raw video frames are never stored; only the 17-dimensional feature vector and binary labels are saved.

Raw participant dataset is excluded from this repository (coursework policy and privacy constraints). It is shared separately via the dataset link above.

## Dataset summary

| Metric | Value |
|--------|-------|
| Participants | 9 |
| Total frames | 144,793 |
| Class balance | 61.5% focused / 38.5% unfocused |
| Features extracted | 17 per frame |
| Features selected | 10 (used by ML models) |

## Data format

Training data lives under `data/collected_<participant>/` as `.npz` files. Each file contains:

| Key | Shape | Description |
|-----|-------|-------------|
| `features` | (N, 17) | Float array of extracted features |
| `labels` | (N,) | Binary: 0 = unfocused, 1 = focused |
| `feature_names` | (17,) | String names matching `FEATURE_NAMES` in `collect_features.py` |

Data files are not included in this repository due to privacy considerations.

## Files

| File | Purpose |
|------|---------|
| `prepare_dataset.py` | Core data pipeline: loads `.npz`, applies feature selection, stratified splits, StandardScaler on train only |
| `data_exploration.ipynb` | Exploratory analysis: feature distributions, class balance, per-person statistics, correlation heatmaps |

## Feature selection

`SELECTED_FEATURES["face_orientation"]` defines the 10 features used by all ML models:

**Head pose (3):** `head_deviation`, `s_face`, `pitch`
**Eye state (4):** `ear_left`, `ear_right`, `ear_avg`, `perclos`
**Gaze (3):** `h_gaze`, `gaze_offset`, `s_eye`

Excluded: `v_gaze` (noisy), `mar` (1.7% trigger rate), `yaw`/`roll` (redundant with `head_deviation`/`s_face`), `blink_rate`/`closure_duration`/`yawn_duration` (temporal overlap with `perclos`).

Selection was validated by XGBoost gain importance and LOPO channel ablation:

| Channel subset | Mean LOPO F1 |
|---------------|-------------|
| All 10 features | 0.829 |
| Eye state only | 0.807 |
| Head pose only | 0.748 |
| Gaze only | 0.726 |

## Key functions

| Function | What it does |
|----------|-------------|
| `load_all_pooled(model_name)` | Concatenates all participant data into one array |
| `load_per_person(model_name)` | Returns `{person: (X, y)}` dict for LOPO cross-validation |
| `get_numpy_splits(model_name)` | Returns scaled train/val/test numpy arrays (70/15/15 split) |
| `get_dataloaders(model_name)` | Returns PyTorch DataLoaders for MLP training |
| `get_default_split_config()` | Returns split ratios and seed from `config/default.yaml` |

## Data cleaning

Applied before splitting (in `ui/pipeline.py` at inference, in `prepare_dataset.py` for training):

1. Angles clipped to physiological ranges (yaw +/-45, pitch/roll +/-30)
2. `head_deviation` recomputed from clipped angles (not clipped after computation)
3. EAR clipped to [0, 0.85], MAR to [0, 1.0]
4. Physiological bounds on gaze_offset, PERCLOS, blink_rate, closure/yawn duration
5. StandardScaler fit on training split only, applied to val/test