dreamlessx commited on
Commit
5c6d1cb
·
verified ·
1 Parent(s): 0f5dd41

Fix slider performance and version string

Browse files
Files changed (1) hide show
  1. app.py +77 -34
app.py CHANGED
@@ -80,6 +80,7 @@ MIDLINE_BOTTOM = 152
80
  # Image preprocessing helpers
81
  # ---------------------------------------------------------------------------
82
 
 
83
  def _normalize_to_bgr(image: np.ndarray) -> np.ndarray:
84
  """Convert any input image format (RGBA, grayscale, etc.) to BGR uint8."""
85
  if image is None:
@@ -163,8 +164,7 @@ def _detect_face_with_hints(image_bgr: np.ndarray) -> tuple[FaceLandmarks | None
163
  # Check if image is mostly black (too dark even after auto-adjust)
164
  if float(np.mean(gray)) < 30:
165
  return None, (
166
- "No face detected -- the image appears very dark. "
167
- "Try a photo with better lighting."
168
  )
169
 
170
  # Check for very low contrast (washed out)
@@ -178,8 +178,7 @@ def _detect_face_with_hints(image_bgr: np.ndarray) -> tuple[FaceLandmarks | None
178
  aspect = w / max(h, 1)
179
  if aspect > 2.5 or aspect < 0.4:
180
  return None, (
181
- "No face detected -- unusual aspect ratio. "
182
- "Use a standard portrait or headshot photo."
183
  )
184
 
185
  return None, (
@@ -193,6 +192,7 @@ def _detect_face_with_hints(image_bgr: np.ndarray) -> tuple[FaceLandmarks | None
193
  # Symmetry analysis
194
  # ---------------------------------------------------------------------------
195
 
 
196
  def compute_symmetry_score(
197
  face: FaceLandmarks,
198
  ) -> tuple[float, dict[str, float]]:
@@ -212,7 +212,7 @@ def compute_symmetry_score(
212
  coords = face.pixel_coords # (478, 2) -- property, not method
213
 
214
  # Compute facial midline from forehead top (10) and chin bottom (152)
215
- mid_top = coords[MIDLINE_TOP] # (2,)
216
  mid_bot = coords[MIDLINE_BOTTOM] # (2,)
217
 
218
  # Midline direction vector and unit normal
@@ -297,18 +297,24 @@ def render_symmetry_overlay(
297
 
298
  # Small label at midline top
299
  cv2.putText(
300
- canvas, "midline", (int(mid_top[0]) + 5, int(mid_top[1]) - 5),
301
- cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 200, 0), 1, cv2.LINE_AA,
 
 
 
 
 
 
302
  )
303
 
304
  def _score_color(score: float) -> tuple[int, int, int]:
305
  """BGR color based on symmetry score."""
306
  if score >= 80:
307
- return (0, 200, 0) # green
308
  elif score >= 50:
309
  return (0, 200, 220) # yellow (BGR)
310
  else:
311
- return (0, 0, 220) # red
312
 
313
  for region, pairs in SYMMETRY_PAIRS.items():
314
  score = region_scores.get(region, 0.0)
@@ -351,8 +357,14 @@ def render_symmetry_overlay(
351
 
352
  label = f"{region}: {score:.0f}"
353
  cv2.putText(
354
- canvas, label, label_pos,
355
- cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1, cv2.LINE_AA,
 
 
 
 
 
 
356
  )
357
 
358
  return canvas
@@ -379,6 +391,7 @@ def _format_symmetry_text(
379
  # Symmetry tab callbacks
380
  # ---------------------------------------------------------------------------
381
 
 
382
  def analyze_symmetry(image_rgb: np.ndarray):
383
  """Analyze facial symmetry from an uploaded photo."""
384
  if image_rgb is None:
@@ -461,6 +474,7 @@ def analyze_symmetry_comparison(
461
  # Core pipeline functions
462
  # ---------------------------------------------------------------------------
463
 
 
464
  def warp_image_tps(image, src_pts, dst_pts):
465
  """Thin-plate spline warp (CPU only)."""
466
  from landmarkdiff.synthetic.tps_warp import warp_image_tps as _warp
@@ -511,7 +525,10 @@ def process_image(image_rgb, procedure, intensity):
511
  if hint:
512
  return image_rgb_512, image_rgb_512, image_rgb_512, image_rgb_512, hint
513
  return (
514
- image_rgb_512, image_rgb_512, image_rgb_512, image_rgb_512,
 
 
 
515
  "No face detected. Try a clearer, well-lit frontal photo.",
516
  )
517
 
@@ -527,9 +544,7 @@ def process_image(image_rgb, procedure, intensity):
527
  composited = mask_composite(warped, image_bgr, mask)
528
  composited_rgb = cv2.cvtColor(composited, cv2.COLOR_BGR2RGB)
529
 
530
- displacement = np.mean(
531
- np.linalg.norm(manipulated.pixel_coords - face.pixel_coords, axis=1)
532
- )
533
  elapsed = time.monotonic() - t0
534
 
535
  # Compute symmetry for original and predicted result
@@ -615,8 +630,7 @@ def intensity_sweep(image_rgb, procedure):
615
  # ---------------------------------------------------------------------------
616
 
617
  _proc_table = "\n".join(
618
- f"| {name.replace('_', ' ').title()} | {desc} |"
619
- for name, desc in PROCEDURE_INFO.items()
620
  )
621
 
622
  with gr.Blocks(
@@ -639,10 +653,16 @@ with gr.Blocks(
639
  with gr.Column(scale=1):
640
  input_image = gr.Image(label="Face Photo", type="numpy", height=350)
641
  procedure = gr.Radio(
642
- choices=PROCEDURES, value="rhinoplasty", label="Procedure",
 
 
643
  )
644
  intensity = gr.Slider(
645
- 0, 100, 50, step=1, label="Intensity (%)",
 
 
 
 
646
  info="0 = no change, 100 = maximum",
647
  )
648
  run_btn = gr.Button("Generate", variant="primary", size="lg")
@@ -666,8 +686,12 @@ with gr.Blocks(
666
  outputs = [out_wireframe, out_mask, out_result, out_original, info_box]
667
  _inputs = [input_image, procedure, intensity]
668
  run_btn.click(fn=process_image, inputs=_inputs, outputs=outputs)
669
- for trigger in _inputs:
670
- trigger.change(fn=process_image, inputs=_inputs, outputs=outputs)
 
 
 
 
671
 
672
  # -- Tab 2: Compare Procedures --
673
  with gr.Tab("Compare All"):
@@ -694,7 +718,8 @@ with gr.Blocks(
694
  if EXAMPLE_IMAGES:
695
  gr.Examples(
696
  examples=[[str(p)] for p in EXAMPLE_IMAGES],
697
- inputs=[cmp_image], label="Examples",
 
698
  )
699
 
700
  cmp_btn.click(fn=compare_procedures, inputs=[cmp_image, cmp_intensity], outputs=cmp_outputs)
@@ -713,7 +738,8 @@ with gr.Blocks(
713
  if EXAMPLE_IMAGES:
714
  gr.Examples(
715
  examples=[[str(p)] for p in EXAMPLE_IMAGES],
716
- inputs=[sweep_image], label="Examples",
 
717
  )
718
 
719
  sweep_btn.click(
@@ -741,22 +767,29 @@ with gr.Blocks(
741
  with gr.Row():
742
  with gr.Column(scale=1):
743
  sym_image = gr.Image(
744
- label="Face Photo", type="numpy", height=350,
 
 
745
  )
746
  sym_btn = gr.Button(
747
- "Analyze Symmetry", variant="primary", size="lg",
 
 
748
  )
749
  with gr.Column(scale=1):
750
  sym_overlay = gr.Image(label="Symmetry Overlay", height=350)
751
 
752
  sym_scores_box = gr.Textbox(
753
- label="Symmetry Scores", lines=8, interactive=False,
 
 
754
  )
755
 
756
  if EXAMPLE_IMAGES:
757
  gr.Examples(
758
  examples=[[str(p)] for p in EXAMPLE_IMAGES],
759
- inputs=[sym_image], label="Examples",
 
760
  )
761
 
762
  sym_btn.click(
@@ -773,23 +806,33 @@ with gr.Blocks(
773
  )
774
  with gr.Row():
775
  sym_pre_image = gr.Image(
776
- label="Pre-Procedure", type="numpy", height=300,
 
 
777
  )
778
  sym_post_image = gr.Image(
779
- label="Post-Procedure", type="numpy", height=300,
 
 
780
  )
781
  sym_cmp_btn = gr.Button(
782
- "Compare Symmetry", variant="primary", size="lg",
 
 
783
  )
784
  with gr.Row():
785
  sym_pre_overlay = gr.Image(
786
- label="Pre Symmetry Overlay", height=300,
 
787
  )
788
  sym_post_overlay = gr.Image(
789
- label="Post Symmetry Overlay", height=300,
 
790
  )
791
  sym_cmp_box = gr.Textbox(
792
- label="Comparison", lines=14, interactive=False,
 
 
793
  )
794
 
795
  sym_cmp_btn.click(
@@ -800,7 +843,7 @@ with gr.Blocks(
800
 
801
  gr.Markdown(
802
  f"<div style='text-align:center;color:#999;font-size:0.8em;padding:8px'>"
803
- f"LandmarkDiff v0.2.3 | TPS on CPU | MediaPipe 478-point mesh | "
804
  f"<a href='{GITHUB_URL}'>GitHub</a> | MIT License</div>"
805
  )
806
 
 
80
  # Image preprocessing helpers
81
  # ---------------------------------------------------------------------------
82
 
83
+
84
  def _normalize_to_bgr(image: np.ndarray) -> np.ndarray:
85
  """Convert any input image format (RGBA, grayscale, etc.) to BGR uint8."""
86
  if image is None:
 
164
  # Check if image is mostly black (too dark even after auto-adjust)
165
  if float(np.mean(gray)) < 30:
166
  return None, (
167
+ "No face detected -- the image appears very dark. Try a photo with better lighting."
 
168
  )
169
 
170
  # Check for very low contrast (washed out)
 
178
  aspect = w / max(h, 1)
179
  if aspect > 2.5 or aspect < 0.4:
180
  return None, (
181
+ "No face detected -- unusual aspect ratio. Use a standard portrait or headshot photo."
 
182
  )
183
 
184
  return None, (
 
192
  # Symmetry analysis
193
  # ---------------------------------------------------------------------------
194
 
195
+
196
  def compute_symmetry_score(
197
  face: FaceLandmarks,
198
  ) -> tuple[float, dict[str, float]]:
 
212
  coords = face.pixel_coords # (478, 2) -- property, not method
213
 
214
  # Compute facial midline from forehead top (10) and chin bottom (152)
215
+ mid_top = coords[MIDLINE_TOP] # (2,)
216
  mid_bot = coords[MIDLINE_BOTTOM] # (2,)
217
 
218
  # Midline direction vector and unit normal
 
297
 
298
  # Small label at midline top
299
  cv2.putText(
300
+ canvas,
301
+ "midline",
302
+ (int(mid_top[0]) + 5, int(mid_top[1]) - 5),
303
+ cv2.FONT_HERSHEY_SIMPLEX,
304
+ 0.4,
305
+ (255, 200, 0),
306
+ 1,
307
+ cv2.LINE_AA,
308
  )
309
 
310
  def _score_color(score: float) -> tuple[int, int, int]:
311
  """BGR color based on symmetry score."""
312
  if score >= 80:
313
+ return (0, 200, 0) # green
314
  elif score >= 50:
315
  return (0, 200, 220) # yellow (BGR)
316
  else:
317
+ return (0, 0, 220) # red
318
 
319
  for region, pairs in SYMMETRY_PAIRS.items():
320
  score = region_scores.get(region, 0.0)
 
357
 
358
  label = f"{region}: {score:.0f}"
359
  cv2.putText(
360
+ canvas,
361
+ label,
362
+ label_pos,
363
+ cv2.FONT_HERSHEY_SIMPLEX,
364
+ 0.4,
365
+ color,
366
+ 1,
367
+ cv2.LINE_AA,
368
  )
369
 
370
  return canvas
 
391
  # Symmetry tab callbacks
392
  # ---------------------------------------------------------------------------
393
 
394
+
395
  def analyze_symmetry(image_rgb: np.ndarray):
396
  """Analyze facial symmetry from an uploaded photo."""
397
  if image_rgb is None:
 
474
  # Core pipeline functions
475
  # ---------------------------------------------------------------------------
476
 
477
+
478
  def warp_image_tps(image, src_pts, dst_pts):
479
  """Thin-plate spline warp (CPU only)."""
480
  from landmarkdiff.synthetic.tps_warp import warp_image_tps as _warp
 
525
  if hint:
526
  return image_rgb_512, image_rgb_512, image_rgb_512, image_rgb_512, hint
527
  return (
528
+ image_rgb_512,
529
+ image_rgb_512,
530
+ image_rgb_512,
531
+ image_rgb_512,
532
  "No face detected. Try a clearer, well-lit frontal photo.",
533
  )
534
 
 
544
  composited = mask_composite(warped, image_bgr, mask)
545
  composited_rgb = cv2.cvtColor(composited, cv2.COLOR_BGR2RGB)
546
 
547
+ displacement = np.mean(np.linalg.norm(manipulated.pixel_coords - face.pixel_coords, axis=1))
 
 
548
  elapsed = time.monotonic() - t0
549
 
550
  # Compute symmetry for original and predicted result
 
630
  # ---------------------------------------------------------------------------
631
 
632
  _proc_table = "\n".join(
633
+ f"| {name.replace('_', ' ').title()} | {desc} |" for name, desc in PROCEDURE_INFO.items()
 
634
  )
635
 
636
  with gr.Blocks(
 
653
  with gr.Column(scale=1):
654
  input_image = gr.Image(label="Face Photo", type="numpy", height=350)
655
  procedure = gr.Radio(
656
+ choices=PROCEDURES,
657
+ value="rhinoplasty",
658
+ label="Procedure",
659
  )
660
  intensity = gr.Slider(
661
+ 0,
662
+ 100,
663
+ 50,
664
+ step=1,
665
+ label="Intensity (%)",
666
  info="0 = no change, 100 = maximum",
667
  )
668
  run_btn = gr.Button("Generate", variant="primary", size="lg")
 
686
  outputs = [out_wireframe, out_mask, out_result, out_original, info_box]
687
  _inputs = [input_image, procedure, intensity]
688
  run_btn.click(fn=process_image, inputs=_inputs, outputs=outputs)
689
+ # Auto-trigger on image upload and procedure change, but not on every
690
+ # slider tick during drag (each tick would re-run TPS on free CPU,
691
+ # causing severe lag). Use .release so it fires once on mouse-up.
692
+ input_image.change(fn=process_image, inputs=_inputs, outputs=outputs)
693
+ procedure.change(fn=process_image, inputs=_inputs, outputs=outputs)
694
+ intensity.release(fn=process_image, inputs=_inputs, outputs=outputs)
695
 
696
  # -- Tab 2: Compare Procedures --
697
  with gr.Tab("Compare All"):
 
718
  if EXAMPLE_IMAGES:
719
  gr.Examples(
720
  examples=[[str(p)] for p in EXAMPLE_IMAGES],
721
+ inputs=[cmp_image],
722
+ label="Examples",
723
  )
724
 
725
  cmp_btn.click(fn=compare_procedures, inputs=[cmp_image, cmp_intensity], outputs=cmp_outputs)
 
738
  if EXAMPLE_IMAGES:
739
  gr.Examples(
740
  examples=[[str(p)] for p in EXAMPLE_IMAGES],
741
+ inputs=[sweep_image],
742
+ label="Examples",
743
  )
744
 
745
  sweep_btn.click(
 
767
  with gr.Row():
768
  with gr.Column(scale=1):
769
  sym_image = gr.Image(
770
+ label="Face Photo",
771
+ type="numpy",
772
+ height=350,
773
  )
774
  sym_btn = gr.Button(
775
+ "Analyze Symmetry",
776
+ variant="primary",
777
+ size="lg",
778
  )
779
  with gr.Column(scale=1):
780
  sym_overlay = gr.Image(label="Symmetry Overlay", height=350)
781
 
782
  sym_scores_box = gr.Textbox(
783
+ label="Symmetry Scores",
784
+ lines=8,
785
+ interactive=False,
786
  )
787
 
788
  if EXAMPLE_IMAGES:
789
  gr.Examples(
790
  examples=[[str(p)] for p in EXAMPLE_IMAGES],
791
+ inputs=[sym_image],
792
+ label="Examples",
793
  )
794
 
795
  sym_btn.click(
 
806
  )
807
  with gr.Row():
808
  sym_pre_image = gr.Image(
809
+ label="Pre-Procedure",
810
+ type="numpy",
811
+ height=300,
812
  )
813
  sym_post_image = gr.Image(
814
+ label="Post-Procedure",
815
+ type="numpy",
816
+ height=300,
817
  )
818
  sym_cmp_btn = gr.Button(
819
+ "Compare Symmetry",
820
+ variant="primary",
821
+ size="lg",
822
  )
823
  with gr.Row():
824
  sym_pre_overlay = gr.Image(
825
+ label="Pre Symmetry Overlay",
826
+ height=300,
827
  )
828
  sym_post_overlay = gr.Image(
829
+ label="Post Symmetry Overlay",
830
+ height=300,
831
  )
832
  sym_cmp_box = gr.Textbox(
833
+ label="Comparison",
834
+ lines=14,
835
+ interactive=False,
836
  )
837
 
838
  sym_cmp_btn.click(
 
843
 
844
  gr.Markdown(
845
  f"<div style='text-align:center;color:#999;font-size:0.8em;padding:8px'>"
846
+ f"LandmarkDiff v0.2.2 | TPS on CPU | MediaPipe 478-point mesh | "
847
  f"<a href='{GITHUB_URL}'>GitHub</a> | MIT License</div>"
848
  )
849