pranavinani commited on
Commit
407933e
·
0 Parent(s):

Deploy gaze emotion detection API

Browse files
Files changed (8) hide show
  1. .gitignore +41 -0
  2. Dockerfile +12 -0
  3. README.md +44 -0
  4. app.py +233 -0
  5. deploy.sh +36 -0
  6. gaze_emotion.py +403 -0
  7. requirements.txt +10 -0
  8. test_api.py +21 -0
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+
22
+ # Virtual environments
23
+ venv/
24
+ env/
25
+ ENV/
26
+
27
+ # IDE
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+ *.swo
32
+
33
+ # OS
34
+ .DS_Store
35
+ Thumbs.db
36
+
37
+ # Test files
38
+ test_image.jpg
39
+ *.jpg
40
+ *.png
41
+ *.jpeg
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /code
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY app.py .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Gaze and Emotion Detection API
3
+ emoji: 👁️
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # Gaze and Emotion Detection API
11
+
12
+ This API provides real-time gaze tracking and emotion detection from images using MediaPipe and DeepFace.
13
+
14
+ ## Features
15
+ - Face detection and tracking
16
+ - Gaze direction estimation (LEFT, RIGHT, UP, DOWN, CENTER)
17
+ - Emotion recognition (happy, sad, angry, fear, surprise, disgust, neutral)
18
+ - Concentration score calculation
19
+ - Blink detection
20
+
21
+ ## API Endpoints
22
+ - `GET /` - Health check
23
+ - `POST /analyze` - Upload image for analysis
24
+
25
+ ## Usage
26
+ Send a POST request to `/analyze` with an image file to get gaze and emotion data in JSON format.
27
+
28
+ ### Example Response:
29
+ ```json
30
+ {
31
+ "emotion": "happy",
32
+ "face_detected": true,
33
+ "gaze_direction": "CENTER",
34
+ "concentration_score": 85.5,
35
+ "blinking": false,
36
+ "gaze_positions": {
37
+ "left_eye": {"horizontal": 0.45, "vertical": 0.52},
38
+ "right_eye": {"horizontal": 0.48, "vertical": 0.50}
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## Testing
44
+ You can test the API using the interactive documentation at `/docs` endpoint.
app.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import cv2
4
+ import mediapipe as mp
5
+ import numpy as np
6
+ from deepface import DeepFace
7
+ import base64
8
+ from io import BytesIO
9
+ from PIL import Image
10
+ import json
11
+
12
+ app = FastAPI()
13
+
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"],
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ # Initialize MediaPipe
23
+ mp_face_mesh = mp.solutions.face_mesh
24
+ face_mesh = mp_face_mesh.FaceMesh(
25
+ refine_landmarks=True,
26
+ min_detection_confidence=0.5,
27
+ min_tracking_confidence=0.5
28
+ )
29
+
30
+ mp_face_detection = mp.solutions.face_detection
31
+ face_detection = mp_face_detection.FaceDetection(min_detection_confidence=0.5)
32
+
33
+ # Eye and iris landmark indices
34
+ LEFT_EYE = [33, 160, 158, 133, 153, 144]
35
+ RIGHT_EYE = [362, 385, 387, 263, 373, 380]
36
+ LEFT_IRIS = [468, 469, 470, 471, 472]
37
+ RIGHT_IRIS = [473, 474, 475, 476, 477]
38
+
39
+ def eye_aspect_ratio(landmarks, eye_points, image_w, image_h):
40
+ p = []
41
+ for idx in eye_points:
42
+ lm = landmarks[idx]
43
+ x, y = int(lm.x * image_w), int(lm.y * image_h)
44
+ p.append((x, y))
45
+
46
+ A = np.linalg.norm(np.array(p[1]) - np.array(p[5]))
47
+ B = np.linalg.norm(np.array(p[2]) - np.array(p[4]))
48
+ C = np.linalg.norm(np.array(p[0]) - np.array(p[3]))
49
+ ear = (A + B) / (2.0 * C)
50
+ return ear
51
+
52
+ def get_iris_position_2d(landmarks, iris_points, eye_points, image_w, image_h):
53
+ try:
54
+ iris_center = landmarks[iris_points[0]]
55
+ iris_x = iris_center.x * image_w
56
+ iris_y = iris_center.y * image_h
57
+
58
+ left_corner = landmarks[eye_points[0]]
59
+ right_corner = landmarks[eye_points[3]]
60
+ top_point = landmarks[eye_points[1]]
61
+ bottom_point = landmarks[eye_points[4]]
62
+
63
+ eye_left = left_corner.x * image_w
64
+ eye_right = right_corner.x * image_w
65
+ eye_top = top_point.y * image_h
66
+ eye_bottom = bottom_point.y * image_h
67
+
68
+ eye_width = eye_right - eye_left
69
+ eye_height = eye_bottom - eye_top
70
+
71
+ if eye_width > 0:
72
+ horizontal_pos = (iris_x - eye_left) / eye_width
73
+ horizontal_pos = max(0, min(1, horizontal_pos))
74
+ else:
75
+ horizontal_pos = 0.5
76
+
77
+ if eye_height > 0:
78
+ vertical_pos = (iris_y - eye_top) / eye_height
79
+ vertical_pos = max(0, min(1, vertical_pos))
80
+ else:
81
+ vertical_pos = 0.5
82
+
83
+ return horizontal_pos, vertical_pos
84
+ except:
85
+ return 0.5, 0.5
86
+
87
+ def get_gaze_direction(h_pos, v_pos):
88
+ directions = []
89
+
90
+ if h_pos < 0.35:
91
+ directions.append("LEFT")
92
+ elif h_pos > 0.65:
93
+ directions.append("RIGHT")
94
+
95
+ if v_pos < 0.35:
96
+ directions.append("UP")
97
+ elif v_pos > 0.65:
98
+ directions.append("DOWN")
99
+
100
+ if not directions:
101
+ directions.append("CENTER")
102
+
103
+ return " + ".join(directions)
104
+
105
+ def get_gaze_score(left_h_pos, left_v_pos, right_h_pos, right_v_pos):
106
+ avg_h_pos = (left_h_pos + right_h_pos) / 2.0
107
+ avg_v_pos = (left_v_pos + right_v_pos) / 2.0
108
+
109
+ h_score = 1.0 if 0.35 <= avg_h_pos <= 0.65 else 0.5
110
+ v_score = 1.0 if 0.35 <= avg_v_pos <= 0.65 else 0.5
111
+
112
+ return (h_score + v_score) / 2.0
113
+
114
+ def get_head_pose_score(landmarks, image_w, image_h):
115
+ nose = landmarks[1]
116
+ x = nose.x * image_w
117
+ y = nose.y * image_h
118
+ d = np.linalg.norm(np.array([x - image_w / 2, y - image_h / 2]))
119
+ return 1.0 if d < 0.3 * image_w else 0.0
120
+
121
+ def compute_concentration_score(gaze, head_pose, blink):
122
+ score = 0.5 * gaze + 0.3 * head_pose + 0.2 * (0 if blink else 1)
123
+ return round(score * 100, 2)
124
+
125
+ def analyze_emotion(frame):
126
+ try:
127
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
128
+ results = face_detection.process(frame_rgb)
129
+
130
+ if results.detections:
131
+ for detection in results.detections:
132
+ bboxC = detection.location_data.relative_bounding_box
133
+ ih, iw, _ = frame.shape
134
+ x, y, w, h = int(bboxC.xmin * iw), int(bboxC.ymin * ih), \
135
+ int(bboxC.width * iw), int(bboxC.height * ih)
136
+
137
+ x, y = max(0, x), max(0, y)
138
+ w = min(w, iw - x)
139
+ h = min(h, ih - y)
140
+
141
+ if w > 50 and h > 50:
142
+ face_img = frame[y:y+h, x:x+w]
143
+ emotion_result = DeepFace.analyze(face_img,
144
+ actions=['emotion'],
145
+ enforce_detection=False)
146
+
147
+ if isinstance(emotion_result, list):
148
+ return emotion_result[0]['dominant_emotion']
149
+ else:
150
+ return emotion_result['dominant_emotion']
151
+ except Exception as e:
152
+ print(f"Error analyzing emotion: {e}")
153
+
154
+ return "neutral"
155
+
156
+ @app.post("/analyze")
157
+ async def analyze_image(file: UploadFile = File(...)):
158
+ try:
159
+ # Read image
160
+ contents = await file.read()
161
+ nparr = np.frombuffer(contents, np.uint8)
162
+ frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
163
+
164
+ if frame is None:
165
+ raise HTTPException(status_code=400, detail="Invalid image format")
166
+
167
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
168
+ image_h, image_w, _ = frame.shape
169
+
170
+ # Process face mesh
171
+ results = face_mesh.process(frame_rgb)
172
+
173
+ # Analyze emotion
174
+ emotion = analyze_emotion(frame)
175
+
176
+ response_data = {
177
+ "emotion": emotion,
178
+ "face_detected": False,
179
+ "gaze_direction": "UNKNOWN",
180
+ "concentration_score": 0.0,
181
+ "blinking": False,
182
+ "gaze_positions": {
183
+ "left_eye": {"horizontal": 0.5, "vertical": 0.5},
184
+ "right_eye": {"horizontal": 0.5, "vertical": 0.5}
185
+ }
186
+ }
187
+
188
+ if results.multi_face_landmarks:
189
+ for face_landmarks in results.multi_face_landmarks:
190
+ landmarks = face_landmarks.landmark
191
+ response_data["face_detected"] = True
192
+
193
+ # Calculate eye aspect ratios
194
+ left_ear = eye_aspect_ratio(landmarks, LEFT_EYE, image_w, image_h)
195
+ right_ear = eye_aspect_ratio(landmarks, RIGHT_EYE, image_w, image_h)
196
+ avg_ear = (left_ear + right_ear) / 2
197
+
198
+ # Get iris positions
199
+ left_h_pos, left_v_pos = get_iris_position_2d(landmarks, LEFT_IRIS, LEFT_EYE, image_w, image_h)
200
+ right_h_pos, right_v_pos = get_iris_position_2d(landmarks, RIGHT_IRIS, RIGHT_EYE, image_w, image_h)
201
+
202
+ # Calculate gaze direction
203
+ avg_direction = get_gaze_direction((left_h_pos + right_h_pos) / 2, (left_v_pos + right_v_pos) / 2)
204
+
205
+ # Calculate scores
206
+ blink = avg_ear < 0.25
207
+ gaze_score = get_gaze_score(left_h_pos, left_v_pos, right_h_pos, right_v_pos)
208
+ head_score = get_head_pose_score(landmarks, image_w, image_h)
209
+ concentration = compute_concentration_score(gaze_score, head_score, blink)
210
+
211
+ response_data.update({
212
+ "gaze_direction": avg_direction,
213
+ "concentration_score": float(concentration),
214
+ "blinking": bool(blink),
215
+ "gaze_positions": {
216
+ "left_eye": {"horizontal": float(left_h_pos), "vertical": float(left_v_pos)},
217
+ "right_eye": {"horizontal": float(right_h_pos), "vertical": float(right_v_pos)}
218
+ }
219
+ })
220
+ break
221
+
222
+ return response_data
223
+
224
+ except Exception as e:
225
+ raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
226
+
227
+ @app.get("/")
228
+ async def root():
229
+ return {"message": "Gaze and Emotion Detection API"}
230
+
231
+ if __name__ == "__main__":
232
+ import uvicorn
233
+ uvicorn.run(app, host="0.0.0.0", port=7860)
deploy.sh ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ echo "🚀 Deploying to Hugging Face Spaces..."
4
+
5
+ # Check if git is initialized
6
+ if [ ! -d ".git" ]; then
7
+ echo "Initializing git repository..."
8
+ git init
9
+ fi
10
+
11
+ # Add all files
12
+ echo "Adding files to git..."
13
+ git add .
14
+
15
+ # Commit changes
16
+ echo "Committing changes..."
17
+ git commit -m "Deploy gaze emotion detection API"
18
+
19
+ echo "✅ Ready for deployment!"
20
+ echo ""
21
+ echo "To deploy to Hugging Face Spaces:"
22
+ echo "1. Go to https://huggingface.co/spaces"
23
+ echo "2. Click 'Create new Space'"
24
+ echo "3. Choose:"
25
+ echo " - Space name: gaze-emotion-api"
26
+ echo " - SDK: Docker"
27
+ echo " - Hardware: CPU basic (free)"
28
+ echo "4. After creating, run:"
29
+ echo " git remote add origin https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME"
30
+ echo " git push origin main"
31
+ echo ""
32
+ echo "Or upload these files manually:"
33
+ echo " - app.py"
34
+ echo " - requirements.txt"
35
+ echo " - Dockerfile"
36
+ echo " - README.md"
gaze_emotion.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import mediapipe as mp
3
+ import numpy as np
4
+ import time
5
+ from collections import deque
6
+ from deepface import DeepFace
7
+
8
+ # Initialize MediaPipe Face Mesh with iris landmarks
9
+ mp_face_mesh = mp.solutions.face_mesh
10
+ face_mesh = mp_face_mesh.FaceMesh(
11
+ refine_landmarks=True,
12
+ min_detection_confidence=0.5,
13
+ min_tracking_confidence=0.5
14
+ )
15
+
16
+ # Initialize MediaPipe Face Detection for emotion analysis
17
+ mp_face_detection = mp.solutions.face_detection
18
+ face_detection = mp_face_detection.FaceDetection(min_detection_confidence=0.5)
19
+ mp_drawing = mp.solutions.drawing_utils
20
+
21
+ # Eye landmark indices
22
+ LEFT_EYE = [33, 160, 158, 133, 153, 144]
23
+ RIGHT_EYE = [362, 385, 387, 263, 373, 380]
24
+
25
+ # Iris landmark indices
26
+ LEFT_IRIS = [468, 469, 470, 471, 472]
27
+ RIGHT_IRIS = [473, 474, 475, 476, 477]
28
+
29
+ # Global variables
30
+ score_history = deque(maxlen=10)
31
+ distraction = 0
32
+ last_emotion_time = 0
33
+ current_emotion = "neutral"
34
+
35
+ # Define emotion colors (BGR format)
36
+ emotion_colors = {
37
+ 'angry': (0, 0, 255), # Red
38
+ 'disgust': (0, 140, 255), # Orange
39
+ 'fear': (0, 0, 0), # Black
40
+ 'happy': (0, 255, 255), # Yellow
41
+ 'sad': (255, 0, 0), # Blue
42
+ 'surprise': (255, 0, 255),# Purple
43
+ 'neutral': (255, 255, 255)# White
44
+ }
45
+
46
+ def eye_aspect_ratio(landmarks, eye_points, image_w, image_h, frame):
47
+ p = []
48
+ for idx in eye_points:
49
+ lm = landmarks[idx]
50
+ x, y = int(lm.x * image_w), int(lm.y * image_h)
51
+ cv2.circle(frame, (x, y), 2, (0, 255, 0), -1)
52
+ p.append((x, y))
53
+
54
+ A = np.linalg.norm(np.array(p[1]) - np.array(p[5]))
55
+ B = np.linalg.norm(np.array(p[2]) - np.array(p[4]))
56
+ C = np.linalg.norm(np.array(p[0]) - np.array(p[3]))
57
+ ear = (A + B) / (2.0 * C)
58
+ return ear
59
+
60
+ def draw_iris(landmarks, iris_points, image_w, image_h, frame, color=(0, 255, 255)):
61
+ """Draw iris/pupil tracking"""
62
+ iris_coords = []
63
+ for idx in iris_points:
64
+ lm = landmarks[idx]
65
+ x, y = int(lm.x * image_w), int(lm.y * image_h)
66
+ iris_coords.append((x, y))
67
+
68
+ if len(iris_coords) >= 5:
69
+ # Draw iris center (first point is center)
70
+ center = iris_coords[0]
71
+ cv2.circle(frame, center, 4, color, -1)
72
+
73
+ # Draw iris boundary
74
+ for i in range(1, len(iris_coords)):
75
+ cv2.circle(frame, iris_coords[i], 2, color, -1)
76
+
77
+ # Draw iris circle
78
+ if len(iris_coords) >= 3:
79
+ radius = int(np.linalg.norm(np.array(iris_coords[0]) - np.array(iris_coords[1])))
80
+ cv2.circle(frame, center, radius, color, 2)
81
+
82
+ return iris_coords
83
+
84
+ def get_iris_position_2d(landmarks, iris_points, eye_points, image_w, image_h):
85
+ """Get iris position relative to eye in both X and Y directions"""
86
+ try:
87
+ # Get iris center
88
+ iris_center = landmarks[iris_points[0]]
89
+ iris_x = iris_center.x * image_w
90
+ iris_y = iris_center.y * image_h
91
+
92
+ # Get eye corners for horizontal position
93
+ left_corner = landmarks[eye_points[0]]
94
+ right_corner = landmarks[eye_points[3]]
95
+
96
+ # Get eye top and bottom for vertical position
97
+ top_point = landmarks[eye_points[1]]
98
+ bottom_point = landmarks[eye_points[4]]
99
+
100
+ # Calculate eye dimensions
101
+ eye_left = left_corner.x * image_w
102
+ eye_right = right_corner.x * image_w
103
+ eye_top = top_point.y * image_h
104
+ eye_bottom = bottom_point.y * image_h
105
+
106
+ eye_width = eye_right - eye_left
107
+ eye_height = eye_bottom - eye_top
108
+
109
+ # Calculate relative positions (0 to 1)
110
+ if eye_width > 0:
111
+ horizontal_pos = (iris_x - eye_left) / eye_width
112
+ horizontal_pos = max(0, min(1, horizontal_pos))
113
+ else:
114
+ horizontal_pos = 0.5
115
+
116
+ if eye_height > 0:
117
+ vertical_pos = (iris_y - eye_top) / eye_height
118
+ vertical_pos = max(0, min(1, vertical_pos))
119
+ else:
120
+ vertical_pos = 0.5
121
+
122
+ return horizontal_pos, vertical_pos
123
+ except:
124
+ return 0.5, 0.5
125
+
126
+ def get_gaze_direction(h_pos, v_pos):
127
+ """Determine gaze direction based on iris position"""
128
+ directions = []
129
+
130
+ # Horizontal direction
131
+ if h_pos < 0.35:
132
+ directions.append("LEFT")
133
+ elif h_pos > 0.65:
134
+ directions.append("RIGHT")
135
+
136
+ # Vertical direction
137
+ if v_pos < 0.35:
138
+ directions.append("UP")
139
+ elif v_pos > 0.65:
140
+ directions.append("DOWN")
141
+
142
+ if not directions:
143
+ directions.append("CENTER")
144
+
145
+ return " + ".join(directions)
146
+
147
+ def is_blinking(ear, threshold=0.25):
148
+ return ear < threshold
149
+
150
+ def get_head_pose_score(landmarks, image_w, image_h):
151
+ nose = landmarks[1]
152
+ x = nose.x * image_w
153
+ y = nose.y * image_h
154
+ d = np.linalg.norm(np.array([x - image_w / 2, y - image_h / 2]))
155
+ return 1.0 if d < 0.3 * image_w else 0.0
156
+
157
+ def get_gaze_score(left_h_pos, left_v_pos, right_h_pos, right_v_pos):
158
+ """Calculate gaze score based on iris positions"""
159
+ avg_h_pos = (left_h_pos + right_h_pos) / 2.0
160
+ avg_v_pos = (left_v_pos + right_v_pos) / 2.0
161
+
162
+ # Score higher when looking at center
163
+ h_score = 1.0 if 0.35 <= avg_h_pos <= 0.65 else 0.5
164
+ v_score = 1.0 if 0.35 <= avg_v_pos <= 0.65 else 0.5
165
+
166
+ return (h_score + v_score) / 2.0
167
+
168
+ def compute_concentration_score(gaze, head_pose, blink):
169
+ score = 0.5 * gaze + 0.3 * head_pose + 0.2 * (0 if blink else 1)
170
+ return round(score * 100, 2)
171
+
172
+ def draw_concentration_bar(score, frame):
173
+ bar_width = 200
174
+ bar_height = 30
175
+ bar_x = 30
176
+ bar_y = 100
177
+
178
+ # Background
179
+ cv2.rectangle(frame, (bar_x, bar_y), (bar_x + bar_width, bar_y + bar_height), (50, 50, 50), -1)
180
+
181
+ # Fill
182
+ fill_width = int(score * bar_width / 100)
183
+ color = (0, 255, 0) if score > 60 else (0, 150, 255) if score > 40 else (0, 100, 255)
184
+ cv2.rectangle(frame, (bar_x, bar_y), (bar_x + fill_width, bar_y + bar_height), color, -1)
185
+
186
+ # Border
187
+ cv2.rectangle(frame, (bar_x, bar_y), (bar_x + bar_width, bar_y + bar_height), (200, 200, 200), 2)
188
+
189
+ # Text
190
+ cv2.putText(frame, f"{score}%", (bar_x + bar_width + 10, bar_y + bar_height // 2 + 5),
191
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (200, 200, 200), 2)
192
+
193
+ def draw_gaze_tracker(left_h_pos, left_v_pos, right_h_pos, right_v_pos, frame):
194
+ """Draw comprehensive gaze tracking display"""
195
+ # Calculate average gaze
196
+ avg_h = (left_h_pos + right_h_pos) / 2.0
197
+ avg_v = (left_v_pos + right_v_pos) / 2.0
198
+
199
+ # Draw gaze grid
200
+ grid_x = 350
201
+ grid_y = 50
202
+ grid_size = 120
203
+
204
+ # Grid background
205
+ cv2.rectangle(frame, (grid_x, grid_y), (grid_x + grid_size, grid_y + grid_size), (50, 50, 50), -1)
206
+ cv2.rectangle(frame, (grid_x, grid_y), (grid_x + grid_size, grid_y + grid_size), (200, 200, 200), 2)
207
+
208
+ # Grid lines
209
+ for i in range(1, 3):
210
+ # Vertical lines
211
+ x_line = grid_x + i * grid_size // 3
212
+ cv2.line(frame, (x_line, grid_y), (x_line, grid_y + grid_size), (100, 100, 100), 1)
213
+ # Horizontal lines
214
+ y_line = grid_y + i * grid_size // 3
215
+ cv2.line(frame, (grid_x, y_line), (grid_x + grid_size, y_line), (100, 100, 100), 1)
216
+
217
+ # Gaze position indicator
218
+ gaze_x = int(grid_x + avg_h * grid_size)
219
+ gaze_y = int(grid_y + avg_v * grid_size)
220
+ cv2.circle(frame, (gaze_x, gaze_y), 8, (0, 255, 255), -1)
221
+ cv2.circle(frame, (gaze_x, gaze_y), 12, (0, 255, 255), 2)
222
+
223
+ # Center target
224
+ center_x = grid_x + grid_size // 2
225
+ center_y = grid_y + grid_size // 2
226
+ cv2.circle(frame, (center_x, center_y), 15, (0, 255, 0), 2)
227
+
228
+ # Labels
229
+ cv2.putText(frame, "Gaze Tracker", (grid_x, grid_y - 10),
230
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 2)
231
+
232
+ def draw_emotion_info(emotion, frame):
233
+ """Draw emotion information"""
234
+ emotion_x = 500
235
+ emotion_y = 50
236
+ emotion_width = 150
237
+ emotion_height = 120
238
+
239
+ # Background
240
+ cv2.rectangle(frame, (emotion_x, emotion_y), (emotion_x + emotion_width, emotion_y + emotion_height), (40, 40, 40), -1)
241
+ cv2.rectangle(frame, (emotion_x, emotion_y), (emotion_x + emotion_width, emotion_y + emotion_height), (200, 200, 200), 2)
242
+
243
+ # Title
244
+ cv2.putText(frame, "Emotion", (emotion_x + 10, emotion_y + 25),
245
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 2)
246
+
247
+ # Current emotion
248
+ emotion_color = emotion_colors.get(emotion, (255, 255, 255))
249
+ cv2.putText(frame, emotion.upper(), (emotion_x + 10, emotion_y + 55),
250
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, emotion_color, 2)
251
+
252
+ # Emotion indicator circle
253
+ cv2.circle(frame, (emotion_x + 75, emotion_y + 85), 20, emotion_color, -1)
254
+ cv2.circle(frame, (emotion_x + 75, emotion_y + 85), 20, (200, 200, 200), 2)
255
+
256
+ def analyze_emotion(frame, face_detection):
257
+ """Analyze emotion from frame"""
258
+ global last_emotion_time, current_emotion
259
+
260
+ current_time = time.time()
261
+
262
+ # Only analyze emotion every 1 second to reduce computational load
263
+ if current_time - last_emotion_time > 1.0:
264
+ try:
265
+ # Convert frame to RGB for face detection
266
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
267
+ results = face_detection.process(frame_rgb)
268
+
269
+ if results.detections:
270
+ for detection in results.detections:
271
+ # Get bounding box coordinates
272
+ bboxC = detection.location_data.relative_bounding_box
273
+ ih, iw, _ = frame.shape
274
+ x, y, w, h = int(bboxC.xmin * iw), int(bboxC.ymin * ih), \
275
+ int(bboxC.width * iw), int(bboxC.height * ih)
276
+
277
+ # Ensure coordinates are within image boundaries
278
+ x, y = max(0, x), max(0, y)
279
+ w = min(w, iw - x)
280
+ h = min(h, ih - y)
281
+
282
+ if w > 50 and h > 50: # Ensure face is large enough
283
+ # Extract face for emotion analysis
284
+ face_img = frame[y:y+h, x:x+w]
285
+
286
+ # Analyze emotion using DeepFace
287
+ emotion_result = DeepFace.analyze(face_img,
288
+ actions=['emotion'],
289
+ enforce_detection=False)
290
+
291
+ # Extract dominant emotion
292
+ if isinstance(emotion_result, list):
293
+ current_emotion = emotion_result[0]['dominant_emotion']
294
+ else:
295
+ current_emotion = emotion_result['dominant_emotion']
296
+
297
+ last_emotion_time = current_time
298
+ break
299
+ except Exception as e:
300
+ print(f"Error analyzing emotion: {e}")
301
+
302
+ return current_emotion
303
+
304
+ # Main loop
305
+ cap = cv2.VideoCapture(0)
306
+ blink_counter = 0
307
+ prev_frame_time = 0
308
+
309
+ while True:
310
+ ret, frame = cap.read()
311
+ if not ret:
312
+ break
313
+
314
+ # Calculate FPS
315
+ new_frame_time = time.time()
316
+ fps = 1/(new_frame_time-prev_frame_time) if prev_frame_time != 0 else 0
317
+ prev_frame_time = new_frame_time
318
+
319
+ # Create UI background
320
+ ui_bg = frame.copy()
321
+ cv2.rectangle(ui_bg, (0, 0), (frame.shape[1], 220), (30, 30, 30), -1)
322
+ cv2.addWeighted(ui_bg, 0.7, frame, 0.3, 0, frame)
323
+
324
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
325
+ image_h, image_w, _ = frame.shape
326
+
327
+ # Process face mesh for eye tracking
328
+ results = face_mesh.process(frame_rgb)
329
+
330
+ # Analyze emotion
331
+ emotion = analyze_emotion(frame, face_detection)
332
+
333
+ if results.multi_face_landmarks:
334
+ for face_landmarks in results.multi_face_landmarks:
335
+ landmarks = face_landmarks.landmark
336
+
337
+ # Calculate eye aspect ratios
338
+ left_ear = eye_aspect_ratio(landmarks, LEFT_EYE, image_w, image_h, frame)
339
+ right_ear = eye_aspect_ratio(landmarks, RIGHT_EYE, image_w, image_h, frame)
340
+ avg_ear = (left_ear + right_ear) / 2
341
+
342
+ # Draw iris tracking
343
+ left_iris_coords = draw_iris(landmarks, LEFT_IRIS, image_w, image_h, frame, (255, 100, 0))
344
+ right_iris_coords = draw_iris(landmarks, RIGHT_IRIS, image_w, image_h, frame, (0, 100, 255))
345
+
346
+ # Get 2D iris positions
347
+ left_h_pos, left_v_pos = get_iris_position_2d(landmarks, LEFT_IRIS, LEFT_EYE, image_w, image_h)
348
+ right_h_pos, right_v_pos = get_iris_position_2d(landmarks, RIGHT_IRIS, RIGHT_EYE, image_w, image_h)
349
+
350
+ # Get gaze directions
351
+ left_direction = get_gaze_direction(left_h_pos, left_v_pos)
352
+ right_direction = get_gaze_direction(right_h_pos, right_v_pos)
353
+ avg_direction = get_gaze_direction((left_h_pos + right_h_pos) / 2, (left_v_pos + right_v_pos) / 2)
354
+
355
+ # Calculate scores
356
+ blink = is_blinking(avg_ear)
357
+ gaze_score = get_gaze_score(left_h_pos, left_v_pos, right_h_pos, right_v_pos)
358
+ head_score = get_head_pose_score(landmarks, image_w, image_h)
359
+ concentration = compute_concentration_score(gaze_score, head_score, blink)
360
+
361
+ # Update history and smooth score
362
+ score_history.append(concentration)
363
+ smooth_score = int(np.mean(score_history))
364
+
365
+ # Draw UI elements
366
+ draw_concentration_bar(smooth_score, frame)
367
+ draw_gaze_tracker(left_h_pos, left_v_pos, right_h_pos, right_v_pos, frame)
368
+ draw_emotion_info(emotion, frame)
369
+
370
+ # Text information
371
+ cv2.putText(frame, f"Concentration: {smooth_score}%", (30, 30),
372
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
373
+
374
+ cv2.putText(frame, f"Gaze: {avg_direction}", (30, 55),
375
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
376
+
377
+ cv2.putText(frame, f"Emotion: {emotion}", (30, 75),
378
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, emotion_colors.get(emotion, (255, 255, 255)), 2)
379
+
380
+ if blink:
381
+ cv2.putText(frame, "BLINKING", (30, 200),
382
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 150, 255), 2)
383
+
384
+ # Distraction tracking
385
+ if smooth_score < 40:
386
+ distraction += 1
387
+ if distraction > 1000:
388
+ distraction = 0
389
+ print(f'Focus alert! Looking {avg_direction}, Emotion: {emotion}')
390
+
391
+ # FPS and status
392
+ cv2.putText(frame, f"FPS: {fps:.1f}", (image_w - 120, 30),
393
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (200, 200, 0), 2)
394
+
395
+ status_color = (0, 255, 0) if distraction < 50 else (0, 100, 255)
396
+ cv2.circle(frame, (image_w - 30, 50), 10, status_color, -1)
397
+
398
+ cv2.imshow("Eye Tracking + Emotion Detection", frame)
399
+ if cv2.waitKey(1) & 0xFF == ord('q'):
400
+ break
401
+
402
+ cap.release()
403
+ cv2.destroyAllWindows()
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ opencv-python-headless
4
+ mediapipe
5
+ numpy
6
+ deepface
7
+ Pillow
8
+ python-multipart
9
+ # tensorflow
10
+ tf-keras
test_api.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+
4
+ # Test the API
5
+ def test_api():
6
+ # Use existing image.png file
7
+ url = "http://localhost:7860/analyze"
8
+
9
+ with open("image.png", "rb") as f:
10
+ files = {"file": ("image.png", f, "image/jpeg")}
11
+ response = requests.post(url, files=files)
12
+
13
+ if response.status_code == 200:
14
+ result = response.json()
15
+ print(json.dumps(result, indent=2))
16
+ else:
17
+ print(f"Error: {response.status_code}")
18
+ print(response.text)
19
+
20
+ if __name__ == "__main__":
21
+ test_api()