Yingtao Zheng (k23158987) commited on
Commit
bf7ab0c
·
2 Parent(s): b4b7b10f6b961e

Merge pull request #3 from k23172173/template-branch

Browse files
.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ venv/
8
+ .venv/
9
+ env/
10
+ .env
11
+ *.egg-info/
12
+ .eggs/
13
+ dist/
14
+ build/
15
+
16
+ # IDE
17
+ .idea/
18
+ .vscode/
19
+ *.swp
20
+ *.swo
21
+
22
+ # Data and outputs (optional: uncomment if you don’t want to track large files)
23
+ # data_preparation/raw/
24
+ # data_preparation/processed/*.npy
25
+ # evaluation/logs/
26
+ # evaluation/results/
27
+
28
+ # Model checkpoints (uncomment to ignore .pt files)
29
+ # *.pt
30
+
31
+ # Project
32
+ docs/
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
README.md CHANGED
@@ -1 +1,10 @@
1
- # GAP_Large_project
 
 
 
 
 
 
 
 
 
 
1
+ # GAP — FocusGuard
2
+
3
+ Real-time focus estimation from webcam (head pose + eye behaviour).
4
+
5
+ ## Layout
6
+
7
+ - **data_preparation/** — Dataset team (raw data, processed, scripts)
8
+ - **models/** — Face orientation, eye behaviour, fusion, landmarks. Training entry: `models/train.py`
9
+ - **evaluation/** — Metrics, runs, results
10
+ - **ui/** — Live demo + session view
data_preparation/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # data_preparation
2
+
3
+ Dataset team owns layout and scripts here.
evaluation/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # evaluation
2
+
3
+ Metrics, experiment configs, and results live here.
models/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # models
2
+
3
+ - `face_orientation_model/` — S_face
4
+ - `eye_behaviour_model/` — S_eye
5
+ - `attention_score_fusion/` — fusion + smoothing
6
+ - `face_landmarks_pretrained/` — MediaPipe FaceMesh (no training)
7
+
8
+ `train.py` trains the MLP on feature vectors; `prepare_dataset.py` loads from `data_preparation/processed/` or synthetic.
models/attention_score_fusion/.gitkeep ADDED
File without changes
models/eye_behaviour_model/.gitkeep ADDED
File without changes
models/face_landmarks_pretrained/.gitkeep ADDED
File without changes
models/face_orientation_model/.gitkeep ADDED
File without changes
models/face_orientation_model/best_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:18c1f2750c7274e72538b94afcc9f0243287a5b2eb8fcce6be6e4ae18ec59cb0
3
+ size 15033
models/prepare_dataset.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import torch
4
+ from torch.utils.data import Dataset, DataLoader, random_split
5
+
6
+ DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data_preparation", "processed")
7
+
8
+ FEATURE_FILES = {
9
+ "face_orientation": {
10
+ "features": "face_orientation_features.npy",
11
+ "labels": "face_orientation_labels.npy",
12
+ },
13
+ "eye_behaviour": {
14
+ "features": "eye_behaviour_features.npy",
15
+ "labels": "eye_behaviour_labels.npy",
16
+ },
17
+ }
18
+
19
+ SYNTHETIC_CONFIG = {
20
+ "face_orientation": {"num_samples": 500, "num_features": 12, "num_classes": 2},
21
+ "eye_behaviour": {"num_samples": 500, "num_features": 8, "num_classes": 2},
22
+ }
23
+
24
+
25
+ class FeatureVectorDataset(Dataset):
26
+ def __init__(self, features: np.ndarray, labels: np.ndarray):
27
+ self.features = torch.tensor(features, dtype=torch.float32)
28
+ self.labels = torch.tensor(labels, dtype=torch.long)
29
+
30
+ def __len__(self):
31
+ return len(self.labels)
32
+
33
+ def __getitem__(self, idx):
34
+ return self.features[idx], self.labels[idx]
35
+
36
+
37
+ def _load_real_data(model_name: str):
38
+ file_cfg = FEATURE_FILES.get(model_name)
39
+ if file_cfg is None:
40
+ return None
41
+
42
+ feat_path = os.path.join(DATA_DIR, file_cfg["features"])
43
+ label_path = os.path.join(DATA_DIR, file_cfg["labels"])
44
+
45
+ if os.path.exists(feat_path) and os.path.exists(label_path):
46
+ features = np.load(feat_path)
47
+ labels = np.load(label_path)
48
+ print(f"[DATA] Loaded real data for '{model_name}': {features.shape[0]} samples, {features.shape[1]} features")
49
+ return features, labels
50
+
51
+ return None
52
+
53
+
54
+ def _generate_synthetic_data(model_name: str):
55
+ cfg = SYNTHETIC_CONFIG.get(model_name, SYNTHETIC_CONFIG["face_orientation"])
56
+ n = cfg["num_samples"]
57
+ d = cfg["num_features"]
58
+ c = cfg["num_classes"]
59
+
60
+ rng = np.random.RandomState(42)
61
+ features = rng.randn(n, d).astype(np.float32)
62
+ labels = rng.randint(0, c, size=n).astype(np.int64)
63
+
64
+ print(f"[DATA] Using synthetic data for '{model_name}': {n} samples, {d} features, {c} classes")
65
+ return features, labels
66
+
67
+
68
+ def get_dataloaders(model_name: str, batch_size: int = 32, split_ratios=(0.7, 0.15, 0.15), seed: int = 42):
69
+ data = _load_real_data(model_name)
70
+ if data is None:
71
+ data = _generate_synthetic_data(model_name)
72
+
73
+ features, labels = data
74
+ num_features = features.shape[1]
75
+ num_classes = int(labels.max()) + 1
76
+
77
+ dataset = FeatureVectorDataset(features, labels)
78
+ total = len(dataset)
79
+ train_n = int(total * split_ratios[0])
80
+ val_n = int(total * split_ratios[1])
81
+ test_n = total - train_n - val_n
82
+
83
+ gen = torch.Generator().manual_seed(seed)
84
+ train_ds, val_ds, test_ds = random_split(dataset, [train_n, val_n, test_n], generator=gen)
85
+
86
+ train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
87
+ val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)
88
+ test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)
89
+
90
+ print(f"[DATA] Split: train={train_n}, val={val_n}, test={test_n}")
91
+ return train_loader, val_loader, test_loader, num_features, num_classes
models/train.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Run from repo root: python -m models.train (or cd models && python train.py)
2
+
3
+ import json
4
+ import os
5
+ import random
6
+
7
+ import numpy as np as np
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.optim as optim
11
+
12
+ from prepare_dataset import get_dataloaders
13
+
14
+ CFG = {
15
+ "model_name": "face_orientation", # "face_orientation" or "eye_behaviour"
16
+ "epochs": 30,
17
+ "batch_size": 32,
18
+ "lr": 1e-3,
19
+ "seed": 42,
20
+ "split_ratios": (0.7, 0.15, 0.15),
21
+ "checkpoints_dir": {
22
+ "face_orientation": os.path.join(os.path.dirname(__file__), "face_orientation_model"),
23
+ "eye_behaviour": os.path.join(os.path.dirname(__file__), "eye_behaviour_model"),
24
+ },
25
+ "logs_dir": os.path.join(os.path.dirname(__file__), "..", "evaluation", "logs"),
26
+ }
27
+
28
+
29
+ def set_seed(seed: int):
30
+ random.seed(seed)
31
+ np.random.seed(seed)
32
+ torch.manual_seed(seed)
33
+ if torch.cuda.is_available():
34
+ torch.cuda.manual_seed_all(seed)
35
+
36
+
37
+ class BaseModel(nn.Module):
38
+ def __init__(self, num_features: int, num_classes: int):
39
+ super().__init__()
40
+ self.network = nn.Sequential(
41
+ nn.Linear(num_features, 64),
42
+ nn.ReLU(),
43
+ nn.Linear(64, 32),
44
+ nn.ReLU(),
45
+ nn.Linear(32, num_classes),
46
+ )
47
+
48
+ def forward(self, x):
49
+ return self.network(x)
50
+
51
+ def training_step(self, loader, optimizer, criterion, device):
52
+ self.train()
53
+ total_loss = 0.0
54
+ correct = 0
55
+ total = 0
56
+
57
+ for features, labels in loader:
58
+ features, labels = features.to(device), labels.to(device)
59
+
60
+ optimizer.zero_grad()
61
+ outputs = self(features)
62
+ loss = criterion(outputs, labels)
63
+ loss.backward()
64
+ optimizer.step()
65
+
66
+ total_loss += loss.item() * features.size(0)
67
+ correct += (outputs.argmax(dim=1) == labels).sum().item()
68
+ total += features.size(0)
69
+
70
+ return total_loss / total, correct / total
71
+
72
+ @torch.no_grad()
73
+ def validation_step(self, loader, criterion, device):
74
+ self.eval()
75
+ total_loss = 0.0
76
+ correct = 0
77
+ total = 0
78
+
79
+ for features, labels in loader:
80
+ features, labels = features.to(device), labels.to(device)
81
+ outputs = self(features)
82
+ loss = criterion(outputs, labels)
83
+
84
+ total_loss += loss.item() * features.size(0)
85
+ correct += (outputs.argmax(dim=1) == labels).sum().item()
86
+ total += features.size(0)
87
+
88
+ return total_loss / total, correct / total
89
+
90
+ @torch.no_grad()
91
+ def test_step(self, loader, criterion, device):
92
+ self.eval()
93
+ total_loss = 0.0
94
+ correct = 0
95
+ total = 0
96
+
97
+ for features, labels in loader:
98
+ features, labels = features.to(device), labels.to(device)
99
+ outputs = self(features)
100
+ loss = criterion(outputs, labels)
101
+
102
+ total_loss += loss.item() * features.size(0)
103
+ correct += (outputs.argmax(dim=1) == labels).sum().item()
104
+ total += features.size(0)
105
+
106
+ return total_loss / total, correct / total
107
+
108
+
109
+ def main():
110
+ set_seed(CFG["seed"])
111
+
112
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
113
+ print(f"[TRAIN] Device: {device}")
114
+ print(f"[TRAIN] Model: {CFG['model_name']}")
115
+
116
+ train_loader, val_loader, test_loader, num_features, num_classes = get_dataloaders(
117
+ model_name=CFG["model_name"],
118
+ batch_size=CFG["batch_size"],
119
+ split_ratios=CFG["split_ratios"],
120
+ seed=CFG["seed"],
121
+ )
122
+
123
+ model = BaseModel(num_features, num_classes).to(device)
124
+ criterion = nn.CrossEntropyLoss()
125
+ optimizer = optim.Adam(model.parameters(), lr=CFG["lr"])
126
+
127
+ print(f"[TRAIN] Parameters: {sum(p.numel() for p in model.parameters()):,}")
128
+
129
+ ckpt_dir = CFG["checkpoints_dir"][CFG["model_name"]]
130
+ os.makedirs(ckpt_dir, exist_ok=True)
131
+ best_ckpt_path = os.path.join(ckpt_dir, "best_model.pt")
132
+
133
+ history = {
134
+ "model_name": CFG["model_name"],
135
+ "epochs": [],
136
+ "train_loss": [],
137
+ "train_acc": [],
138
+ "val_loss": [],
139
+ "val_acc": [],
140
+ }
141
+
142
+ best_val_acc = 0.0
143
+
144
+ print(f"\n{'Epoch':>6} | {'Train Loss':>10} | {'Train Acc':>9} | {'Val Loss':>10} | {'Val Acc':>9}")
145
+ print("-" * 60)
146
+
147
+ for epoch in range(1, CFG["epochs"] + 1):
148
+ train_loss, train_acc = model.training_step(train_loader, optimizer, criterion, device)
149
+ val_loss, val_acc = model.validation_step(val_loader, criterion, device)
150
+
151
+ history["epochs"].append(epoch)
152
+ history["train_loss"].append(round(train_loss, 4))
153
+ history["train_acc"].append(round(train_acc, 4))
154
+ history["val_loss"].append(round(val_loss, 4))
155
+ history["val_acc"].append(round(val_acc, 4))
156
+
157
+ marker = ""
158
+ if val_acc > best_val_acc:
159
+ best_val_acc = val_acc
160
+ torch.save(model.state_dict(), best_ckpt_path)
161
+ marker = " *"
162
+
163
+ print(f"{epoch:>6} | {train_loss:>10.4f} | {train_acc:>8.2%} | {val_loss:>10.4f} | {val_acc:>8.2%}{marker}")
164
+
165
+ print(f"\nBest validation accuracy: {best_val_acc:.2%}")
166
+ print(f"Checkpoint saved to: {best_ckpt_path}")
167
+
168
+ model.load_state_dict(torch.load(best_ckpt_path, weights_only=True))
169
+ test_loss, test_acc = model.test_step(test_loader, criterion, device)
170
+ print(f"\n[TEST] Loss: {test_loss:.4f} | Accuracy: {test_acc:.2%}")
171
+
172
+ history["test_loss"] = round(test_loss, 4)
173
+ history["test_acc"] = round(test_acc, 4)
174
+
175
+ logs_dir = CFG["logs_dir"]
176
+ os.makedirs(logs_dir, exist_ok=True)
177
+ log_path = os.path.join(logs_dir, f"{CFG['model_name']}_training_log.json")
178
+
179
+ with open(log_path, "w") as f:
180
+ json.dump(history, f, indent=2)
181
+
182
+ print(f"[LOG] Training history saved to: {log_path}")
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
ui/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # ui
2
+
3
+ Live demo and session view — structure up to the team.