Spaces:
Sleeping
Sleeping
File size: 10,224 Bytes
db4445c 3b4893a db4445c 3b4893a db4445c f026c78 db4445c 3b4893a db4445c 1f80bce 3b4893a f026c78 3b4893a f026c78 3b4893a 1f80bce b91c0b1 1f80bce b91c0b1 1f80bce 3b4893a b91c0b1 3b4893a 1f80bce 4b259f1 1f80bce 4b259f1 3b4893a 1f80bce f026c78 1f80bce f026c78 1f80bce 3b4893a 1f80bce 3b4893a 1f80bce 3b4893a 1f80bce 3b4893a f026c78 db4445c 3b4893a db4445c 3b4893a db4445c 3b4893a f026c78 3b4893a 1f80bce f026c78 3b4893a 1f80bce f026c78 3b4893a db4445c 3b4893a f026c78 3b4893a f026c78 db4445c 3b4893a | 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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | """
Facial Expression Recognition App
LittleMonkeyLab | Goldsmiths Observatory
"""
import gradio as gr
import cv2
import mediapipe as mp
import numpy as np
import os
from datetime import datetime
# Initialize MediaPipe Face Mesh
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
static_image_mode=True,
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5
)
# Define key facial landmarks for expressions
FACIAL_LANDMARKS = {
'left_brow': [52, 65, 46], # inner, middle, outer
'right_brow': [285, 295, 276], # inner, middle, outer
'left_eye': [159, 145, 133], # top, bottom, outer
'right_eye': [386, 374, 362], # top, bottom, outer
'nose': [6, 197], # bridge, tip
'mouth': [61, 291, 0, 17, 13, 14], # left corner, right corner, top lip, bottom lip, upper inner, lower inner
'jaw': [17, 84, 314] # center, left, right
}
def calculate_distances(points, landmarks):
"""Calculate normalized distances between facial landmarks."""
def distance(p1_idx, p2_idx):
try:
p1 = points[p1_idx]
p2 = points[p2_idx]
return np.linalg.norm(p1 - p2)
except:
return 0.0
# Get face height for normalization
face_height = distance(FACIAL_LANDMARKS['nose'][0], FACIAL_LANDMARKS['jaw'][0])
if face_height == 0:
return {}
measurements = {
# Inner brow raising (AU1)
'inner_brow_raise': (
distance(FACIAL_LANDMARKS['left_brow'][0], FACIAL_LANDMARKS['nose'][0]) +
distance(FACIAL_LANDMARKS['right_brow'][0], FACIAL_LANDMARKS['nose'][0])
) / (2 * face_height),
# Outer brow raising (AU2)
'outer_brow_raise': (
distance(FACIAL_LANDMARKS['left_brow'][2], FACIAL_LANDMARKS['nose'][0]) +
distance(FACIAL_LANDMARKS['right_brow'][2], FACIAL_LANDMARKS['nose'][0])
) / (2 * face_height),
# Brow lowering (AU4)
'brow_furrow': distance(FACIAL_LANDMARKS['left_brow'][0], FACIAL_LANDMARKS['right_brow'][0]) / face_height,
# Eye opening (AU5)
'eye_opening': (
distance(FACIAL_LANDMARKS['left_eye'][0], FACIAL_LANDMARKS['left_eye'][1]) +
distance(FACIAL_LANDMARKS['right_eye'][0], FACIAL_LANDMARKS['right_eye'][1])
) / (2 * face_height),
# Smile width (AU12)
'smile_width': distance(FACIAL_LANDMARKS['mouth'][0], FACIAL_LANDMARKS['mouth'][1]) / face_height,
# Mouth height (AU25/26)
'mouth_opening': distance(FACIAL_LANDMARKS['mouth'][4], FACIAL_LANDMARKS['mouth'][5]) / face_height,
# Lip corner height (for smile/frown detection)
'lip_corner_height': (
(points[FACIAL_LANDMARKS['mouth'][0]][1] + points[FACIAL_LANDMARKS['mouth'][1]][1])/2 -
points[FACIAL_LANDMARKS['mouth'][2]][1]
) / face_height
}
return measurements
def analyze_expression(image):
if image is None:
return None, "No image provided", None
# Convert to RGB if needed
if len(image.shape) == 2:
image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
elif image.shape[2] == 4:
image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB)
# Process the image
results = face_mesh.process(image)
if not results.multi_face_landmarks:
return None, "No face detected", None
# Get landmarks
landmarks = results.multi_face_landmarks[0]
points = np.array([[lm.x, lm.y, lm.z] for lm in landmarks.landmark])
# Calculate facial measurements
measurements = calculate_distances(points, landmarks)
# Analyze Action Units with refined thresholds
aus = {
'AU01': measurements['inner_brow_raise'] > 0.12, # Inner Brow Raiser
'AU02': measurements['outer_brow_raise'] > 0.12, # Outer Brow Raiser
'AU04': measurements['brow_furrow'] < 0.2, # Brow Lowerer (tighter threshold for anger)
'AU05': measurements['eye_opening'] > 0.1, # Upper Lid Raiser
'AU12': measurements['smile_width'] > 0.45, # Lip Corner Puller
'AU25': measurements['mouth_opening'] > 0.08, # Lips Part
'AU26': measurements['mouth_opening'] > 0.15 # Jaw Drop
}
# Refined emotion classification with mutual exclusion
emotions = {}
# Check Anger first (takes precedence due to distinctive features)
if aus['AU04'] and not aus['AU12']: # Lowered brows without smile
emotions["Angry"] = True
# Happy - clear smile without anger indicators
elif aus['AU12'] and measurements['lip_corner_height'] < -0.02 and not aus['AU04']:
emotions["Happy"] = True
# Sad - raised inner brow with neutral/down mouth
elif aus['AU01'] and measurements['lip_corner_height'] > 0.01 and not aus['AU12']:
emotions["Sad"] = True
# Surprised - raised brows with open mouth
elif (aus['AU01'] or aus['AU02']) and (aus['AU25'] or aus['AU26']) and not aus['AU04']:
emotions["Surprised"] = True
# Neutral - no strong indicators of other emotions
elif not any([aus['AU01'], aus['AU02'], aus['AU04'], aus['AU12'], aus['AU26']]) and abs(measurements['lip_corner_height']) < 0.02:
emotions["Neutral"] = True
else:
emotions["Neutral"] = True # Default to neutral if no clear emotion is detected
# Create visualization
viz_image = image.copy()
h, w = viz_image.shape[:2]
# Draw facial landmarks with different colors for key points
colors = {
'brow': (0, 255, 0), # Green
'eye': (255, 255, 0), # Yellow
'nose': (0, 255, 255), # Cyan
'mouth': (255, 0, 255), # Magenta
'jaw': (255, 128, 0) # Orange
}
# Draw landmarks with feature-specific colors - made more visible
for feature, points_list in FACIAL_LANDMARKS.items():
color = colors.get(feature.split('_')[0], (0, 255, 0))
for point_idx in points_list:
pos = (int(landmarks.landmark[point_idx].x * w),
int(landmarks.landmark[point_idx].y * h))
# Larger circles with white outline for visibility
cv2.circle(viz_image, pos, 4, (255, 255, 255), -1) # White background
cv2.circle(viz_image, pos, 3, color, -1) # Colored center
# Add emotion text
detected_emotions = [emotion for emotion, is_present in emotions.items() if is_present]
emotion_text = " + ".join(detected_emotions) if detected_emotions else "Neutral"
# Create detailed analysis text
analysis = f"Expression: {emotion_text}\n\nActive Action Units:\n"
au_descriptions = {
'AU01': 'Inner Brow Raiser',
'AU02': 'Outer Brow Raiser',
'AU04': 'Brow Lowerer',
'AU05': 'Upper Lid Raiser',
'AU12': 'Lip Corner Puller (Smile)',
'AU25': 'Lips Part',
'AU26': 'Jaw Drop'
}
active_aus = [f"{au}" for au, active in aus.items() if active]
aus_text = "_".join(active_aus) if active_aus else "NoAUs"
# Create filename with timestamp, emotion, and AUs
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
download_filename = f"FER_{timestamp}_{emotion_text.replace(' + ', '_')}_{aus_text}.jpg"
# Add text with black background
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.7
thickness = 2
y_pos = 30
for line in emotion_text.split('\n'):
(text_w, text_h), _ = cv2.getTextSize(line, font, font_scale, thickness)
cv2.rectangle(viz_image, (10, y_pos - text_h - 5), (text_w + 20, y_pos + 5), (0, 0, 0), -1)
cv2.putText(viz_image, line, (15, y_pos), font, font_scale, (255, 255, 255), thickness)
y_pos += text_h + 20
return viz_image, analysis, download_filename
def save_original_image(image, filename):
if image is None or filename is None:
return None
return image
# Create Gradio interface
with gr.Blocks(css="app.css") as demo:
# Header with Observatory logo
with gr.Row(elem_classes="header-container"):
with gr.Column():
gr.Image("images/LMLOBS.png", show_label=False, container=False, elem_classes="header-logo")
gr.Markdown("# Facial Expression Recognition")
gr.Markdown("### LittleMonkeyLab | Goldsmiths Observatory")
with gr.Row():
with gr.Column():
input_image = gr.Image(label="Upload Image", type="numpy")
download_button = gr.Button("Download Original Image with Expression", visible=False)
gr.Markdown("""
### Instructions:
1. Upload a clear facial image
2. View the detected expression and Action Units (AUs)
3. Colored dots show key facial features:
- Green: Eyebrows
- Yellow: Eyes
- Cyan: Nose
- Magenta: Mouth
- Orange: Jaw
4. Click 'Download' to save the original image
""")
with gr.Column():
output_image = gr.Image(label="Analysis")
analysis_text = gr.Textbox(label="Expression Analysis", lines=8)
download_output = gr.File(label="Download", visible=False)
# Footer
with gr.Row(elem_classes="center-content"):
with gr.Column():
gr.Image("images/LMLLOGO.png", show_label=False, container=False, elem_classes="footer-logo")
gr.Markdown("© LittleMonkeyLab | Goldsmiths Observatory", elem_classes="footer-text")
# Set up the event handlers
filename = gr.State()
def update_interface(image):
viz_image, analysis, download_name = analyze_expression(image)
download_button.visible = True if image is not None else False
return viz_image, analysis, download_name
input_image.change(
fn=update_interface,
inputs=[input_image],
outputs=[output_image, analysis_text, filename]
)
download_button.click(
fn=save_original_image,
inputs=[input_image, filename],
outputs=download_output
)
if __name__ == "__main__":
demo.launch()
|