Spaces:
Sleeping
Sleeping
Update landmarkdiff/landmarks.py to v0.3.2
Browse files- landmarkdiff/landmarks.py +41 -3
landmarkdiff/landmarks.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from pathlib import Path
|
| 7 |
|
|
@@ -9,6 +10,8 @@ import cv2
|
|
| 9 |
import mediapipe as mp
|
| 10 |
import numpy as np
|
| 11 |
|
|
|
|
|
|
|
| 12 |
# Region color map for visualization (BGR)
|
| 13 |
REGION_COLORS: dict[str, tuple[int, int, int]] = {
|
| 14 |
"jawline": (255, 255, 255), # white
|
|
@@ -167,12 +170,45 @@ class FaceLandmarks:
|
|
| 167 |
|
| 168 |
@property
|
| 169 |
def pixel_coords(self) -> np.ndarray:
|
| 170 |
-
"""Convert normalized landmarks to pixel coordinates (478, 2).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
coords = self.landmarks[:, :2].copy()
|
| 172 |
coords[:, 0] *= self.image_width
|
| 173 |
coords[:, 1] *= self.image_height
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
return coords
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
def get_region(self, region: str) -> np.ndarray:
|
| 177 |
"""Get landmark indices for a named region."""
|
| 178 |
indices = LANDMARK_REGIONS.get(region, [])
|
|
@@ -201,11 +237,13 @@ def extract_landmarks(
|
|
| 201 |
try:
|
| 202 |
landmarks, confidence = _extract_tasks_api(rgb, min_detection_confidence)
|
| 203 |
except Exception:
|
|
|
|
| 204 |
try:
|
| 205 |
landmarks, confidence = _extract_solutions_api(
|
| 206 |
rgb, min_detection_confidence, min_tracking_confidence
|
| 207 |
)
|
| 208 |
except Exception:
|
|
|
|
| 209 |
return None
|
| 210 |
|
| 211 |
if landmarks is None:
|
|
@@ -338,7 +376,7 @@ def render_landmark_image(
|
|
| 338 |
"""Render MediaPipe face mesh tessellation on black canvas.
|
| 339 |
|
| 340 |
Draws the full 2556-edge tessellation mesh that CrucibleAI/ControlNetMediaPipeFace
|
| 341 |
-
was pre-trained on. This is critical
|
| 342 |
wireframes, not sparse dots.
|
| 343 |
|
| 344 |
Falls back to colored dots if tessellation connections aren't available.
|
|
@@ -380,7 +418,7 @@ def render_landmark_image(
|
|
| 380 |
p2 = tuple(pts[conn.end])
|
| 381 |
cv2.line(canvas, p1, p2, (255, 255, 255), 1, cv2.LINE_AA)
|
| 382 |
|
| 383 |
-
except ImportError:
|
| 384 |
# Fallback: draw colored dots if tessellation not available
|
| 385 |
idx_to_color: dict[int, tuple[int, int, int]] = {}
|
| 386 |
for region, indices in LANDMARK_REGIONS.items():
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import logging
|
| 6 |
from dataclasses import dataclass
|
| 7 |
from pathlib import Path
|
| 8 |
|
|
|
|
| 10 |
import mediapipe as mp
|
| 11 |
import numpy as np
|
| 12 |
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
# Region color map for visualization (BGR)
|
| 16 |
REGION_COLORS: dict[str, tuple[int, int, int]] = {
|
| 17 |
"jawline": (255, 255, 255), # white
|
|
|
|
| 170 |
|
| 171 |
@property
|
| 172 |
def pixel_coords(self) -> np.ndarray:
|
| 173 |
+
"""Convert normalized landmarks to pixel coordinates (478, 2).
|
| 174 |
+
|
| 175 |
+
Coordinates are clamped to valid image bounds so that extreme
|
| 176 |
+
head poses do not produce out-of-range indices.
|
| 177 |
+
"""
|
| 178 |
coords = self.landmarks[:, :2].copy()
|
| 179 |
coords[:, 0] *= self.image_width
|
| 180 |
coords[:, 1] *= self.image_height
|
| 181 |
+
coords[:, 0] = np.clip(coords[:, 0], 0, self.image_width - 1)
|
| 182 |
+
coords[:, 1] = np.clip(coords[:, 1], 0, self.image_height - 1)
|
| 183 |
+
return coords
|
| 184 |
+
|
| 185 |
+
def pixel_coords_at(self, width: int, height: int) -> np.ndarray:
|
| 186 |
+
"""Convert normalized landmarks to pixel coordinates at a given size.
|
| 187 |
+
|
| 188 |
+
Use this when the image has been resized after landmark extraction.
|
| 189 |
+
Coordinates are clamped to [0, width-1] x [0, height-1].
|
| 190 |
+
"""
|
| 191 |
+
coords = self.landmarks[:, :2].copy()
|
| 192 |
+
coords[:, 0] *= width
|
| 193 |
+
coords[:, 1] *= height
|
| 194 |
+
coords[:, 0] = np.clip(coords[:, 0], 0, width - 1)
|
| 195 |
+
coords[:, 1] = np.clip(coords[:, 1], 0, height - 1)
|
| 196 |
return coords
|
| 197 |
|
| 198 |
+
def rescale(self, width: int, height: int) -> FaceLandmarks:
|
| 199 |
+
"""Return a copy with updated image dimensions.
|
| 200 |
+
|
| 201 |
+
Landmarks stay in normalized [0,1] space; only the stored
|
| 202 |
+
width/height change, so ``pixel_coords`` returns values at
|
| 203 |
+
the new resolution.
|
| 204 |
+
"""
|
| 205 |
+
return FaceLandmarks(
|
| 206 |
+
landmarks=self.landmarks.copy(),
|
| 207 |
+
image_width=width,
|
| 208 |
+
image_height=height,
|
| 209 |
+
confidence=self.confidence,
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
def get_region(self, region: str) -> np.ndarray:
|
| 213 |
"""Get landmark indices for a named region."""
|
| 214 |
indices = LANDMARK_REGIONS.get(region, [])
|
|
|
|
| 237 |
try:
|
| 238 |
landmarks, confidence = _extract_tasks_api(rgb, min_detection_confidence)
|
| 239 |
except Exception:
|
| 240 |
+
logger.debug("Tasks API unavailable, trying Solutions API", exc_info=True)
|
| 241 |
try:
|
| 242 |
landmarks, confidence = _extract_solutions_api(
|
| 243 |
rgb, min_detection_confidence, min_tracking_confidence
|
| 244 |
)
|
| 245 |
except Exception:
|
| 246 |
+
logger.debug("Both MediaPipe APIs failed", exc_info=True)
|
| 247 |
return None
|
| 248 |
|
| 249 |
if landmarks is None:
|
|
|
|
| 376 |
"""Render MediaPipe face mesh tessellation on black canvas.
|
| 377 |
|
| 378 |
Draws the full 2556-edge tessellation mesh that CrucibleAI/ControlNetMediaPipeFace
|
| 379 |
+
was pre-trained on. This is critical -- the ControlNet expects dense triangulated
|
| 380 |
wireframes, not sparse dots.
|
| 381 |
|
| 382 |
Falls back to colored dots if tessellation connections aren't available.
|
|
|
|
| 418 |
p2 = tuple(pts[conn.end])
|
| 419 |
cv2.line(canvas, p1, p2, (255, 255, 255), 1, cv2.LINE_AA)
|
| 420 |
|
| 421 |
+
except (ImportError, AttributeError):
|
| 422 |
# Fallback: draw colored dots if tessellation not available
|
| 423 |
idx_to_color: dict[int, tuple[int, int, int]] = {}
|
| 424 |
for region, indices in LANDMARK_REGIONS.items():
|