dreamlessx commited on
Commit
a82aad5
·
verified ·
1 Parent(s): 16a86a4

Update landmarkdiff/landmarks.py to v0.3.2

Browse files
Files changed (1) hide show
  1. 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 the ControlNet expects dense triangulated
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():