dreamlessx commited on
Commit
0332541
·
1 Parent(s): 433e26f

Add example images, procedure details, status indicators, usage tracking

Browse files

- Add 3 synthetic example face images with gr.Examples on all tabs
- Add dynamic procedure description that updates on selection
- Add animated Processing/Done/Error status indicators with CSS
- Add UsageTracker class for simple JSON-based analytics
- Add processing time to info output
- Improve footer with version, tech stack, and citation link
- Bump version to v0.2.2

README.md CHANGED
@@ -52,4 +52,4 @@ GPU modes (ControlNet, img2img) with photorealistic rendering are available in t
52
  - [Wiki](https://github.com/dreamlessx/LandmarkDiff-public/wiki)
53
  - [Discussions](https://github.com/dreamlessx/LandmarkDiff-public/discussions)
54
 
55
- **Version:** v0.2.1
 
52
  - [Wiki](https://github.com/dreamlessx/LandmarkDiff-public/wiki)
53
  - [Discussions](https://github.com/dreamlessx/LandmarkDiff-public/discussions)
54
 
55
+ **Version:** v0.2.2
app.py CHANGED
@@ -2,8 +2,14 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import logging
 
 
 
6
  import traceback
 
 
7
 
8
  import cv2
9
  import gradio as gr
@@ -17,7 +23,7 @@ from landmarkdiff.masking import generate_surgical_mask
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger(__name__)
19
 
20
- VERSION = "v0.2.1"
21
 
22
  GITHUB_URL = "https://github.com/dreamlessx/LandmarkDiff-public"
23
  DOCS_URL = f"{GITHUB_URL}/tree/main/docs"
@@ -33,6 +39,120 @@ PROCEDURE_DESCRIPTIONS = {
33
  "mentoplasty": "Chin surgery -- adjusts chin projection and vertical height",
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  def warp_image_tps(image, src_pts, dst_pts):
38
  """Thin-plate spline warp (CPU only)."""
@@ -56,11 +176,20 @@ def _error_result(msg):
56
  return blank, blank, blank, blank, msg
57
 
58
 
 
 
 
 
 
59
  def process_image(image_rgb, procedure, intensity):
60
  """Process a single image through the TPS pipeline."""
 
 
61
  if image_rgb is None:
62
  return _error_result("Upload a face photo to begin.")
63
 
 
 
64
  try:
65
  image_bgr = cv2.cvtColor(np.asarray(image_rgb, dtype=np.uint8), cv2.COLOR_RGB2BGR)
66
  image_bgr = cv2.resize(image_bgr, (512, 512))
@@ -103,12 +232,15 @@ def process_image(image_rgb, procedure, intensity):
103
  np.linalg.norm(manipulated.pixel_coords - face.pixel_coords, axis=1)
104
  )
105
 
 
 
106
  info = (
107
  f"Procedure: {procedure}\n"
108
  f"Intensity: {intensity:.0f}%\n"
109
  f"Landmarks: {len(face.landmarks)}\n"
110
  f"Avg displacement: {displacement:.1f} px\n"
111
  f"Confidence: {face.confidence:.2f}\n"
 
112
  f"Mode: TPS (CPU)"
113
  )
114
  return wireframe_rgb, mask_vis, composited_rgb, side_by_side, info
@@ -120,6 +252,8 @@ def process_image(image_rgb, procedure, intensity):
120
 
121
  def compare_procedures(image_rgb, intensity):
122
  """Compare all procedures at the same intensity."""
 
 
123
  if image_rgb is None:
124
  blank = np.zeros((512, 512, 3), dtype=np.uint8)
125
  return [blank] * len(PROCEDURES)
@@ -150,6 +284,8 @@ def compare_procedures(image_rgb, intensity):
150
 
151
  def intensity_sweep(image_rgb, procedure):
152
  """Generate intensity sweep from 0 to 100."""
 
 
153
  if image_rgb is None:
154
  return []
155
 
@@ -179,6 +315,10 @@ def intensity_sweep(image_rgb, procedure):
179
  return []
180
 
181
 
 
 
 
 
182
  # -- Build the procedure table for the description --
183
  _proc_rows = "\n".join(
184
  f"| **{name.replace('_', ' ').title()}** | {desc} |"
@@ -228,22 +368,74 @@ pipeline for photorealistic rendering, followed by CodeFormer + Real-ESRGAN post
228
 
229
  FOOTER_MD = f"""
230
  ---
231
- <p style="text-align:center; color:#888; font-size:0.85em;">
232
- LandmarkDiff {VERSION} &middot;
233
- <a href="{GITHUB_URL}">GitHub</a> &middot;
234
- <a href="{WIKI_URL}">Wiki</a> &middot;
235
- <a href="{DISCUSSIONS_URL}">Discussions</a> &middot;
236
- MIT License
237
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  """
239
 
240
 
241
  with gr.Blocks(
242
  title="LandmarkDiff - Surgical Outcome Prediction",
243
  theme=gr.themes.Soft(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  ) as demo:
245
  gr.Markdown(HEADER_MD)
246
 
 
247
  with gr.Tab("Single Procedure"):
248
  with gr.Row():
249
  with gr.Column(scale=1):
@@ -253,6 +445,10 @@ with gr.Blocks(
253
  value="rhinoplasty",
254
  label="Surgical Procedure",
255
  )
 
 
 
 
256
  intensity = gr.Slider(
257
  minimum=0,
258
  maximum=100,
@@ -262,7 +458,11 @@ with gr.Blocks(
262
  info="0 = no change, 100 = maximum effect",
263
  )
264
  run_btn = gr.Button("Generate Preview", variant="primary", size="lg")
265
- info_box = gr.Textbox(label="Info", lines=6, interactive=False)
 
 
 
 
266
 
267
  with gr.Column(scale=2):
268
  with gr.Row():
@@ -272,18 +472,59 @@ with gr.Blocks(
272
  out_result = gr.Image(label="Predicted Result", height=256)
273
  out_sidebyside = gr.Image(label="Before / After", height=256)
274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  run_btn.click(
276
- fn=process_image,
 
 
 
 
277
  inputs=[input_image, procedure, intensity],
278
- outputs=[out_wireframe, out_mask, out_result, out_sidebyside, info_box],
279
  )
 
 
280
  for trigger in [input_image, procedure, intensity]:
281
  trigger.change(
282
- fn=process_image,
 
 
 
 
283
  inputs=[input_image, procedure, intensity],
284
- outputs=[out_wireframe, out_mask, out_result, out_sidebyside, info_box],
285
  )
286
 
 
287
  with gr.Tab("Compare Procedures"):
288
  gr.Markdown("Compare all six procedures side by side at the same intensity.")
289
  with gr.Row():
@@ -291,6 +532,9 @@ with gr.Blocks(
291
  cmp_image = gr.Image(label="Upload Face Photo", type="numpy", height=300)
292
  cmp_intensity = gr.Slider(0, 100, 50, step=1, label="Intensity (%)")
293
  cmp_btn = gr.Button("Compare All", variant="primary", size="lg")
 
 
 
294
  with gr.Column(scale=2):
295
  cmp_outputs = []
296
  rows_needed = (len(PROCEDURES) + 2) // 3
@@ -306,12 +550,29 @@ with gr.Blocks(
306
  )
307
  )
308
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  cmp_btn.click(
310
- fn=compare_procedures,
 
 
 
 
311
  inputs=[cmp_image, cmp_intensity],
312
- outputs=cmp_outputs,
313
  )
314
 
 
315
  with gr.Tab("Intensity Sweep"):
316
  gr.Markdown(
317
  "See how a procedure looks across intensity levels (0% through 100% in 20% steps)."
@@ -325,15 +586,38 @@ with gr.Blocks(
325
  label="Procedure",
326
  )
327
  sweep_btn = gr.Button("Generate Sweep", variant="primary", size="lg")
 
 
 
328
  with gr.Column(scale=2):
329
  sweep_gallery = gr.Gallery(
330
  label="Intensity Sweep (0% - 100%)", columns=3, height=400
331
  )
332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  sweep_btn.click(
334
- fn=intensity_sweep,
 
 
 
 
335
  inputs=[sweep_image, sweep_procedure],
336
- outputs=[sweep_gallery],
337
  )
338
 
339
  gr.Markdown(FOOTER_MD)
 
2
 
3
  from __future__ import annotations
4
 
5
+ import json
6
  import logging
7
+ import os
8
+ import threading
9
+ import time
10
  import traceback
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
 
14
  import cv2
15
  import gradio as gr
 
23
  logging.basicConfig(level=logging.INFO)
24
  logger = logging.getLogger(__name__)
25
 
26
+ VERSION = "v0.2.2"
27
 
28
  GITHUB_URL = "https://github.com/dreamlessx/LandmarkDiff-public"
29
  DOCS_URL = f"{GITHUB_URL}/tree/main/docs"
 
39
  "mentoplasty": "Chin surgery -- adjusts chin projection and vertical height",
40
  }
41
 
42
+ # -- Detailed procedure info shown when user selects a procedure --
43
+ PROCEDURE_DETAILS = {
44
+ "rhinoplasty": (
45
+ "**Rhinoplasty** (nose reshaping)\n\n"
46
+ "Modifies the nasal bridge height, tip projection, tip rotation, and alar (nostril) "
47
+ "width. The landmark displacement targets the nose dorsum, tip, columella, and alar "
48
+ "base regions. At low intensity (10-30%) the effect is subtle refinement; at high "
49
+ "intensity (70-100%) the reshaping is more dramatic.\n\n"
50
+ "Affected landmarks: nasal bridge, tip, alar base, columella"
51
+ ),
52
+ "blepharoplasty": (
53
+ "**Blepharoplasty** (eyelid surgery)\n\n"
54
+ "Adjusts upper and lower eyelid position and canthal tilt. Targets the periorbital "
55
+ "region including upper lid crease, lower lid margin, and lateral/medial canthi. "
56
+ "Simulates both upper blepharoplasty (lid ptosis correction) and lower blepharoplasty "
57
+ "(under-eye bag removal).\n\n"
58
+ "Affected landmarks: upper/lower eyelid margins, canthi, periorbital region"
59
+ ),
60
+ "rhytidectomy": (
61
+ "**Rhytidectomy** (facelift)\n\n"
62
+ "Tightens the midface and jawline by displacing landmarks along vectors that simulate "
63
+ "SMAS lift and skin redraping. Affects the cheek, jowl, and submental regions. The "
64
+ "effect tightens nasolabial folds and redefines the jawline contour.\n\n"
65
+ "Affected landmarks: cheek, jowl, jawline, submental region"
66
+ ),
67
+ "orthognathic": (
68
+ "**Orthognathic surgery** (jaw repositioning)\n\n"
69
+ "Simulates maxillary and mandibular osteotomy outcomes by repositioning the skeletal "
70
+ "framework. Affects jaw position, chin projection, and overall facial proportion. "
71
+ "Used for correcting class II/III malocclusion and facial asymmetry.\n\n"
72
+ "Affected landmarks: maxilla, mandible, chin, lower face contour"
73
+ ),
74
+ "brow_lift": (
75
+ "**Brow lift** (forehead rejuvenation)\n\n"
76
+ "Elevates brow position and reduces forehead ptosis. Targets the eyebrow arch, "
77
+ "lateral brow tail, and glabellar region. Simulates both endoscopic and coronal "
78
+ "brow lift approaches. Higher intensities produce more visible brow elevation.\n\n"
79
+ "Affected landmarks: brow arch, lateral brow, glabella, upper forehead"
80
+ ),
81
+ "mentoplasty": (
82
+ "**Mentoplasty** (chin surgery)\n\n"
83
+ "Adjusts chin projection (anteroposterior position) and vertical height. Simulates "
84
+ "both augmentation (advancement) and reduction genioplasty. Affects the pogonion, "
85
+ "menton, and lower border of the mandible.\n\n"
86
+ "Affected landmarks: chin point, lower mandibular border, mentolabial fold"
87
+ ),
88
+ }
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Usage analytics -- simple thread-safe counter persisted to disk
93
+ # ---------------------------------------------------------------------------
94
+ class UsageTracker:
95
+ """Track demo usage counts to a JSON file (thread-safe)."""
96
+
97
+ def __init__(self, path: str = "usage_stats.json"):
98
+ self._path = Path(path)
99
+ self._lock = threading.Lock()
100
+ self._stats: dict = self._load()
101
+
102
+ def _load(self) -> dict:
103
+ if self._path.exists():
104
+ try:
105
+ return json.loads(self._path.read_text())
106
+ except (json.JSONDecodeError, OSError):
107
+ pass
108
+ return {
109
+ "total_runs": 0,
110
+ "procedures": {},
111
+ "tabs": {},
112
+ "first_run": None,
113
+ "last_run": None,
114
+ }
115
+
116
+ def _save(self) -> None:
117
+ try:
118
+ self._path.write_text(json.dumps(self._stats, indent=2))
119
+ except OSError:
120
+ logger.warning("Could not persist usage stats")
121
+
122
+ def record(self, tab: str, procedure: str | None = None) -> None:
123
+ with self._lock:
124
+ now = datetime.now(timezone.utc).isoformat()
125
+ self._stats["total_runs"] = self._stats.get("total_runs", 0) + 1
126
+ if self._stats.get("first_run") is None:
127
+ self._stats["first_run"] = now
128
+ self._stats["last_run"] = now
129
+
130
+ tabs = self._stats.setdefault("tabs", {})
131
+ tabs[tab] = tabs.get(tab, 0) + 1
132
+
133
+ if procedure:
134
+ procs = self._stats.setdefault("procedures", {})
135
+ procs[procedure] = procs.get(procedure, 0) + 1
136
+
137
+ self._save()
138
+
139
+ @property
140
+ def total_runs(self) -> int:
141
+ return self._stats.get("total_runs", 0)
142
+
143
+ @property
144
+ def summary(self) -> str:
145
+ total = self._stats.get("total_runs", 0)
146
+ top_proc = ""
147
+ procs = self._stats.get("procedures", {})
148
+ if procs:
149
+ top = max(procs, key=procs.get)
150
+ top_proc = f" | Most popular: {top.replace('_', ' ').title()}"
151
+ return f"Total runs: {total}{top_proc}"
152
+
153
+
154
+ tracker = UsageTracker()
155
+
156
 
157
  def warp_image_tps(image, src_pts, dst_pts):
158
  """Thin-plate spline warp (CPU only)."""
 
176
  return blank, blank, blank, blank, msg
177
 
178
 
179
+ def _get_procedure_description(procedure: str) -> str:
180
+ """Return the detailed Markdown description for a procedure."""
181
+ return PROCEDURE_DETAILS.get(procedure, "Select a procedure to see details.")
182
+
183
+
184
  def process_image(image_rgb, procedure, intensity):
185
  """Process a single image through the TPS pipeline."""
186
+ tracker.record("single", procedure)
187
+
188
  if image_rgb is None:
189
  return _error_result("Upload a face photo to begin.")
190
 
191
+ t0 = time.monotonic()
192
+
193
  try:
194
  image_bgr = cv2.cvtColor(np.asarray(image_rgb, dtype=np.uint8), cv2.COLOR_RGB2BGR)
195
  image_bgr = cv2.resize(image_bgr, (512, 512))
 
232
  np.linalg.norm(manipulated.pixel_coords - face.pixel_coords, axis=1)
233
  )
234
 
235
+ elapsed = time.monotonic() - t0
236
+
237
  info = (
238
  f"Procedure: {procedure}\n"
239
  f"Intensity: {intensity:.0f}%\n"
240
  f"Landmarks: {len(face.landmarks)}\n"
241
  f"Avg displacement: {displacement:.1f} px\n"
242
  f"Confidence: {face.confidence:.2f}\n"
243
+ f"Processing time: {elapsed:.2f}s\n"
244
  f"Mode: TPS (CPU)"
245
  )
246
  return wireframe_rgb, mask_vis, composited_rgb, side_by_side, info
 
252
 
253
  def compare_procedures(image_rgb, intensity):
254
  """Compare all procedures at the same intensity."""
255
+ tracker.record("compare")
256
+
257
  if image_rgb is None:
258
  blank = np.zeros((512, 512, 3), dtype=np.uint8)
259
  return [blank] * len(PROCEDURES)
 
284
 
285
  def intensity_sweep(image_rgb, procedure):
286
  """Generate intensity sweep from 0 to 100."""
287
+ tracker.record("sweep", procedure)
288
+
289
  if image_rgb is None:
290
  return []
291
 
 
315
  return []
316
 
317
 
318
+ # -- Example images --
319
+ EXAMPLE_DIR = Path(__file__).parent / "examples"
320
+ EXAMPLE_IMAGES = sorted(EXAMPLE_DIR.glob("*.png")) if EXAMPLE_DIR.exists() else []
321
+
322
  # -- Build the procedure table for the description --
323
  _proc_rows = "\n".join(
324
  f"| **{name.replace('_', ' ').title()}** | {desc} |"
 
368
 
369
  FOOTER_MD = f"""
370
  ---
371
+ <div style="text-align:center; color:#888; font-size:0.85em; padding: 12px 0;">
372
+ <p>
373
+ <strong>LandmarkDiff</strong> {VERSION} &middot;
374
+ TPS warping on CPU &middot;
375
+ MediaPipe 478-point mesh &middot;
376
+ 6 surgical procedures
377
+ </p>
378
+ <p>
379
+ <a href="{GITHUB_URL}">GitHub</a> &middot;
380
+ <a href="{DOCS_URL}">Docs</a> &middot;
381
+ <a href="{WIKI_URL}">Wiki</a> &middot;
382
+ <a href="{DISCUSSIONS_URL}">Discussions</a> &middot;
383
+ MIT License
384
+ </p>
385
+ <p style="font-size:0.75em; color:#aaa;">
386
+ Built with Gradio &middot;
387
+ Powered by MediaPipe + OpenCV &middot;
388
+ <a href="{GITHUB_URL}/blob/main/CITATION.cff">Cite this work</a>
389
+ </p>
390
+ </div>
391
  """
392
 
393
 
394
  with gr.Blocks(
395
  title="LandmarkDiff - Surgical Outcome Prediction",
396
  theme=gr.themes.Soft(),
397
+ css="""
398
+ .status-processing {
399
+ background: linear-gradient(90deg, #e3f2fd 0%, #bbdefb 50%, #e3f2fd 100%);
400
+ background-size: 200% 100%;
401
+ animation: shimmer 2s infinite;
402
+ padding: 8px 16px;
403
+ border-radius: 6px;
404
+ text-align: center;
405
+ font-weight: 500;
406
+ }
407
+ @keyframes shimmer {
408
+ 0% { background-position: -200% 0; }
409
+ 100% { background-position: 200% 0; }
410
+ }
411
+ .status-ready {
412
+ background: #e8f5e9;
413
+ padding: 8px 16px;
414
+ border-radius: 6px;
415
+ text-align: center;
416
+ color: #2e7d32;
417
+ font-weight: 500;
418
+ }
419
+ .status-error {
420
+ background: #ffebee;
421
+ padding: 8px 16px;
422
+ border-radius: 6px;
423
+ text-align: center;
424
+ color: #c62828;
425
+ font-weight: 500;
426
+ }
427
+ .proc-detail-box {
428
+ background: #f5f5f5;
429
+ border-left: 3px solid #1976d2;
430
+ padding: 12px 16px;
431
+ border-radius: 4px;
432
+ margin-top: 8px;
433
+ }
434
+ """,
435
  ) as demo:
436
  gr.Markdown(HEADER_MD)
437
 
438
+ # -- Single Procedure tab --
439
  with gr.Tab("Single Procedure"):
440
  with gr.Row():
441
  with gr.Column(scale=1):
 
445
  value="rhinoplasty",
446
  label="Surgical Procedure",
447
  )
448
+ proc_detail = gr.Markdown(
449
+ value=_get_procedure_description("rhinoplasty"),
450
+ elem_classes=["proc-detail-box"],
451
+ )
452
  intensity = gr.Slider(
453
  minimum=0,
454
  maximum=100,
 
458
  info="0 = no change, 100 = maximum effect",
459
  )
460
  run_btn = gr.Button("Generate Preview", variant="primary", size="lg")
461
+ status_box = gr.HTML(
462
+ value='<div class="status-ready">Ready -- upload a photo or click an example below</div>',
463
+ label="Status",
464
+ )
465
+ info_box = gr.Textbox(label="Info", lines=7, interactive=False)
466
 
467
  with gr.Column(scale=2):
468
  with gr.Row():
 
472
  out_result = gr.Image(label="Predicted Result", height=256)
473
  out_sidebyside = gr.Image(label="Before / After", height=256)
474
 
475
+ # -- Example images --
476
+ if EXAMPLE_IMAGES:
477
+ gr.Markdown("### Try an Example")
478
+ gr.Examples(
479
+ examples=[[str(p)] for p in EXAMPLE_IMAGES],
480
+ inputs=[input_image],
481
+ label="Click an example face to load it (these are synthetic sketches "
482
+ "-- for best results, upload a real photo)",
483
+ )
484
+
485
+ # -- Procedure description update --
486
+ procedure.change(
487
+ fn=_get_procedure_description,
488
+ inputs=[procedure],
489
+ outputs=[proc_detail],
490
+ )
491
+
492
+ # -- Processing with status indicator --
493
+ def _process_with_status(image_rgb, proc, intens):
494
+ results = process_image(image_rgb, proc, intens)
495
+ # Last element is the info/error text
496
+ info_text = results[-1]
497
+ if "error" in info_text.lower() or "No face" in info_text:
498
+ status_html = f'<div class="status-error">{info_text.split(chr(10))[0]}</div>'
499
+ else:
500
+ status_html = '<div class="status-ready">Done -- result ready</div>'
501
+ return results + (status_html,)
502
+
503
+ all_outputs = [out_wireframe, out_mask, out_result, out_sidebyside, info_box, status_box]
504
+
505
  run_btn.click(
506
+ fn=lambda: '<div class="status-processing">Processing... extracting landmarks and warping</div>',
507
+ inputs=None,
508
+ outputs=[status_box],
509
+ ).then(
510
+ fn=_process_with_status,
511
  inputs=[input_image, procedure, intensity],
512
+ outputs=all_outputs,
513
  )
514
+
515
+ # Auto-trigger on input change (image upload, procedure change, intensity change)
516
  for trigger in [input_image, procedure, intensity]:
517
  trigger.change(
518
+ fn=lambda: '<div class="status-processing">Processing...</div>',
519
+ inputs=None,
520
+ outputs=[status_box],
521
+ ).then(
522
+ fn=_process_with_status,
523
  inputs=[input_image, procedure, intensity],
524
+ outputs=all_outputs,
525
  )
526
 
527
+ # -- Compare Procedures tab --
528
  with gr.Tab("Compare Procedures"):
529
  gr.Markdown("Compare all six procedures side by side at the same intensity.")
530
  with gr.Row():
 
532
  cmp_image = gr.Image(label="Upload Face Photo", type="numpy", height=300)
533
  cmp_intensity = gr.Slider(0, 100, 50, step=1, label="Intensity (%)")
534
  cmp_btn = gr.Button("Compare All", variant="primary", size="lg")
535
+ cmp_status = gr.HTML(
536
+ value='<div class="status-ready">Ready</div>',
537
+ )
538
  with gr.Column(scale=2):
539
  cmp_outputs = []
540
  rows_needed = (len(PROCEDURES) + 2) // 3
 
550
  )
551
  )
552
 
553
+ # Example images for Compare tab
554
+ if EXAMPLE_IMAGES:
555
+ gr.Examples(
556
+ examples=[[str(p)] for p in EXAMPLE_IMAGES],
557
+ inputs=[cmp_image],
558
+ label="Example faces",
559
+ )
560
+
561
+ def _compare_with_status(img, intens):
562
+ results = compare_procedures(img, intens)
563
+ return results + ['<div class="status-ready">Done -- 6 procedures compared</div>']
564
+
565
  cmp_btn.click(
566
+ fn=lambda: '<div class="status-processing">Processing 6 procedures...</div>',
567
+ inputs=None,
568
+ outputs=[cmp_status],
569
+ ).then(
570
+ fn=_compare_with_status,
571
  inputs=[cmp_image, cmp_intensity],
572
+ outputs=cmp_outputs + [cmp_status],
573
  )
574
 
575
+ # -- Intensity Sweep tab --
576
  with gr.Tab("Intensity Sweep"):
577
  gr.Markdown(
578
  "See how a procedure looks across intensity levels (0% through 100% in 20% steps)."
 
586
  label="Procedure",
587
  )
588
  sweep_btn = gr.Button("Generate Sweep", variant="primary", size="lg")
589
+ sweep_status = gr.HTML(
590
+ value='<div class="status-ready">Ready</div>',
591
+ )
592
  with gr.Column(scale=2):
593
  sweep_gallery = gr.Gallery(
594
  label="Intensity Sweep (0% - 100%)", columns=3, height=400
595
  )
596
 
597
+ # Example images for Sweep tab
598
+ if EXAMPLE_IMAGES:
599
+ gr.Examples(
600
+ examples=[[str(p)] for p in EXAMPLE_IMAGES],
601
+ inputs=[sweep_image],
602
+ label="Example faces",
603
+ )
604
+
605
+ def _sweep_with_status(img, proc):
606
+ results = intensity_sweep(img, proc)
607
+ if results:
608
+ status = '<div class="status-ready">Done -- 6 intensity levels generated</div>'
609
+ else:
610
+ status = '<div class="status-error">No face detected or processing failed</div>'
611
+ return results, status
612
+
613
  sweep_btn.click(
614
+ fn=lambda: '<div class="status-processing">Generating 6 intensity levels...</div>',
615
+ inputs=None,
616
+ outputs=[sweep_status],
617
+ ).then(
618
+ fn=_sweep_with_status,
619
  inputs=[sweep_image, sweep_procedure],
620
+ outputs=[sweep_gallery, sweep_status],
621
  )
622
 
623
  gr.Markdown(FOOTER_MD)
examples/example_face_1.png ADDED
examples/example_face_2.png ADDED
examples/example_face_3.png ADDED