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

Upload landmarkdiff/clinical.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. landmarkdiff/clinical.py +159 -0
landmarkdiff/clinical.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Clinical edge cases: vitiligo, Bell's palsy, keloid, Ehlers-Danlos.
2
+
3
+ Each condition modifies the pipeline differently (mask exclusion,
4
+ asymmetric deformation, wider radii, etc).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+ import cv2
12
+ import numpy as np
13
+
14
+ from landmarkdiff.landmarks import FaceLandmarks
15
+
16
+
17
+ @dataclass
18
+ class ClinicalFlags:
19
+ """Flags that change how the pipeline handles this patient."""
20
+
21
+ vitiligo: bool = False
22
+ bells_palsy: bool = False
23
+ bells_palsy_side: str = "left" # affected side: "left" or "right"
24
+ keloid_prone: bool = False
25
+ keloid_regions: list[str] = field(default_factory=list) # e.g. ["jawline", "nose"]
26
+ ehlers_danlos: bool = False
27
+
28
+ def has_any(self) -> bool:
29
+ return self.vitiligo or self.bells_palsy or self.keloid_prone or self.ehlers_danlos
30
+
31
+
32
+ def detect_vitiligo_patches(
33
+ image: np.ndarray,
34
+ face: FaceLandmarks,
35
+ l_threshold: float = 85.0,
36
+ min_patch_area: int = 200,
37
+ ) -> np.ndarray:
38
+ """Detect depigmented (vitiligo) patches on face using LAB luminance."""
39
+ h, w = image.shape[:2]
40
+ lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB).astype(np.float32)
41
+
42
+ # Create face ROI mask from landmarks
43
+ coords = face.pixel_coords.astype(np.int32)
44
+ hull = cv2.convexHull(coords)
45
+ face_mask = np.zeros((h, w), dtype=np.uint8)
46
+ cv2.fillConvexPoly(face_mask, hull, 255)
47
+
48
+ # Get face-region luminance statistics
49
+ l_channel = lab[:, :, 0]
50
+ face_pixels = l_channel[face_mask > 0]
51
+ if len(face_pixels) == 0:
52
+ return np.zeros((h, w), dtype=np.uint8)
53
+
54
+ l_mean = np.mean(face_pixels)
55
+ l_std = np.std(face_pixels)
56
+
57
+ # Vitiligo patches: significantly brighter than mean skin
58
+ threshold = min(l_threshold, l_mean + 2.0 * l_std)
59
+ bright_mask = ((l_channel > threshold) & (face_mask > 0)).astype(np.uint8) * 255
60
+
61
+ # Also check for low saturation (a,b channels close to 128)
62
+ a_channel = lab[:, :, 1]
63
+ b_channel = lab[:, :, 2]
64
+ low_sat = (
65
+ (np.abs(a_channel - 128) < 15) & (np.abs(b_channel - 128) < 15)
66
+ ).astype(np.uint8) * 255
67
+
68
+ # Combined: bright AND low-saturation within face
69
+ vitiligo_raw = cv2.bitwise_and(bright_mask, low_sat)
70
+
71
+ # Filter small noise patches
72
+ contours, _ = cv2.findContours(vitiligo_raw, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
73
+ result = np.zeros((h, w), dtype=np.uint8)
74
+ for cnt in contours:
75
+ if cv2.contourArea(cnt) >= min_patch_area:
76
+ cv2.fillPoly(result, [cnt], 255)
77
+
78
+ return result
79
+
80
+
81
+ def adjust_mask_for_vitiligo(
82
+ mask: np.ndarray,
83
+ vitiligo_patches: np.ndarray,
84
+ preservation_factor: float = 0.3,
85
+ ) -> np.ndarray:
86
+ """Reduce mask intensity over vitiligo patches to preserve them."""
87
+ patches_f = vitiligo_patches.astype(np.float32) / 255.0
88
+ reduction = patches_f * preservation_factor
89
+ return np.clip(mask - reduction, 0.0, 1.0)
90
+
91
+
92
+ def get_bells_palsy_side_indices(
93
+ side: str,
94
+ ) -> dict[str, list[int]]:
95
+ """Get landmark indices for the affected side in Bell's palsy."""
96
+ if side == "left":
97
+ return {
98
+ "eye": [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246],
99
+ "eyebrow": [70, 63, 105, 66, 107, 55, 65, 52, 53, 46],
100
+ "mouth_corner": [61, 146, 91, 181, 84],
101
+ "jawline": [132, 136, 172, 58, 150, 176, 148, 149],
102
+ }
103
+ else:
104
+ return {
105
+ "eye": [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398],
106
+ "eyebrow": [300, 293, 334, 296, 336, 285, 295, 282, 283, 276],
107
+ "mouth_corner": [291, 308, 324, 318, 402],
108
+ "jawline": [361, 365, 397, 288, 379, 400, 377, 378],
109
+ }
110
+
111
+
112
+ def get_keloid_exclusion_mask(
113
+ face: FaceLandmarks,
114
+ regions: list[str],
115
+ width: int,
116
+ height: int,
117
+ margin_px: int = 10,
118
+ ) -> np.ndarray:
119
+ """Generate mask of keloid-prone regions to exclude from aggressive compositing."""
120
+ from landmarkdiff.landmarks import LANDMARK_REGIONS
121
+
122
+ mask = np.zeros((height, width), dtype=np.float32)
123
+ coords = face.pixel_coords.astype(np.int32)
124
+
125
+ for region in regions:
126
+ indices = LANDMARK_REGIONS.get(region, [])
127
+ if not indices:
128
+ continue
129
+ pts = coords[indices]
130
+ hull = cv2.convexHull(pts)
131
+ cv2.fillConvexPoly(mask, hull, 1.0)
132
+
133
+ # Dilate by margin
134
+ if margin_px > 0:
135
+ kernel = cv2.getStructuringElement(
136
+ cv2.MORPH_ELLIPSE, (2 * margin_px + 1, 2 * margin_px + 1)
137
+ )
138
+ mask = cv2.dilate(mask, kernel)
139
+
140
+ return np.clip(mask, 0.0, 1.0)
141
+
142
+
143
+ def adjust_mask_for_keloid(
144
+ mask: np.ndarray,
145
+ keloid_mask: np.ndarray,
146
+ reduction_factor: float = 0.5,
147
+ ) -> np.ndarray:
148
+ """Soften mask transitions in keloid-prone areas."""
149
+ # Reduce mask intensity in keloid-prone areas
150
+ keloid_reduction = keloid_mask * reduction_factor
151
+ modified = mask * (1.0 - keloid_reduction)
152
+
153
+ # Extra Gaussian blur in keloid regions for softer transitions
154
+ blur_kernel = 31
155
+ blurred = cv2.GaussianBlur(modified, (blur_kernel, blur_kernel), 10.0)
156
+
157
+ # Use blurred version only in keloid regions
158
+ result = modified * (1.0 - keloid_mask) + blurred * keloid_mask
159
+ return np.clip(result, 0.0, 1.0)