dreamlessx commited on
Commit
c107618
·
verified ·
1 Parent(s): 344a626

Upload landmarkdiff/manipulation.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. landmarkdiff/manipulation.py +380 -0
landmarkdiff/manipulation.py ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Landmark manipulation via Gaussian RBF deformation.
2
+
3
+ v1/v2 uses relative sliders (0-100 intensity).
4
+ mm inputs only in v3+ with FLAME calibrated metric space.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Optional, TYPE_CHECKING
11
+
12
+ import numpy as np
13
+
14
+ from landmarkdiff.landmarks import FaceLandmarks, LANDMARK_REGIONS
15
+
16
+ if TYPE_CHECKING:
17
+ from landmarkdiff.clinical import ClinicalFlags
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class DeformationHandle:
22
+ """Single deformation control point."""
23
+
24
+ landmark_index: int
25
+ displacement: np.ndarray # (2,) or (3,) pixel displacement
26
+ influence_radius: float # Gaussian RBF radius in pixels
27
+
28
+
29
+ # Procedure-specific landmark indices from the technical specification
30
+ PROCEDURE_LANDMARKS: dict[str, list[int]] = {
31
+ "rhinoplasty": [
32
+ 1, 2, 4, 5, 6, 19, 94, 141, 168, 195, 197, 236, 240,
33
+ 274, 275, 278, 279, 294, 326, 327, 360, 363, 370, 456, 460,
34
+ ],
35
+ "blepharoplasty": [
36
+ 33, 7, 163, 144, 145, 153, 154, 155, 157, 158, 159, 160, 161, 246,
37
+ 362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386,
38
+ 385, 384, 398,
39
+ ],
40
+ "rhytidectomy": [
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
+ "orthognathic": [
46
+ 0, 17, 18, 36, 37, 39, 40, 57, 61, 78, 80, 81, 82, 84, 87, 88,
47
+ 91, 95, 146, 167, 169, 170, 175, 181, 191, 200, 201, 202, 204,
48
+ 208, 211, 212, 214, 269, 270, 291, 311, 312, 317, 321, 324, 325,
49
+ 375, 396, 405, 407, 415,
50
+ ],
51
+ "brow_lift": [
52
+ 70, 63, 105, 66, 107, # left brow
53
+ 300, 293, 334, 296, 336, # right brow
54
+ 9, 8, 10, 109, 67, 103, 338, 297, 332, # forehead/upper face
55
+ ],
56
+ "mentoplasty": [
57
+ 148, 149, 150, 152, 171, 175, 176, 377,
58
+ ],
59
+ }
60
+ # Default influence radii per procedure (in pixels at 512x512)
61
+ PROCEDURE_RADIUS: dict[str, float] = {
62
+ "rhinoplasty": 30.0,
63
+ "blepharoplasty": 15.0,
64
+ "rhytidectomy": 40.0,
65
+ "orthognathic": 35.0,
66
+ "brow_lift": 25.0,
67
+ "mentoplasty": 25.0,
68
+ }
69
+
70
+
71
+ def gaussian_rbf_deform(
72
+ landmarks: np.ndarray,
73
+ handle: DeformationHandle,
74
+ ) -> np.ndarray:
75
+ """Gaussian RBF deform: delta * exp(-dist^2 / 2r^2). Returns copy."""
76
+ result = landmarks.copy()
77
+ center = landmarks[handle.landmark_index, :2]
78
+ displacement = handle.displacement[:2]
79
+
80
+ distances_sq = np.sum((landmarks[:, :2] - center) ** 2, axis=1)
81
+ weights = np.exp(-distances_sq / (2.0 * handle.influence_radius ** 2))
82
+
83
+ result[:, 0] += displacement[0] * weights
84
+ result[:, 1] += displacement[1] * weights
85
+
86
+ if landmarks.shape[1] > 2 and len(handle.displacement) > 2:
87
+ result[:, 2] += handle.displacement[2] * weights
88
+
89
+ return result
90
+
91
+
92
+ def apply_procedure_preset(
93
+ face: FaceLandmarks,
94
+ procedure: str,
95
+ intensity: float = 50.0,
96
+ image_size: int = 512,
97
+ clinical_flags: Optional["ClinicalFlags"] = None,
98
+ ) -> FaceLandmarks:
99
+ """Apply a named procedure preset at given intensity (0-100)."""
100
+ if procedure not in PROCEDURE_LANDMARKS:
101
+ raise ValueError(f"Unknown procedure: {procedure}. Choose from {list(PROCEDURE_LANDMARKS)}")
102
+
103
+ landmarks = face.landmarks.copy()
104
+ indices = PROCEDURE_LANDMARKS[procedure]
105
+ radius = PROCEDURE_RADIUS[procedure]
106
+ scale = intensity / 100.0
107
+
108
+ # Ehlers-Danlos: wider influence radii for hypermobile tissue
109
+ if clinical_flags and clinical_flags.ehlers_danlos:
110
+ radius *= 1.5
111
+
112
+ # Procedure-specific displacement vectors (normalized to image_size)
113
+ pixel_scale = image_size / 512.0
114
+ handles = _get_procedure_handles(procedure, indices, scale, radius * pixel_scale)
115
+
116
+ # Bell's palsy: remove handles on the affected (paralyzed) side
117
+ if clinical_flags and clinical_flags.bells_palsy:
118
+ from landmarkdiff.clinical import get_bells_palsy_side_indices
119
+ affected = get_bells_palsy_side_indices(clinical_flags.bells_palsy_side)
120
+ affected_indices = set()
121
+ for region_indices in affected.values():
122
+ affected_indices.update(region_indices)
123
+ handles = [h for h in handles if h.landmark_index not in affected_indices]
124
+
125
+ # Convert to pixel space for deformation
126
+ pixel_landmarks = landmarks.copy()
127
+ pixel_landmarks[:, 0] *= face.image_width
128
+ pixel_landmarks[:, 1] *= face.image_height
129
+
130
+ for handle in handles:
131
+ pixel_landmarks = gaussian_rbf_deform(pixel_landmarks, handle)
132
+
133
+ # Convert back to normalized
134
+ result = pixel_landmarks.copy()
135
+ result[:, 0] /= face.image_width
136
+ result[:, 1] /= face.image_height
137
+
138
+ return FaceLandmarks(
139
+ landmarks=result,
140
+ image_width=face.image_width,
141
+ image_height=face.image_height,
142
+ confidence=face.confidence,
143
+ )
144
+
145
+
146
+ def _get_procedure_handles(
147
+ procedure: str,
148
+ indices: list[int],
149
+ scale: float,
150
+ radius: float,
151
+ ) -> list[DeformationHandle]:
152
+ """Build deformation handles per procedure. 2D pixel displacements, calibrated at 512x512."""
153
+ handles = []
154
+
155
+ if procedure == "rhinoplasty":
156
+ # --- Alar base narrowing: move nostrils inward (toward midline) ---
157
+ # left nostril -> move RIGHT (+X)
158
+ left_alar = [240, 236, 141, 363, 370]
159
+ for idx in left_alar:
160
+ if idx in indices:
161
+ handles.append(DeformationHandle(
162
+ landmark_index=idx,
163
+ displacement=np.array([2.5 * scale, 0.0]),
164
+ influence_radius=radius * 0.6,
165
+ ))
166
+ # right nostril -> move LEFT (-X)
167
+ right_alar = [460, 456, 274, 275, 278, 279]
168
+ for idx in right_alar:
169
+ if idx in indices:
170
+ handles.append(DeformationHandle(
171
+ landmark_index=idx,
172
+ displacement=np.array([-2.5 * scale, 0.0]),
173
+ influence_radius=radius * 0.6,
174
+ ))
175
+
176
+ # --- Tip refinement: subtle upward rotation + narrowing ---
177
+ tip_indices = [1, 2, 94, 19]
178
+ for idx in tip_indices:
179
+ if idx in indices:
180
+ handles.append(DeformationHandle(
181
+ landmark_index=idx,
182
+ displacement=np.array([0.0, -2.0 * scale]),
183
+ influence_radius=radius * 0.5,
184
+ ))
185
+
186
+ # --- Dorsum narrowing: bilateral squeeze of nasal bridge ---
187
+ dorsum_left = [195, 197, 236]
188
+ for idx in dorsum_left:
189
+ if idx in indices:
190
+ handles.append(DeformationHandle(
191
+ landmark_index=idx,
192
+ displacement=np.array([1.5 * scale, 0.0]),
193
+ influence_radius=radius * 0.5,
194
+ ))
195
+ dorsum_right = [326, 327, 456]
196
+ for idx in dorsum_right:
197
+ if idx in indices:
198
+ handles.append(DeformationHandle(
199
+ landmark_index=idx,
200
+ displacement=np.array([-1.5 * scale, 0.0]),
201
+ influence_radius=radius * 0.5,
202
+ ))
203
+
204
+ elif procedure == "blepharoplasty":
205
+ # --- Upper lid elevation (primary effect) ---
206
+ upper_lid_left = [159, 160, 161] # central upper lid
207
+ upper_lid_right = [386, 385, 384]
208
+ for idx in upper_lid_left + upper_lid_right:
209
+ if idx in indices:
210
+ handles.append(DeformationHandle(
211
+ landmark_index=idx,
212
+ displacement=np.array([0.0, -2.0 * scale]),
213
+ influence_radius=radius,
214
+ ))
215
+ # --- Medial/lateral lid corners: less displacement (tapered) ---
216
+ corner_left = [158, 157, 133, 33]
217
+ corner_right = [387, 388, 362, 263]
218
+ for idx in corner_left + corner_right:
219
+ if idx in indices:
220
+ handles.append(DeformationHandle(
221
+ landmark_index=idx,
222
+ displacement=np.array([0.0, -0.8 * scale]),
223
+ influence_radius=radius * 0.7,
224
+ ))
225
+ # --- Subtle lower lid tightening ---
226
+ lower_lid_left = [145, 153, 154]
227
+ lower_lid_right = [374, 380, 381]
228
+ for idx in lower_lid_left + lower_lid_right:
229
+ if idx in indices:
230
+ handles.append(DeformationHandle(
231
+ landmark_index=idx,
232
+ displacement=np.array([0.0, 0.5 * scale]),
233
+ influence_radius=radius * 0.5,
234
+ ))
235
+
236
+ elif procedure == "rhytidectomy":
237
+ # Different displacement vectors by anatomical sub-region.
238
+ # Jowl area: strongest lift (upward + toward ear)
239
+ jowl_left = [132, 136, 172, 58, 150, 176]
240
+ for idx in jowl_left:
241
+ if idx in indices:
242
+ handles.append(DeformationHandle(
243
+ landmark_index=idx,
244
+ displacement=np.array([-2.5 * scale, -3.0 * scale]),
245
+ influence_radius=radius,
246
+ ))
247
+ jowl_right = [361, 365, 397, 288, 379, 400]
248
+ for idx in jowl_right:
249
+ if idx in indices:
250
+ handles.append(DeformationHandle(
251
+ landmark_index=idx,
252
+ displacement=np.array([2.5 * scale, -3.0 * scale]),
253
+ influence_radius=radius,
254
+ ))
255
+ # Chin/submental: upward only (no lateral)
256
+ chin = [152, 148, 377, 378]
257
+ for idx in chin:
258
+ if idx in indices:
259
+ handles.append(DeformationHandle(
260
+ landmark_index=idx,
261
+ displacement=np.array([0.0, -2.0 * scale]),
262
+ influence_radius=radius * 0.8,
263
+ ))
264
+ # Temple/upper face: very mild lift
265
+ temple_left = [10, 21, 54, 67, 103, 109, 162, 127]
266
+ temple_right = [284, 297, 332, 338, 323, 356, 389, 454]
267
+ for idx in temple_left:
268
+ if idx in indices:
269
+ handles.append(DeformationHandle(
270
+ landmark_index=idx,
271
+ displacement=np.array([-0.5 * scale, -1.0 * scale]),
272
+ influence_radius=radius * 0.6,
273
+ ))
274
+ for idx in temple_right:
275
+ if idx in indices:
276
+ handles.append(DeformationHandle(
277
+ landmark_index=idx,
278
+ displacement=np.array([0.5 * scale, -1.0 * scale]),
279
+ influence_radius=radius * 0.6,
280
+ ))
281
+
282
+ elif procedure == "orthognathic":
283
+ # --- Mandible repositioning: move jaw up and forward (visible as upward in 2D) ---
284
+ lower_jaw = [17, 18, 200, 201, 202, 204, 208, 211, 212, 214]
285
+ for idx in lower_jaw:
286
+ if idx in indices:
287
+ handles.append(DeformationHandle(
288
+ landmark_index=idx,
289
+ displacement=np.array([0.0, -3.0 * scale]),
290
+ influence_radius=radius,
291
+ ))
292
+ # --- Chin projection: move chin point forward/upward ---
293
+ chin_pts = [175, 170, 169, 167, 396]
294
+ for idx in chin_pts:
295
+ if idx in indices:
296
+ handles.append(DeformationHandle(
297
+ landmark_index=idx,
298
+ displacement=np.array([0.0, -2.0 * scale]),
299
+ influence_radius=radius * 0.7,
300
+ ))
301
+ # --- Lateral jaw: bilateral symmetric inward pull for narrowing ---
302
+ jaw_left = [57, 61, 78, 91, 95, 146, 181]
303
+ for idx in jaw_left:
304
+ if idx in indices:
305
+ handles.append(DeformationHandle(
306
+ landmark_index=idx,
307
+ displacement=np.array([1.5 * scale, -1.0 * scale]),
308
+ influence_radius=radius * 0.8,
309
+ ))
310
+ jaw_right = [291, 311, 312, 321, 324, 325, 375, 405]
311
+ for idx in jaw_right:
312
+ if idx in indices:
313
+ handles.append(DeformationHandle(
314
+ landmark_index=idx,
315
+ displacement=np.array([-1.5 * scale, -1.0 * scale]),
316
+ influence_radius=radius * 0.8,
317
+ ))
318
+
319
+ elif procedure == "brow_lift":
320
+ # --- Brow elevation ---
321
+ brow_left = [70, 63, 105, 66, 107]
322
+ brow_right = [300, 293, 334, 296, 336]
323
+
324
+ # Lateral brow often lifted more than medial
325
+ left_weights = [0.7, 0.8, 0.9, 1.0, 1.1]
326
+ for i, idx in enumerate(brow_left):
327
+ if idx in indices:
328
+ handles.append(DeformationHandle(
329
+ landmark_index=idx,
330
+ displacement=np.array([0.0, -4.0 * left_weights[i] * scale]),
331
+ influence_radius=radius,
332
+ ))
333
+
334
+ right_weights = [0.7, 0.8, 0.9, 1.0, 1.1]
335
+ for i, idx in enumerate(brow_right):
336
+ if idx in indices:
337
+ handles.append(DeformationHandle(
338
+ landmark_index=idx,
339
+ displacement=np.array([0.0, -4.0 * right_weights[i] * scale]),
340
+ influence_radius=radius,
341
+ ))
342
+
343
+ # --- Forehead smoothing / subtle lift ---
344
+ forehead = [9, 8, 10, 109, 67, 103, 338, 297, 332]
345
+ for idx in forehead:
346
+ if idx in indices:
347
+ handles.append(DeformationHandle(
348
+ landmark_index=idx,
349
+ displacement=np.array([0.0, -1.5 * scale]),
350
+ influence_radius=radius * 1.2,
351
+ ))
352
+ elif procedure == "mentoplasty":
353
+ # --- Chin tip advancement: move chin forward (upward in 2D) ---
354
+ chin_tip = [152, 175]
355
+ for idx in chin_tip:
356
+ if idx in indices:
357
+ handles.append(DeformationHandle(
358
+ landmark_index=idx,
359
+ displacement=np.array([0.0, -4.0 * scale]),
360
+ influence_radius=radius,
361
+ ))
362
+ # --- Lower chin contour: follow tip with softer displacement ---
363
+ lower_contour = [148, 149, 150, 176, 377]
364
+ for idx in lower_contour:
365
+ if idx in indices:
366
+ handles.append(DeformationHandle(
367
+ landmark_index=idx,
368
+ displacement=np.array([0.0, -2.5 * scale]),
369
+ influence_radius=radius * 0.8,
370
+ ))
371
+ # --- Jaw angles: minimal upward pull for natural transition ---
372
+ jaw_angles = [171, 396]
373
+ for idx in jaw_angles:
374
+ if idx in indices:
375
+ handles.append(DeformationHandle(
376
+ landmark_index=idx,
377
+ displacement=np.array([0.0, -1.0 * scale]),
378
+ influence_radius=radius * 0.6,
379
+ ))
380
+ return handles