Spaces:
Running
Running
Upload landmarkdiff/manipulation.py with huggingface_hub
Browse files- landmarkdiff/manipulation.py +63 -2
landmarkdiff/manipulation.py
CHANGED
|
@@ -95,15 +95,39 @@ def apply_procedure_preset(
|
|
| 95 |
intensity: float = 50.0,
|
| 96 |
image_size: int = 512,
|
| 97 |
clinical_flags: Optional["ClinicalFlags"] = None,
|
|
|
|
|
|
|
| 98 |
) -> FaceLandmarks:
|
| 99 |
-
"""Apply a
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
@@ -143,6 +167,43 @@ def apply_procedure_preset(
|
|
| 143 |
)
|
| 144 |
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
def _get_procedure_handles(
|
| 147 |
procedure: str,
|
| 148 |
indices: list[int],
|
|
|
|
| 95 |
intensity: float = 50.0,
|
| 96 |
image_size: int = 512,
|
| 97 |
clinical_flags: Optional["ClinicalFlags"] = None,
|
| 98 |
+
displacement_model_path: Optional[str] = None,
|
| 99 |
+
noise_scale: float = 0.0,
|
| 100 |
) -> FaceLandmarks:
|
| 101 |
+
"""Apply a surgical procedure preset to landmarks.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
face: Input face landmarks.
|
| 105 |
+
procedure: Procedure name (rhinoplasty, blepharoplasty, etc.).
|
| 106 |
+
intensity: Relative intensity 0-100 (mild=33, moderate=66, aggressive=100).
|
| 107 |
+
image_size: Reference image size for displacement scaling.
|
| 108 |
+
clinical_flags: Optional clinical condition flags.
|
| 109 |
+
displacement_model_path: Path to a fitted DisplacementModel (.npz).
|
| 110 |
+
When provided, uses data-driven displacements from real surgery pairs
|
| 111 |
+
instead of hand-tuned RBF vectors.
|
| 112 |
+
noise_scale: Variation noise scale for data-driven mode (0=deterministic).
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
New FaceLandmarks with manipulated landmarks.
|
| 116 |
+
"""
|
| 117 |
if procedure not in PROCEDURE_LANDMARKS:
|
| 118 |
raise ValueError(f"Unknown procedure: {procedure}. Choose from {list(PROCEDURE_LANDMARKS)}")
|
| 119 |
|
| 120 |
landmarks = face.landmarks.copy()
|
| 121 |
+
scale = intensity / 100.0
|
| 122 |
+
|
| 123 |
+
# Data-driven displacement mode
|
| 124 |
+
if displacement_model_path is not None:
|
| 125 |
+
return _apply_data_driven(
|
| 126 |
+
face, procedure, scale, displacement_model_path, noise_scale,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
indices = PROCEDURE_LANDMARKS[procedure]
|
| 130 |
radius = PROCEDURE_RADIUS[procedure]
|
|
|
|
| 131 |
|
| 132 |
# Ehlers-Danlos: wider influence radii for hypermobile tissue
|
| 133 |
if clinical_flags and clinical_flags.ehlers_danlos:
|
|
|
|
| 167 |
)
|
| 168 |
|
| 169 |
|
| 170 |
+
def _apply_data_driven(
|
| 171 |
+
face: FaceLandmarks,
|
| 172 |
+
procedure: str,
|
| 173 |
+
scale: float,
|
| 174 |
+
model_path: str,
|
| 175 |
+
noise_scale: float = 0.0,
|
| 176 |
+
) -> FaceLandmarks:
|
| 177 |
+
"""Apply data-driven displacements from a fitted DisplacementModel.
|
| 178 |
+
|
| 179 |
+
The model provides mean displacement vectors learned from real surgery pairs,
|
| 180 |
+
applied directly to all 478 landmarks (not just procedure-specific subset).
|
| 181 |
+
"""
|
| 182 |
+
from landmarkdiff.displacement_model import DisplacementModel
|
| 183 |
+
|
| 184 |
+
model = DisplacementModel.load(model_path)
|
| 185 |
+
field = model.get_displacement_field(
|
| 186 |
+
procedure=procedure,
|
| 187 |
+
intensity=scale,
|
| 188 |
+
noise_scale=noise_scale,
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# field is (478, 2) in normalized coordinates
|
| 192 |
+
landmarks = face.landmarks.copy()
|
| 193 |
+
n_lm = min(landmarks.shape[0], field.shape[0])
|
| 194 |
+
landmarks[:n_lm, :2] += field[:n_lm]
|
| 195 |
+
|
| 196 |
+
# Clamp to [0, 1]
|
| 197 |
+
landmarks = np.clip(landmarks, 0.0, 1.0)
|
| 198 |
+
|
| 199 |
+
return FaceLandmarks(
|
| 200 |
+
landmarks=landmarks,
|
| 201 |
+
image_width=face.image_width,
|
| 202 |
+
image_height=face.image_height,
|
| 203 |
+
confidence=face.confidence,
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
def _get_procedure_handles(
|
| 208 |
procedure: str,
|
| 209 |
indices: list[int],
|