dreamlessx commited on
Commit
6aad5c7
·
verified ·
1 Parent(s): c107618

Upload landmarkdiff/masking.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. landmarkdiff/masking.py +137 -0
landmarkdiff/masking.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Surgical mask gen - morphological dilation + Gaussian feather.
2
+
3
+ Procedural (not SAM2), deterministic, no model dependency.
4
+ Feathered edges prevent visible seams during inpainting.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import cv2
12
+ import numpy as np
13
+
14
+ from landmarkdiff.landmarks import FaceLandmarks, LANDMARK_REGIONS
15
+
16
+ if TYPE_CHECKING:
17
+ from landmarkdiff.clinical import ClinicalFlags
18
+
19
+ # Procedure-specific mask parameters
20
+ MASK_CONFIG: dict[str, dict] = {
21
+ "rhinoplasty": {
22
+ "landmark_indices": [
23
+ 1, 2, 4, 5, 6, 19, 94, 141, 168, 195, 197, 236, 240,
24
+ 274, 275, 278, 279, 294, 326, 327, 360, 363, 370, 456, 460,
25
+ ],
26
+ "dilation_px": 30,
27
+ "feather_sigma": 15.0,
28
+ },
29
+ "blepharoplasty": {
30
+ "landmark_indices": [
31
+ 33, 7, 163, 144, 145, 153, 154, 155, 157, 158, 159, 160, 161, 246,
32
+ 362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386,
33
+ 385, 384, 398,
34
+ ],
35
+ "dilation_px": 15,
36
+ "feather_sigma": 10.0,
37
+ },
38
+ "rhytidectomy": {
39
+ "landmark_indices": [
40
+ 10, 21, 54, 58, 67, 93, 103, 109, 127, 132, 136, 150, 162, 172,
41
+ 176, 187, 207, 213, 234, 284, 297, 323, 332, 338, 356, 361, 365,
42
+ 379, 389, 397, 400, 427, 454,
43
+ ],
44
+ "dilation_px": 40,
45
+ "feather_sigma": 20.0,
46
+ },
47
+ "orthognathic": {
48
+ "landmark_indices": [
49
+ 0, 17, 18, 36, 37, 39, 40, 57, 61, 78, 80, 81, 82, 84, 87, 88,
50
+ 91, 95, 146, 167, 169, 170, 175, 181, 191, 200, 201, 202, 204,
51
+ 208, 211, 212, 214,
52
+ ],
53
+ "dilation_px": 35,
54
+ "feather_sigma": 18.0,
55
+ },
56
+ }
57
+
58
+
59
+ def generate_surgical_mask(
60
+ face: FaceLandmarks,
61
+ procedure: str,
62
+ width: int | None = None,
63
+ height: int | None = None,
64
+ clinical_flags: "ClinicalFlags | None" = None,
65
+ image: np.ndarray | None = None,
66
+ ) -> np.ndarray:
67
+ """Convex hull -> dilate -> noise at boundary -> Gaussian feather. Returns float32 [0-1]."""
68
+ if procedure not in MASK_CONFIG:
69
+ raise ValueError(f"Unknown procedure: {procedure}. Choose from {list(MASK_CONFIG)}")
70
+
71
+ config = MASK_CONFIG[procedure]
72
+ w = width or face.image_width
73
+ h = height or face.image_height
74
+
75
+ # Get pixel coordinates of procedure landmarks
76
+ coords = face.landmarks[:, :2].copy()
77
+ coords[:, 0] *= w
78
+ coords[:, 1] *= h
79
+ pts = coords[config["landmark_indices"]].astype(np.int32)
80
+
81
+ # Create binary mask from convex hull
82
+ binary = np.zeros((h, w), dtype=np.uint8)
83
+ hull = cv2.convexHull(pts)
84
+ cv2.fillConvexPoly(binary, hull, 255)
85
+
86
+ # Morphological dilation
87
+ dilation = config["dilation_px"]
88
+ kernel = cv2.getStructuringElement(
89
+ cv2.MORPH_ELLIPSE,
90
+ (2 * dilation + 1, 2 * dilation + 1),
91
+ )
92
+ dilated = cv2.dilate(binary, kernel)
93
+
94
+ # Add slight boundary noise to prevent clean-edge seams
95
+ # (Spec: Perlin noise 2-4px on boundary before feathering)
96
+ boundary = cv2.subtract(
97
+ cv2.dilate(dilated, np.ones((5, 5), np.uint8)),
98
+ cv2.erode(dilated, np.ones((5, 5), np.uint8)),
99
+ )
100
+ noise = np.random.default_rng().integers(0, 4, size=(h, w), dtype=np.uint8)
101
+ noise_boundary = cv2.bitwise_and(boundary, noise.astype(np.uint8) * 64)
102
+ dilated = cv2.add(dilated, noise_boundary)
103
+ dilated = np.clip(dilated, 0, 255).astype(np.uint8)
104
+
105
+ # Gaussian feathering
106
+ sigma = config["feather_sigma"]
107
+ ksize = int(6 * sigma) | 1 # ensure odd
108
+ feathered = cv2.GaussianBlur(
109
+ dilated.astype(np.float32) / 255.0,
110
+ (ksize, ksize),
111
+ sigma,
112
+ )
113
+
114
+ mask = np.clip(feathered, 0.0, 1.0)
115
+
116
+ # Clinical edge case adjustments
117
+ if clinical_flags is not None:
118
+ # Vitiligo: reduce mask over depigmented patches to preserve them
119
+ if clinical_flags.vitiligo and image is not None:
120
+ from landmarkdiff.clinical import detect_vitiligo_patches, adjust_mask_for_vitiligo
121
+ patches = detect_vitiligo_patches(image, face)
122
+ mask = adjust_mask_for_vitiligo(mask, patches)
123
+
124
+ # Keloid: soften transitions in keloid-prone regions
125
+ if clinical_flags.keloid_prone and clinical_flags.keloid_regions:
126
+ from landmarkdiff.clinical import get_keloid_exclusion_mask, adjust_mask_for_keloid
127
+ keloid_mask = get_keloid_exclusion_mask(
128
+ face, clinical_flags.keloid_regions, w, h,
129
+ )
130
+ mask = adjust_mask_for_keloid(mask, keloid_mask)
131
+
132
+ return mask
133
+
134
+
135
+ def mask_to_3channel(mask: np.ndarray) -> np.ndarray:
136
+ """Convert single-channel mask to 3-channel for compositing."""
137
+ return np.stack([mask, mask, mask], axis=-1)