dreamlessx commited on
Commit
cfdd827
·
verified ·
1 Parent(s): 1e18167

Upload landmarkdiff/conditioning.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. landmarkdiff/conditioning.py +135 -0
landmarkdiff/conditioning.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Conditioning signal: static adjacency wireframe + auto-Canny.
2
+
3
+ Static adjacency (not Delaunay) to avoid triangle inversion on big displacements.
4
+ Auto-Canny adapts thresholds to skin tone (Fitzpatrick I-VI safe).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import cv2
10
+ import numpy as np
11
+
12
+ from landmarkdiff.landmarks import FaceLandmarks
13
+
14
+ # Static anatomical adjacency for MediaPipe 478 landmarks.
15
+ # Connects landmarks along anatomically meaningful contours:
16
+ # jawline, nasal dorsum, orbital rim, lip vermilion, eyebrow arch.
17
+ # This is invariant to landmark displacement (unlike Delaunay).
18
+
19
+ JAWLINE_CONTOUR = [
20
+ 10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
21
+ 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
22
+ 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109, 10,
23
+ ]
24
+
25
+ LEFT_EYE_CONTOUR = [
26
+ 33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246, 33,
27
+ ]
28
+
29
+ RIGHT_EYE_CONTOUR = [
30
+ 362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398, 362,
31
+ ]
32
+
33
+ LEFT_EYEBROW = [70, 63, 105, 66, 107, 55, 65, 52, 53, 46]
34
+ RIGHT_EYEBROW = [300, 293, 334, 296, 336, 285, 295, 282, 283, 276]
35
+
36
+ NOSE_BRIDGE = [168, 6, 197, 195, 5, 4, 1]
37
+ NOSE_TIP = [94, 2, 326, 327, 294, 278, 279, 275, 274, 460, 456, 363, 370]
38
+ NOSE_BOTTOM = [19, 1, 274, 275, 440, 344, 278, 294, 460, 305, 289, 392, 289, 305, 460]
39
+
40
+ OUTER_LIPS = [
41
+ 61, 146, 91, 181, 84, 17, 314, 405, 321, 375, 291,
42
+ 308, 324, 318, 402, 317, 14, 87, 178, 88, 95, 78, 61,
43
+ ]
44
+
45
+ INNER_LIPS = [
46
+ 78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308,
47
+ 324, 318, 402, 317, 14, 87, 178, 88, 95, 78,
48
+ ]
49
+
50
+ FACE_OVAL = [
51
+ 10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
52
+ 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
53
+ 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109, 10,
54
+ ]
55
+
56
+ ALL_CONTOURS = [
57
+ JAWLINE_CONTOUR,
58
+ LEFT_EYE_CONTOUR,
59
+ RIGHT_EYE_CONTOUR,
60
+ LEFT_EYEBROW,
61
+ RIGHT_EYEBROW,
62
+ NOSE_BRIDGE,
63
+ NOSE_TIP,
64
+ OUTER_LIPS,
65
+ INNER_LIPS,
66
+ ]
67
+
68
+
69
+ def render_wireframe(
70
+ face: FaceLandmarks,
71
+ width: int | None = None,
72
+ height: int | None = None,
73
+ thickness: int = 1,
74
+ ) -> np.ndarray:
75
+ """Render static anatomical adjacency wireframe on black canvas."""
76
+ w = width or face.image_width
77
+ h = height or face.image_height
78
+ canvas = np.zeros((h, w), dtype=np.uint8)
79
+
80
+ coords = face.landmarks[:, :2].copy()
81
+ coords[:, 0] *= w
82
+ coords[:, 1] *= h
83
+ pts = coords.astype(np.int32)
84
+
85
+ for contour in ALL_CONTOURS:
86
+ for i in range(len(contour) - 1):
87
+ p1 = tuple(pts[contour[i]])
88
+ p2 = tuple(pts[contour[i + 1]])
89
+ cv2.line(canvas, p1, p2, 255, thickness)
90
+
91
+ return canvas
92
+
93
+
94
+ def auto_canny(image: np.ndarray) -> np.ndarray:
95
+ """Auto-Canny edge detection with adaptive thresholds."""
96
+ median = np.median(image[image > 0]) if np.any(image > 0) else 128.0
97
+ low = int(max(0, 0.66 * median))
98
+ high = int(min(255, 1.33 * median))
99
+
100
+ edges = cv2.Canny(image, low, high)
101
+
102
+ # Morphological skeletonization for guaranteed 1-pixel thickness
103
+ # ControlNet blurs on 2+ pixel edges
104
+ skeleton = np.zeros_like(edges)
105
+ element = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
106
+ temp = edges.copy()
107
+
108
+ while True:
109
+ eroded = cv2.erode(temp, element)
110
+ dilated = cv2.dilate(eroded, element)
111
+ diff = cv2.subtract(temp, dilated)
112
+ skeleton = cv2.bitwise_or(skeleton, diff)
113
+ temp = eroded.copy()
114
+ if cv2.countNonZero(temp) == 0:
115
+ break
116
+
117
+ return skeleton
118
+
119
+
120
+ def generate_conditioning(
121
+ face: FaceLandmarks,
122
+ width: int | None = None,
123
+ height: int | None = None,
124
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
125
+ """Generate full conditioning signal for ControlNet."""
126
+ from landmarkdiff.landmarks import render_landmark_image
127
+
128
+ w = width or face.image_width
129
+ h = height or face.image_height
130
+
131
+ landmark_img = render_landmark_image(face, w, h)
132
+ wireframe = render_wireframe(face, w, h)
133
+ canny = auto_canny(wireframe)
134
+
135
+ return landmark_img, canny, wireframe