dreamlessx commited on
Commit
da3a0ac
·
verified ·
1 Parent(s): 73fad7a

Upload landmarkdiff/landmarks.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. landmarkdiff/landmarks.py +51 -14
landmarkdiff/landmarks.py CHANGED
@@ -1,4 +1,4 @@
1
- """MediaPipe Face Mesh v2 landmark extraction."""
2
 
3
  from __future__ import annotations
4
 
@@ -21,7 +21,6 @@ REGION_COLORS: dict[str, tuple[int, int, int]] = {
21
  "lips": (0, 0, 255), # red
22
  "iris_left": (255, 0, 255), # magenta
23
  "iris_right": (255, 0, 255),
24
- "face_oval": (200, 200, 200), # light gray
25
  }
26
 
27
  # MediaPipe landmark index groups by anatomical region
@@ -54,7 +53,7 @@ LANDMARK_REGIONS: dict[str, list[int]] = {
54
 
55
  @dataclass(frozen=True)
56
  class FaceLandmarks:
57
- """478 face landmarks + image size + detection confidence."""
58
 
59
  landmarks: np.ndarray # (478, 3) normalized (x, y, z)
60
  image_width: int
@@ -63,14 +62,14 @@ class FaceLandmarks:
63
 
64
  @property
65
  def pixel_coords(self) -> np.ndarray:
66
- """Normalized -> pixel coords, shape (478, 2)."""
67
  coords = self.landmarks[:, :2].copy()
68
  coords[:, 0] *= self.image_width
69
  coords[:, 1] *= self.image_height
70
  return coords
71
 
72
  def get_region(self, region: str) -> np.ndarray:
73
- """Return landmarks for the given region name."""
74
  indices = LANDMARK_REGIONS.get(region, [])
75
  return self.landmarks[indices]
76
 
@@ -80,11 +79,20 @@ def extract_landmarks(
80
  min_detection_confidence: float = 0.5,
81
  min_tracking_confidence: float = 0.5,
82
  ) -> Optional[FaceLandmarks]:
83
- """Run MediaPipe Face Mesh on a BGR image, return FaceLandmarks or None."""
 
 
 
 
 
 
 
 
 
84
  h, w = image.shape[:2]
85
  rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
86
 
87
- # Tasks API first, fall back to legacy solutions API
88
  try:
89
  landmarks, confidence = _extract_tasks_api(rgb, min_detection_confidence)
90
  except Exception:
@@ -108,7 +116,7 @@ def _extract_tasks_api(
108
  rgb: np.ndarray,
109
  min_confidence: float,
110
  ) -> tuple[Optional[np.ndarray], float]:
111
- """Tasks API path (mediapipe >= 0.10.20)."""
112
  FaceLandmarker = mp.tasks.vision.FaceLandmarker
113
  FaceLandmarkerOptions = mp.tasks.vision.FaceLandmarkerOptions
114
  RunningMode = mp.tasks.vision.RunningMode
@@ -144,7 +152,9 @@ def _extract_tasks_api(
144
  dtype=np.float32,
145
  )
146
 
147
- return landmarks, min_confidence
 
 
148
 
149
 
150
  def _extract_solutions_api(
@@ -152,7 +162,7 @@ def _extract_solutions_api(
152
  min_detection_confidence: float,
153
  min_tracking_confidence: float,
154
  ) -> tuple[Optional[np.ndarray], float]:
155
- """Legacy solutions API fallback."""
156
  with mp.solutions.face_mesh.FaceMesh(
157
  static_image_mode=True,
158
  max_num_faces=1,
@@ -170,7 +180,8 @@ def _extract_solutions_api(
170
  [(lm.x, lm.y, lm.z) for lm in face.landmark],
171
  dtype=np.float32,
172
  )
173
- return landmarks, min(min_detection_confidence, min_tracking_confidence)
 
174
 
175
 
176
  def visualize_landmarks(
@@ -179,7 +190,17 @@ def visualize_landmarks(
179
  radius: int = 1,
180
  draw_regions: bool = True,
181
  ) -> np.ndarray:
182
- """Draw colored landmark dots on a copy of the image."""
 
 
 
 
 
 
 
 
 
 
183
  canvas = image.copy()
184
  coords = face.pixel_coords
185
 
@@ -207,7 +228,23 @@ def render_landmark_image(
207
  height: Optional[int] = None,
208
  radius: int = 2,
209
  ) -> np.ndarray:
210
- """Render tessellation mesh on black canvas. Falls back to dots if no connections."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  w = width or face.image_width
212
  h = height or face.image_height
213
  canvas = np.zeros((h, w, 3), dtype=np.uint8)
@@ -251,7 +288,7 @@ def render_landmark_image(
251
 
252
 
253
  def load_image(path: str | Path) -> np.ndarray:
254
- """Load image as BGR numpy array, raises FileNotFoundError on failure."""
255
  img = cv2.imread(str(path))
256
  if img is None:
257
  raise FileNotFoundError(f"Could not load image: {path}")
 
1
+ """Facial landmark extraction using MediaPipe Face Mesh v2."""
2
 
3
  from __future__ import annotations
4
 
 
21
  "lips": (0, 0, 255), # red
22
  "iris_left": (255, 0, 255), # magenta
23
  "iris_right": (255, 0, 255),
 
24
  }
25
 
26
  # MediaPipe landmark index groups by anatomical region
 
53
 
54
  @dataclass(frozen=True)
55
  class FaceLandmarks:
56
+ """Extracted facial landmarks with metadata."""
57
 
58
  landmarks: np.ndarray # (478, 3) normalized (x, y, z)
59
  image_width: int
 
62
 
63
  @property
64
  def pixel_coords(self) -> np.ndarray:
65
+ """Convert normalized landmarks to pixel coordinates (478, 2)."""
66
  coords = self.landmarks[:, :2].copy()
67
  coords[:, 0] *= self.image_width
68
  coords[:, 1] *= self.image_height
69
  return coords
70
 
71
  def get_region(self, region: str) -> np.ndarray:
72
+ """Get landmark indices for a named region."""
73
  indices = LANDMARK_REGIONS.get(region, [])
74
  return self.landmarks[indices]
75
 
 
79
  min_detection_confidence: float = 0.5,
80
  min_tracking_confidence: float = 0.5,
81
  ) -> Optional[FaceLandmarks]:
82
+ """Extract 478 facial landmarks from an image using MediaPipe Face Mesh.
83
+
84
+ Args:
85
+ image: BGR image as numpy array.
86
+ min_detection_confidence: Minimum face detection confidence.
87
+ min_tracking_confidence: Minimum landmark tracking confidence.
88
+
89
+ Returns:
90
+ FaceLandmarks if a face is detected, None otherwise.
91
+ """
92
  h, w = image.shape[:2]
93
  rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
94
 
95
+ # Try new Tasks API first (mediapipe >= 0.10.20), fall back to legacy solutions API
96
  try:
97
  landmarks, confidence = _extract_tasks_api(rgb, min_detection_confidence)
98
  except Exception:
 
116
  rgb: np.ndarray,
117
  min_confidence: float,
118
  ) -> tuple[Optional[np.ndarray], float]:
119
+ """Extract landmarks using MediaPipe Tasks API (>= 0.10.20)."""
120
  FaceLandmarker = mp.tasks.vision.FaceLandmarker
121
  FaceLandmarkerOptions = mp.tasks.vision.FaceLandmarkerOptions
122
  RunningMode = mp.tasks.vision.RunningMode
 
152
  dtype=np.float32,
153
  )
154
 
155
+ # MediaPipe Tasks API doesn't expose per-landmark detection confidence;
156
+ # return 1.0 to indicate successful detection
157
+ return landmarks, 1.0
158
 
159
 
160
  def _extract_solutions_api(
 
162
  min_detection_confidence: float,
163
  min_tracking_confidence: float,
164
  ) -> tuple[Optional[np.ndarray], float]:
165
+ """Extract landmarks using legacy MediaPipe Solutions API."""
166
  with mp.solutions.face_mesh.FaceMesh(
167
  static_image_mode=True,
168
  max_num_faces=1,
 
180
  [(lm.x, lm.y, lm.z) for lm in face.landmark],
181
  dtype=np.float32,
182
  )
183
+ # Legacy API doesn't expose detection confidence; return 1.0 for success
184
+ return landmarks, 1.0
185
 
186
 
187
  def visualize_landmarks(
 
190
  radius: int = 1,
191
  draw_regions: bool = True,
192
  ) -> np.ndarray:
193
+ """Draw colored landmark dots on image by anatomical region.
194
+
195
+ Args:
196
+ image: BGR image to draw on (will be copied).
197
+ face: Extracted face landmarks.
198
+ radius: Dot radius in pixels.
199
+ draw_regions: If True, color by region. Otherwise all white.
200
+
201
+ Returns:
202
+ Annotated image copy.
203
+ """
204
  canvas = image.copy()
205
  coords = face.pixel_coords
206
 
 
228
  height: Optional[int] = None,
229
  radius: int = 2,
230
  ) -> np.ndarray:
231
+ """Render MediaPipe face mesh tessellation on black canvas.
232
+
233
+ Draws the full 2556-edge tessellation mesh that CrucibleAI/ControlNetMediaPipeFace
234
+ was pre-trained on. This is critical — the ControlNet expects dense triangulated
235
+ wireframes, not sparse dots.
236
+
237
+ Falls back to colored dots if tessellation connections aren't available.
238
+
239
+ Args:
240
+ face: Extracted face landmarks.
241
+ width: Canvas width (defaults to face.image_width).
242
+ height: Canvas height (defaults to face.image_height).
243
+ radius: Dot radius (used for key landmark dots overlay).
244
+
245
+ Returns:
246
+ BGR image with face mesh on black background.
247
+ """
248
  w = width or face.image_width
249
  h = height or face.image_height
250
  canvas = np.zeros((h, w, 3), dtype=np.uint8)
 
288
 
289
 
290
  def load_image(path: str | Path) -> np.ndarray:
291
+ """Load an image from disk as BGR numpy array."""
292
  img = cv2.imread(str(path))
293
  if img is None:
294
  raise FileNotFoundError(f"Could not load image: {path}")