dreamlessx commited on
Commit
4ed47f2
·
1 Parent(s): f069dfc

Fix Gradio API crash, render_wireframe args, add brow_lift/mentoplasty masks

Browse files
Files changed (4) hide show
  1. README.md +4 -4
  2. app.py +28 -14
  3. landmarkdiff/masking.py +170 -16
  4. requirements.txt +1 -0
README.md CHANGED
@@ -1,11 +1,11 @@
1
  ---
2
  title: LandmarkDiff
3
- emoji: 🔬
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.12.0
8
- python_version: 3.11
9
  app_file: app.py
10
  pinned: true
11
  license: mit
@@ -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.0
 
1
  ---
2
  title: LandmarkDiff
3
+ emoji: "\U0001F52C"
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 5.29.0
8
+ python_version: "3.11"
9
  app_file: app.py
10
  pinned: true
11
  license: mit
 
52
  - [Wiki](https://github.com/dreamlessx/LandmarkDiff-public/wiki)
53
  - [Discussions](https://github.com/dreamlessx/LandmarkDiff-public/discussions)
54
 
55
+ **Version:** v0.2.1
app.py CHANGED
@@ -6,12 +6,12 @@ import cv2
6
  import gradio as gr
7
  import numpy as np
8
 
9
- from landmarkdiff.landmarks import extract_landmarks, render_landmark_image
10
  from landmarkdiff.conditioning import render_wireframe
11
- from landmarkdiff.manipulation import apply_procedure_preset, PROCEDURE_LANDMARKS
 
12
  from landmarkdiff.masking import generate_surgical_mask
13
 
14
- VERSION = "v0.2.0"
15
 
16
  GITHUB_URL = "https://github.com/dreamlessx/LandmarkDiff-public"
17
  DOCS_URL = f"{GITHUB_URL}/tree/main/docs"
@@ -31,6 +31,7 @@ PROCEDURE_DESCRIPTIONS = {
31
  def warp_image_tps(image, src_pts, dst_pts):
32
  """Thin-plate spline warp (CPU only)."""
33
  from landmarkdiff.synthetic.tps_warp import warp_image_tps as _warp
 
34
  return _warp(image, src_pts, dst_pts)
35
 
36
 
@@ -55,14 +56,20 @@ def process_image(image_rgb, procedure, intensity):
55
 
56
  face = extract_landmarks(image_bgr)
57
  if face is None:
58
- return image_rgb_512, image_rgb_512, image_rgb_512, image_rgb_512, "No face detected. Try a clearer photo with good lighting."
 
 
 
 
 
 
59
 
60
  # Manipulate landmarks
61
  manipulated = apply_procedure_preset(face, procedure, float(intensity), image_size=512)
62
 
63
- # Generate wireframe
64
- wireframe = render_wireframe(manipulated, (512, 512))
65
- wireframe_rgb = cv2.cvtColor(wireframe, cv2.COLOR_BGR2RGB) if wireframe.ndim == 3 else wireframe
66
 
67
  # Generate mask
68
  mask = generate_surgical_mask(face, procedure, 512, 512)
@@ -77,9 +84,7 @@ def process_image(image_rgb, procedure, intensity):
77
  side_by_side = np.hstack([image_rgb_512, composited_rgb])
78
 
79
  # Displacement stats
80
- displacement = np.mean(np.linalg.norm(
81
- manipulated.pixel_coords - face.pixel_coords, axis=1
82
- ))
83
 
84
  info = (
85
  f"Procedure: {procedure}\n"
@@ -156,7 +161,8 @@ HEADER_MD = f"""
156
 
157
  **Anatomically-conditioned facial surgery outcome prediction from standard clinical photography**
158
 
159
- Upload a face photo, select a procedure, and adjust intensity to see a predicted surgical outcome in real time.
 
160
  This demo runs TPS (thin-plate spline) warping on CPU. The full package also supports
161
  GPU-accelerated ControlNet and img2img inference modes.
162
 
@@ -219,7 +225,10 @@ with gr.Blocks(
219
  label="Surgical Procedure",
220
  )
221
  intensity = gr.Slider(
222
- minimum=0, maximum=100, value=50, step=1,
 
 
 
223
  label="Intensity (%)",
224
  info="0 = no change, 100 = maximum effect",
225
  )
@@ -262,7 +271,10 @@ with gr.Blocks(
262
  proc_idx = row_idx * 3 + col_idx
263
  if proc_idx < len(PROCEDURES):
264
  cmp_outputs.append(
265
- gr.Image(label=PROCEDURES[proc_idx].replace("_", " ").title(), height=200)
 
 
 
266
  )
267
 
268
  cmp_btn.click(
@@ -285,7 +297,9 @@ with gr.Blocks(
285
  )
286
  sweep_btn = gr.Button("Generate Sweep", variant="primary", size="lg")
287
  with gr.Column(scale=2):
288
- sweep_gallery = gr.Gallery(label="Intensity Sweep (0% - 100%)", columns=3, height=400)
 
 
289
 
290
  sweep_btn.click(
291
  fn=intensity_sweep,
 
6
  import gradio as gr
7
  import numpy as np
8
 
 
9
  from landmarkdiff.conditioning import render_wireframe
10
+ from landmarkdiff.landmarks import extract_landmarks
11
+ from landmarkdiff.manipulation import PROCEDURE_LANDMARKS, apply_procedure_preset
12
  from landmarkdiff.masking import generate_surgical_mask
13
 
14
+ VERSION = "v0.2.1"
15
 
16
  GITHUB_URL = "https://github.com/dreamlessx/LandmarkDiff-public"
17
  DOCS_URL = f"{GITHUB_URL}/tree/main/docs"
 
31
  def warp_image_tps(image, src_pts, dst_pts):
32
  """Thin-plate spline warp (CPU only)."""
33
  from landmarkdiff.synthetic.tps_warp import warp_image_tps as _warp
34
+
35
  return _warp(image, src_pts, dst_pts)
36
 
37
 
 
56
 
57
  face = extract_landmarks(image_bgr)
58
  if face is None:
59
+ return (
60
+ image_rgb_512,
61
+ image_rgb_512,
62
+ image_rgb_512,
63
+ image_rgb_512,
64
+ "No face detected. Try a clearer photo with good lighting.",
65
+ )
66
 
67
  # Manipulate landmarks
68
  manipulated = apply_procedure_preset(face, procedure, float(intensity), image_size=512)
69
 
70
+ # Generate wireframe (pass width and height as separate keyword args)
71
+ wireframe = render_wireframe(manipulated, width=512, height=512)
72
+ wireframe_rgb = cv2.cvtColor(wireframe, cv2.COLOR_GRAY2RGB)
73
 
74
  # Generate mask
75
  mask = generate_surgical_mask(face, procedure, 512, 512)
 
84
  side_by_side = np.hstack([image_rgb_512, composited_rgb])
85
 
86
  # Displacement stats
87
+ displacement = np.mean(np.linalg.norm(manipulated.pixel_coords - face.pixel_coords, axis=1))
 
 
88
 
89
  info = (
90
  f"Procedure: {procedure}\n"
 
161
 
162
  **Anatomically-conditioned facial surgery outcome prediction from standard clinical photography**
163
 
164
+ Upload a face photo, select a procedure, and adjust intensity to see a predicted
165
+ surgical outcome in real time.
166
  This demo runs TPS (thin-plate spline) warping on CPU. The full package also supports
167
  GPU-accelerated ControlNet and img2img inference modes.
168
 
 
225
  label="Surgical Procedure",
226
  )
227
  intensity = gr.Slider(
228
+ minimum=0,
229
+ maximum=100,
230
+ value=50,
231
+ step=1,
232
  label="Intensity (%)",
233
  info="0 = no change, 100 = maximum effect",
234
  )
 
271
  proc_idx = row_idx * 3 + col_idx
272
  if proc_idx < len(PROCEDURES):
273
  cmp_outputs.append(
274
+ gr.Image(
275
+ label=PROCEDURES[proc_idx].replace("_", " ").title(),
276
+ height=200,
277
+ )
278
  )
279
 
280
  cmp_btn.click(
 
297
  )
298
  sweep_btn = gr.Button("Generate Sweep", variant="primary", size="lg")
299
  with gr.Column(scale=2):
300
+ sweep_gallery = gr.Gallery(
301
+ label="Intensity Sweep (0% - 100%)", columns=3, height=400
302
+ )
303
 
304
  sweep_btn.click(
305
  fn=intensity_sweep,
landmarkdiff/masking.py CHANGED
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING
12
  import cv2
13
  import numpy as np
14
 
15
- from landmarkdiff.landmarks import FaceLandmarks, LANDMARK_REGIONS
16
 
17
  if TYPE_CHECKING:
18
  from landmarkdiff.clinical import ClinicalFlags
@@ -21,39 +21,188 @@ if TYPE_CHECKING:
21
  MASK_CONFIG: dict[str, dict] = {
22
  "rhinoplasty": {
23
  "landmark_indices": [
24
- 1, 2, 4, 5, 6, 19, 94, 141, 168, 195, 197, 236, 240,
25
- 274, 275, 278, 279, 294, 326, 327, 360, 363, 370, 456, 460,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  ],
27
  "dilation_px": 30,
28
  "feather_sigma": 15.0,
29
  },
30
  "blepharoplasty": {
31
  "landmark_indices": [
32
- 33, 7, 163, 144, 145, 153, 154, 155, 157, 158, 159, 160, 161, 246,
33
- 362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386,
34
- 385, 384, 398,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  ],
36
  "dilation_px": 15,
37
  "feather_sigma": 10.0,
38
  },
39
  "rhytidectomy": {
40
  "landmark_indices": [
41
- 10, 21, 54, 58, 67, 93, 103, 109, 127, 132, 136, 150, 162, 172,
42
- 176, 187, 207, 213, 234, 284, 297, 323, 332, 338, 356, 361, 365,
43
- 379, 389, 397, 400, 427, 454,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  ],
45
  "dilation_px": 40,
46
  "feather_sigma": 20.0,
47
  },
48
  "orthognathic": {
49
  "landmark_indices": [
50
- 0, 17, 18, 36, 37, 39, 40, 57, 61, 78, 80, 81, 82, 84, 87, 88,
51
- 91, 95, 146, 167, 169, 170, 175, 181, 191, 200, 201, 202, 204,
52
- 208, 211, 212, 214,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  ],
54
  "dilation_px": 35,
55
  "feather_sigma": 18.0,
56
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
 
59
 
@@ -62,7 +211,7 @@ def generate_surgical_mask(
62
  procedure: str,
63
  width: int | None = None,
64
  height: int | None = None,
65
- clinical_flags: "ClinicalFlags | None" = None,
66
  image: np.ndarray | None = None,
67
  ) -> np.ndarray:
68
  """Generate a feathered surgical mask for a procedure.
@@ -134,15 +283,20 @@ def generate_surgical_mask(
134
  if clinical_flags is not None:
135
  # Vitiligo: reduce mask over depigmented patches to preserve them
136
  if clinical_flags.vitiligo and image is not None:
137
- from landmarkdiff.clinical import detect_vitiligo_patches, adjust_mask_for_vitiligo
 
138
  patches = detect_vitiligo_patches(image, face)
139
  mask = adjust_mask_for_vitiligo(mask, patches)
140
 
141
  # Keloid: soften transitions in keloid-prone regions
142
  if clinical_flags.keloid_prone and clinical_flags.keloid_regions:
143
- from landmarkdiff.clinical import get_keloid_exclusion_mask, adjust_mask_for_keloid
 
144
  keloid_mask = get_keloid_exclusion_mask(
145
- face, clinical_flags.keloid_regions, w, h,
 
 
 
146
  )
147
  mask = adjust_mask_for_keloid(mask, keloid_mask)
148
 
 
12
  import cv2
13
  import numpy as np
14
 
15
+ from landmarkdiff.landmarks import FaceLandmarks
16
 
17
  if TYPE_CHECKING:
18
  from landmarkdiff.clinical import ClinicalFlags
 
21
  MASK_CONFIG: dict[str, dict] = {
22
  "rhinoplasty": {
23
  "landmark_indices": [
24
+ 1,
25
+ 2,
26
+ 4,
27
+ 5,
28
+ 6,
29
+ 19,
30
+ 94,
31
+ 141,
32
+ 168,
33
+ 195,
34
+ 197,
35
+ 236,
36
+ 240,
37
+ 274,
38
+ 275,
39
+ 278,
40
+ 279,
41
+ 294,
42
+ 326,
43
+ 327,
44
+ 360,
45
+ 363,
46
+ 370,
47
+ 456,
48
+ 460,
49
  ],
50
  "dilation_px": 30,
51
  "feather_sigma": 15.0,
52
  },
53
  "blepharoplasty": {
54
  "landmark_indices": [
55
+ 33,
56
+ 7,
57
+ 163,
58
+ 144,
59
+ 145,
60
+ 153,
61
+ 154,
62
+ 155,
63
+ 157,
64
+ 158,
65
+ 159,
66
+ 160,
67
+ 161,
68
+ 246,
69
+ 362,
70
+ 382,
71
+ 381,
72
+ 380,
73
+ 374,
74
+ 373,
75
+ 390,
76
+ 249,
77
+ 263,
78
+ 466,
79
+ 388,
80
+ 387,
81
+ 386,
82
+ 385,
83
+ 384,
84
+ 398,
85
  ],
86
  "dilation_px": 15,
87
  "feather_sigma": 10.0,
88
  },
89
  "rhytidectomy": {
90
  "landmark_indices": [
91
+ 10,
92
+ 21,
93
+ 54,
94
+ 58,
95
+ 67,
96
+ 93,
97
+ 103,
98
+ 109,
99
+ 127,
100
+ 132,
101
+ 136,
102
+ 150,
103
+ 162,
104
+ 172,
105
+ 176,
106
+ 187,
107
+ 207,
108
+ 213,
109
+ 234,
110
+ 284,
111
+ 297,
112
+ 323,
113
+ 332,
114
+ 338,
115
+ 356,
116
+ 361,
117
+ 365,
118
+ 379,
119
+ 389,
120
+ 397,
121
+ 400,
122
+ 427,
123
+ 454,
124
  ],
125
  "dilation_px": 40,
126
  "feather_sigma": 20.0,
127
  },
128
  "orthognathic": {
129
  "landmark_indices": [
130
+ 0,
131
+ 17,
132
+ 18,
133
+ 36,
134
+ 37,
135
+ 39,
136
+ 40,
137
+ 57,
138
+ 61,
139
+ 78,
140
+ 80,
141
+ 81,
142
+ 82,
143
+ 84,
144
+ 87,
145
+ 88,
146
+ 91,
147
+ 95,
148
+ 146,
149
+ 167,
150
+ 169,
151
+ 170,
152
+ 175,
153
+ 181,
154
+ 191,
155
+ 200,
156
+ 201,
157
+ 202,
158
+ 204,
159
+ 208,
160
+ 211,
161
+ 212,
162
+ 214,
163
  ],
164
  "dilation_px": 35,
165
  "feather_sigma": 18.0,
166
  },
167
+ "brow_lift": {
168
+ "landmark_indices": [
169
+ 70,
170
+ 63,
171
+ 105,
172
+ 66,
173
+ 107, # left brow
174
+ 300,
175
+ 293,
176
+ 334,
177
+ 296,
178
+ 336, # right brow
179
+ 9,
180
+ 8,
181
+ 10, # forehead midline
182
+ 109,
183
+ 67,
184
+ 103, # upper face left
185
+ 338,
186
+ 297,
187
+ 332, # upper face right
188
+ ],
189
+ "dilation_px": 25,
190
+ "feather_sigma": 15.0,
191
+ },
192
+ "mentoplasty": {
193
+ "landmark_indices": [
194
+ 148,
195
+ 149,
196
+ 150,
197
+ 152,
198
+ 171,
199
+ 175,
200
+ 176,
201
+ 377,
202
+ ],
203
+ "dilation_px": 25,
204
+ "feather_sigma": 12.0,
205
+ },
206
  }
207
 
208
 
 
211
  procedure: str,
212
  width: int | None = None,
213
  height: int | None = None,
214
+ clinical_flags: ClinicalFlags | None = None,
215
  image: np.ndarray | None = None,
216
  ) -> np.ndarray:
217
  """Generate a feathered surgical mask for a procedure.
 
283
  if clinical_flags is not None:
284
  # Vitiligo: reduce mask over depigmented patches to preserve them
285
  if clinical_flags.vitiligo and image is not None:
286
+ from landmarkdiff.clinical import adjust_mask_for_vitiligo, detect_vitiligo_patches
287
+
288
  patches = detect_vitiligo_patches(image, face)
289
  mask = adjust_mask_for_vitiligo(mask, patches)
290
 
291
  # Keloid: soften transitions in keloid-prone regions
292
  if clinical_flags.keloid_prone and clinical_flags.keloid_regions:
293
+ from landmarkdiff.clinical import adjust_mask_for_keloid, get_keloid_exclusion_mask
294
+
295
  keloid_mask = get_keloid_exclusion_mask(
296
+ face,
297
+ clinical_flags.keloid_regions,
298
+ w,
299
+ h,
300
  )
301
  mask = adjust_mask_for_keloid(mask, keloid_mask)
302
 
requirements.txt CHANGED
@@ -3,3 +3,4 @@ mediapipe>=0.10.9
3
  opencv-python-headless>=4.9.0
4
  numpy>=1.26.0
5
  Pillow>=10.0.0
 
 
3
  opencv-python-headless>=4.9.0
4
  numpy>=1.26.0
5
  Pillow>=10.0.0
6
+ pydantic<2.11