Johnnyyyyy56 commited on
Commit
44c8277
·
verified ·
1 Parent(s): 0f284bb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -154
app.py CHANGED
@@ -21,25 +21,22 @@ logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
  EMOTION_MAP = {
24
- "angry": {"emoji": "😠", "folder": "angry"},
25
- "disgust": {"emoji": "🤢", "folder": "disgust"},
26
- "fear": {"emoji": "😨", "folder": "fear"},
27
- "happy": {"emoji": "😄", "folder": "happy"},
28
- "sad": {"emoji": "😢", "folder": "sad"},
29
  "surprise": {"emoji": "😲", "folder": "surprise"},
30
- "neutral": {"emoji": "😐", "folder": "neutral"}
31
  }
32
 
33
- BACKENDS = ['mtcnn', 'opencv', 'ssd', 'dlib']
34
-
35
  BASE_SAVE_DIR = "/tmp/emotion_dataset"
36
  Path(BASE_SAVE_DIR).mkdir(parents=True, exist_ok=True)
37
 
38
- # Create sub-folders per emotion
39
  for emotion in EMOTION_MAP.values():
40
  Path(BASE_SAVE_DIR, emotion["folder"]).mkdir(exist_ok=True)
41
 
42
- # CSV log file (batch-no, timestamp, emotion, confidence, image_path)
43
  LOG_CSV = Path(BASE_SAVE_DIR) / "emotion_logs.csv"
44
  if not LOG_CSV.exists():
45
  with open(LOG_CSV, "w", newline="", encoding="utf-8") as f:
@@ -51,24 +48,12 @@ if not LOG_CSV.exists():
51
  # =========
52
 
53
  def write_log(batch_no: str, timestamp_iso: str, emotion: str, confidence: float, image_path: str):
54
- """Append a row to the CSV log."""
55
  try:
56
  with open(LOG_CSV, "a", newline="", encoding="utf-8") as f:
57
  csv.writer(f).writerow([batch_no, timestamp_iso, emotion, f"{confidence:.2f}", image_path])
58
  except Exception as e:
59
  logger.warning(f"Could not write to CSV log: {e}")
60
 
61
-
62
- def clean_old_files(directory: str, max_files_per_emotion: int = 50):
63
- for emotion in EMOTION_MAP.values():
64
- files = sorted(Path(directory, emotion["folder"]).glob("*.jpg"), key=os.path.getmtime)
65
- while len(files) > max_files_per_emotion:
66
- try:
67
- files.pop(0).unlink()
68
- except Exception as ex:
69
- logger.warning(f"Failed removing old file: {ex}")
70
-
71
-
72
  def enhance_image(frame: np.ndarray) -> np.ndarray:
73
  try:
74
  gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
@@ -77,7 +62,6 @@ def enhance_image(frame: np.ndarray) -> np.ndarray:
77
  except Exception:
78
  return frame
79
 
80
-
81
  def save_face_image(face_region: np.ndarray, emotion: str, confidence: float) -> str:
82
  emotion_data = EMOTION_MAP.get(emotion, EMOTION_MAP["neutral"])
83
  timestamp = int(time.time())
@@ -91,42 +75,40 @@ def save_face_image(face_region: np.ndarray, emotion: str, confidence: float) ->
91
  # =====================
92
 
93
  last_process_time = 0
 
94
 
95
  def predict_emotion(batch_no: str, input_data):
96
- """Gradio callback: returns processed frame + text summary."""
97
- global last_process_time
98
-
99
- # throttle to ~3fps
100
  now = time.time()
101
  if now - last_process_time < 0.33:
102
- placeholder = input_data if isinstance(input_data, np.ndarray) else None
103
- return placeholder, "⌛ Processing..."
104
  last_process_time = now
105
 
106
- # validate batch number
107
  batch_no = batch_no.strip() or "UNKNOWN"
108
 
109
  if input_data is None:
110
  return None, "No image input. Check webcam or file upload."
111
 
112
- # load / convert frame
113
- if isinstance(input_data, str): # uploaded file path
114
  frame = cv2.imread(input_data)
115
  if frame is None:
116
  return None, "Error reading uploaded image file."
117
  frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
118
- else: # numpy array from webcam
119
  frame = input_data.copy()
120
  if frame.ndim == 3 and frame.shape[2] == 4:
121
  frame = cv2.cvtColor(frame, cv2.COLOR_RGBA2RGB)
122
 
123
  frame = enhance_image(frame)
124
 
125
- # face & emotion detection (try multiple backends)
126
  results = None
127
  for backend in BACKENDS:
128
  try:
129
- results = DeepFace.analyze(frame, actions=['emotion'], detector_backend=backend, enforce_detection=False, silent=True)
 
 
 
130
  break
131
  except Exception:
132
  continue
@@ -137,17 +119,11 @@ def predict_emotion(batch_no: str, input_data):
137
  emotion_summary = ""
138
  timestamp_iso = time.strftime("%Y-%m-%dT%H:%M:%S")
139
 
140
- if isinstance(results, list):
141
- iterable = results
142
- else:
143
- iterable = [results]
144
-
145
- for res in iterable:
146
  (x, y, w, h) = (res['region'][k] for k in ('x', 'y', 'w', 'h'))
147
  dom_emotion = res['dominant_emotion']
148
  conf = res['emotion'][dom_emotion]
149
 
150
- # crop face with padding
151
  pad = 20
152
  x1, y1 = max(0, x-pad), max(0, y-pad)
153
  x2, y2 = min(frame.shape[1], x+w+pad), min(frame.shape[0], y+h+pad)
@@ -155,145 +131,122 @@ def predict_emotion(batch_no: str, input_data):
155
  if face_region.size == 0:
156
  continue
157
 
158
- # save face & log row
159
  img_path = save_face_image(face_region, dom_emotion, conf)
160
  write_log(batch_no, timestamp_iso, dom_emotion, conf, img_path)
161
 
162
- # draw overlays
163
  cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
164
  emoji = EMOTION_MAP[dom_emotion]['emoji']
165
- cv2.putText(frame, f"{dom_emotion} {emoji}", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
 
166
 
167
  emotion_summary += f"Batch {batch_no} => {dom_emotion.title()} {emoji}: {conf:.1f}%\n"
168
 
169
- clean_old_files(BASE_SAVE_DIR)
170
  emotion_summary = emotion_summary.strip() or "No emotion data."
171
  return frame, emotion_summary
172
 
173
- # ==========
174
- # Zip helpers
175
- # ==========
176
-
177
- def get_saved_faces():
178
- return [str(p) for p in Path(BASE_SAVE_DIR).rglob("*.jpg")]
179
-
180
-
181
- def create_zip_file(face_files, zip_name: str) -> str | None:
182
- if not face_files:
183
- return None
184
- zip_path = Path(BASE_SAVE_DIR) / zip_name
185
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
186
- for f in face_files:
187
- zf.write(f, Path(f).relative_to(BASE_SAVE_DIR))
188
- return str(zip_path)
189
-
190
  # ========================
191
  # Gradio Interface Building
192
  # ========================
193
 
194
  def build_interface():
195
- with gr.Blocks(title="Emotion Detection & Logging", css=".gradio-container {max-width: 900px !important}") as demo:
 
 
 
 
196
  gr.Markdown("""
197
- # Real-Time Emotion Detection with Batch Logging
198
- 1. **Scan/enter employee batch number**
199
- 2. **Capture face via webcam** or **upload an image**
200
- 3. Faces are **saved per emotion** and every detection is **logged** (CSV) with batch number, timestamp & confidence
201
  """)
202
 
203
- with gr.Tabs() as tabs:
204
- # ------------- Live Camera Tab -------------
205
- with gr.Tab("🎥 Live Camera", id="live_cam_tab") as live_tab:
206
- batch_live = gr.Textbox(label="Scan/Enter Batch No", placeholder="e.g. E123")
207
- webcam = gr.Image(streaming=True, label="Live Camera", mirror_webcam=True)
208
- cam_out = gr.Image(label="Processed Output")
209
- cam_txt = gr.Textbox(label="Detection Log")
210
- webcam.change(predict_emotion, inputs=[batch_live, webcam], outputs=[cam_out, cam_txt])
211
-
212
- # ------------- Upload Image Tab -------------
213
- with gr.Tab("📁 Upload Image", id="upload_tab"):
214
- batch_up = gr.Textbox(label="Scan/Enter Batch No", placeholder="e.g. E456")
215
- uploader = gr.Image(type="filepath", label="Upload Image")
216
- up_out = gr.Image(label="Processed Output")
217
- up_txt = gr.Textbox(label="Detection Log")
218
- uploader.change(predict_emotion, inputs=[batch_up, uploader], outputs=[up_out, up_txt])
219
-
220
- # ------------- Dataset / Logs Tab -------------
221
- with gr.Tab("💾 Dataset & Logs", id="data_tab"):
222
- gr.Markdown("### Browse Saved Faces & Download Logs")
223
- emotion_select = gr.Dropdown(list(EMOTION_MAP.keys()), value="happy", label="Select Emotion")
224
- emo_gallery = gr.Gallery(label="Faces for selected emotion", columns=4)
225
- all_gallery = gr.Gallery(label="All Faces", columns=4)
226
-
227
- def refresh_galleries(selected_emotion):
228
- emo_dir = Path(BASE_SAVE_DIR) / EMOTION_MAP[selected_emotion]['folder']
229
- emo_files = [str(p) for p in emo_dir.glob("*.jpg")]
230
- return emo_files, get_saved_faces()
231
-
232
- emotion_select.change(refresh_galleries, inputs=emotion_select, outputs=[emo_gallery, all_gallery])
233
- refresh_btn = gr.Button("🔄 Refresh")
234
- refresh_btn.click(refresh_galleries, inputs=emotion_select, outputs=[emo_gallery, all_gallery])
235
-
236
- # Download buttons
237
- with gr.Row():
238
- download_emo_btn = gr.Button("📦 Download selected emotion faces")
239
- download_all_btn = gr.Button("📦 Download all faces")
240
- download_logs_btn = gr.Button("📔 Download CSV logs")
241
-
242
- out_emo_zip = gr.File(label="Download", visible=False)
243
- out_all_zip = gr.File(label="Download", visible=False)
244
- out_logs = gr.File(label="CSV", visible=False)
245
-
246
- def dl_emo(selected_emotion):
247
- emo_dir = Path(BASE_SAVE_DIR) / EMOTION_MAP[selected_emotion]['folder']
248
- zip_path = create_zip_file([str(p) for p in emo_dir.glob("*.jpg")], f"{selected_emotion}_faces.zip")
249
- return gr.File(value=zip_path, visible=bool(zip_path))
250
-
251
- def dl_all():
252
- return gr.File(value=create_zip_file(get_saved_faces(), "all_faces.zip"), visible=True)
253
-
254
- def dl_csv():
255
- return gr.File(value=str(LOG_CSV), visible=True)
256
-
257
- download_emo_btn.click(dl_emo, inputs=emotion_select, outputs=out_emo_zip)
258
- download_all_btn.click(dl_all, outputs=out_all_zip)
259
- download_logs_btn.click(dl_csv, outputs=out_logs)
260
-
261
- # Clear all data
262
- clear_btn = gr.Button("🗑️ Clear ALL faces & logs", variant="stop")
263
- def clear_everything():
264
- for f in get_saved_faces():
265
- try:
266
- Path(f).unlink()
267
- except:
268
- pass
269
- try:
270
- LOG_CSV.unlink()
271
- except:
272
- pass
273
- return [], [], gr.File(visible=False), gr.File(visible=False)
274
- clear_btn.click(clear_everything, outputs=[emo_gallery, all_gallery, out_emo_zip, out_all_zip])
275
-
276
- # Auto-switch to webcam after batch number is entered
277
- batch_live.change(
278
- fn=None,
279
  js="""
280
- (batchValue) => {
281
- if (batchValue && batchValue.trim() !== '') {
282
- setTimeout(() => {
283
- document.querySelectorAll('.tab-nav button')[0].click();
284
- }, 5000);
 
 
 
 
 
 
 
285
  }
286
- return [];
287
  }
 
288
  """
289
  )
290
 
291
- # Initial gallery load
292
- demo.load(lambda sel="happy": refresh_galleries(sel), outputs=[emo_gallery, all_gallery])
293
-
294
  return demo
295
 
296
-
297
  demo = build_interface()
298
 
299
  if __name__ == "__main__":
 
21
  logger = logging.getLogger(__name__)
22
 
23
  EMOTION_MAP = {
24
+ "angry": {"emoji": "😠", "folder": "angry"},
25
+ "disgust": {"emoji": "🤢", "folder": "disgust"},
26
+ "fear": {"emoji": "😨", "folder": "fear"},
27
+ "happy": {"emoji": "😄", "folder": "happy"},
28
+ "sad": {"emoji": "😢", "folder": "sad"},
29
  "surprise": {"emoji": "😲", "folder": "surprise"},
30
+ "neutral": {"emoji": "😐", "folder": "neutral"}
31
  }
32
 
33
+ BACKENDS = ['opencv', 'mtcnn', 'ssd', 'dlib']
 
34
  BASE_SAVE_DIR = "/tmp/emotion_dataset"
35
  Path(BASE_SAVE_DIR).mkdir(parents=True, exist_ok=True)
36
 
 
37
  for emotion in EMOTION_MAP.values():
38
  Path(BASE_SAVE_DIR, emotion["folder"]).mkdir(exist_ok=True)
39
 
 
40
  LOG_CSV = Path(BASE_SAVE_DIR) / "emotion_logs.csv"
41
  if not LOG_CSV.exists():
42
  with open(LOG_CSV, "w", newline="", encoding="utf-8") as f:
 
48
  # =========
49
 
50
  def write_log(batch_no: str, timestamp_iso: str, emotion: str, confidence: float, image_path: str):
 
51
  try:
52
  with open(LOG_CSV, "a", newline="", encoding="utf-8") as f:
53
  csv.writer(f).writerow([batch_no, timestamp_iso, emotion, f"{confidence:.2f}", image_path])
54
  except Exception as e:
55
  logger.warning(f"Could not write to CSV log: {e}")
56
 
 
 
 
 
 
 
 
 
 
 
 
57
  def enhance_image(frame: np.ndarray) -> np.ndarray:
58
  try:
59
  gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
 
62
  except Exception:
63
  return frame
64
 
 
65
  def save_face_image(face_region: np.ndarray, emotion: str, confidence: float) -> str:
66
  emotion_data = EMOTION_MAP.get(emotion, EMOTION_MAP["neutral"])
67
  timestamp = int(time.time())
 
75
  # =====================
76
 
77
  last_process_time = 0
78
+ capture_requested = False
79
 
80
  def predict_emotion(batch_no: str, input_data):
81
+ global last_process_time, capture_requested
82
+
 
 
83
  now = time.time()
84
  if now - last_process_time < 0.33:
85
+ return input_data if isinstance(input_data, np.ndarray) else None, "⌛ Processing..."
 
86
  last_process_time = now
87
 
 
88
  batch_no = batch_no.strip() or "UNKNOWN"
89
 
90
  if input_data is None:
91
  return None, "No image input. Check webcam or file upload."
92
 
93
+ if isinstance(input_data, str):
 
94
  frame = cv2.imread(input_data)
95
  if frame is None:
96
  return None, "Error reading uploaded image file."
97
  frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
98
+ else:
99
  frame = input_data.copy()
100
  if frame.ndim == 3 and frame.shape[2] == 4:
101
  frame = cv2.cvtColor(frame, cv2.COLOR_RGBA2RGB)
102
 
103
  frame = enhance_image(frame)
104
 
 
105
  results = None
106
  for backend in BACKENDS:
107
  try:
108
+ results = DeepFace.analyze(frame, actions=['emotion'],
109
+ detector_backend=backend,
110
+ enforce_detection=False,
111
+ silent=True)
112
  break
113
  except Exception:
114
  continue
 
119
  emotion_summary = ""
120
  timestamp_iso = time.strftime("%Y-%m-%dT%H:%M:%S")
121
 
122
+ for res in results if isinstance(results, list) else [results]:
 
 
 
 
 
123
  (x, y, w, h) = (res['region'][k] for k in ('x', 'y', 'w', 'h'))
124
  dom_emotion = res['dominant_emotion']
125
  conf = res['emotion'][dom_emotion]
126
 
 
127
  pad = 20
128
  x1, y1 = max(0, x-pad), max(0, y-pad)
129
  x2, y2 = min(frame.shape[1], x+w+pad), min(frame.shape[0], y+h+pad)
 
131
  if face_region.size == 0:
132
  continue
133
 
 
134
  img_path = save_face_image(face_region, dom_emotion, conf)
135
  write_log(batch_no, timestamp_iso, dom_emotion, conf, img_path)
136
 
 
137
  cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
138
  emoji = EMOTION_MAP[dom_emotion]['emoji']
139
+ cv2.putText(frame, f"{dom_emotion} {emoji}", (x, y-10),
140
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
141
 
142
  emotion_summary += f"Batch {batch_no} => {dom_emotion.title()} {emoji}: {conf:.1f}%\n"
143
 
 
144
  emotion_summary = emotion_summary.strip() or "No emotion data."
145
  return frame, emotion_summary
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  # ========================
148
  # Gradio Interface Building
149
  # ========================
150
 
151
  def build_interface():
152
+ with gr.Blocks(title="Automated Emotion Detection", css="""
153
+ .gradio-container {max-width: 900px !important}
154
+ .capture-btn {background: #ff4d4d !important; color: white !important}
155
+ """) as demo:
156
+
157
  gr.Markdown("""
158
+ # Automated Emotion Detection System
159
+ 1. **Scan your batch number**
160
+ 2. **System will automatically capture your face after 5 seconds**
161
+ 3. **Emotion analysis results will appear below**
162
  """)
163
 
164
+ batch_no = gr.Textbox(label="Scan/Enter Batch Number",
165
+ placeholder="e.g. EMP123",
166
+ elem_classes=["batch-input"])
167
+
168
+ with gr.Row():
169
+ webcam = gr.Image(streaming=True, label="Live Camera Feed",
170
+ mirror_webcam=True, visible=False)
171
+ output_image = gr.Image(label="Processed Result", visible=False)
172
+
173
+ result_text = gr.Textbox(label="Emotion Analysis Result", visible=False)
174
+
175
+ # Hidden components for control flow
176
+ capture_trigger = gr.Button(visible=False)
177
+ batch_state = gr.State("UNKNOWN")
178
+
179
+ def toggle_components(batch_value):
180
+ if batch_value.strip():
181
+ return [
182
+ gr.Image(visible=True), # webcam
183
+ gr.Image(visible=False), # output_image
184
+ gr.Textbox(visible=False), # result_text
185
+ True # trigger capture
186
+ ]
187
+ return [gr.Image(visible=False), gr.Image(visible=False),
188
+ gr.Textbox(visible=False), False]
189
+
190
+ def capture_and_analyze(batch_value, img):
191
+ if img is None:
192
+ return None, None, "No face detected. Please try again."
193
+
194
+ frame = img.copy()
195
+ if frame.ndim == 3 and frame.shape[2] == 4:
196
+ frame = cv2.cvtColor(frame, cv2.COLOR_RGBA2RGB)
197
+
198
+ processed_frame, emotion_text = predict_emotion(batch_value, frame)
199
+ return processed_frame, processed_frame, emotion_text
200
+
201
+ # When batch number is entered
202
+ batch_no.change(
203
+ toggle_components,
204
+ inputs=[batch_no],
205
+ outputs=[webcam, output_image, result_text, capture_trigger]
206
+ ).then(
207
+ lambda: time.sleep(5), # Wait 5 seconds
208
+ None,
209
+ None,
210
+ queue=False
211
+ ).then(
212
+ lambda: gr.Button(click=True), # Trigger capture
213
+ None,
214
+ capture_trigger,
215
+ queue=False
216
+ )
217
+
218
+ # When capture is triggered
219
+ capture_trigger.click(
220
+ capture_and_analyze,
221
+ inputs=[batch_no, webcam],
222
+ outputs=[webcam, output_image, result_text]
223
+ )
224
+
225
+ demo.load(
226
+ None,
227
+ None,
228
+ None,
 
 
 
 
 
 
 
 
 
 
 
229
  js="""
230
+ function focusBatchInput() {
231
+ const batchInput = document.querySelector('.batch-input input');
232
+ if (batchInput) {
233
+ batchInput.focus();
234
+ batchInput.addEventListener('input', function(e) {
235
+ if (e.target.value.trim() !== '') {
236
+ setTimeout(() => {
237
+ const webcam = document.querySelector('[aria-label="Live Camera Feed"]');
238
+ if (webcam) webcam.click();
239
+ }, 5000);
240
+ }
241
+ });
242
  }
 
243
  }
244
+ document.addEventListener('DOMContentLoaded', focusBatchInput);
245
  """
246
  )
247
 
 
 
 
248
  return demo
249
 
 
250
  demo = build_interface()
251
 
252
  if __name__ == "__main__":