Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,8 +6,9 @@ Pipeline:
|
|
| 6 |
3. 3-zone paste-back:
|
| 7 |
Zone A β inner mouth (teeth): AI result, light color correction
|
| 8 |
Zone B β lip tissue ring: AI shape + 90% color forced to original
|
| 9 |
-
Zone C β skin extension area:
|
| 10 |
4. Only teeth change β lip color, texture, skin all stay original
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
import os, io, base64, urllib.request, textwrap
|
|
@@ -101,10 +102,11 @@ def sample_face_tones(img_rgb, face_lm, W, H):
|
|
| 101 |
# ββ Build 3 zone masks (in bbox-local coords) βββββββββββββββββββββββββββββββββ
|
| 102 |
def build_zone_masks(H, W, shifted_outer, shifted_inner, lip_height):
|
| 103 |
"""
|
| 104 |
-
Returns three
|
| 105 |
-
zone_teeth β inner lip polygon (teeth area) β AI only
|
| 106 |
-
zone_lips β outer minus inner (lip tissue ring) β shape AI, color original
|
| 107 |
-
zone_skin β upward/downward extension
|
|
|
|
| 108 |
"""
|
| 109 |
# Outer polygon filled
|
| 110 |
outer_mask = np.zeros((H, W), np.uint8)
|
|
@@ -117,7 +119,7 @@ def build_zone_masks(H, W, shifted_outer, shifted_inner, lip_height):
|
|
| 117 |
# Lip tissue = outer minus inner
|
| 118 |
lip_tissue = cv2.subtract(outer_mask, inner_mask)
|
| 119 |
|
| 120 |
-
#
|
| 121 |
lx1 = int(shifted_outer[:, 0].min())
|
| 122 |
lx2 = int(shifted_outer[:, 0].max())
|
| 123 |
ly1 = int(shifted_outer[:, 1].min())
|
|
@@ -130,17 +132,34 @@ def build_zone_masks(H, W, shifted_outer, shifted_inner, lip_height):
|
|
| 130 |
ext_left = max(0, lx1 - 4)
|
| 131 |
ext_right = min(W, lx2 + 4)
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
skin_ext = cv2.subtract(skin_ext, outer_mask)
|
|
|
|
| 138 |
|
| 139 |
-
# Dilate
|
| 140 |
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 141 |
inner_mask = cv2.dilate(inner_mask, k, iterations=1)
|
| 142 |
lip_tissue = cv2.dilate(lip_tissue, k, iterations=1)
|
| 143 |
-
skin_ext = cv2.dilate(skin_ext, k, iterations=1)
|
| 144 |
|
| 145 |
return inner_mask, lip_tissue, skin_ext
|
| 146 |
|
|
@@ -248,8 +267,9 @@ def paste_back_3zone(original_rgb, generated_pil, bbox, face_lm, W, H, lip_heigh
|
|
| 248 |
Result: lips keep their original skin tone, texture, gradients.
|
| 249 |
|
| 250 |
Zone C β skin_ext (area outside lip polygon, inside the extended edit zone):
|
| 251 |
-
|
| 252 |
-
|
|
|
|
| 253 |
"""
|
| 254 |
x1, y1, x2, y2 = bbox
|
| 255 |
bw, bh = x2 - x1, y2 - y1
|
|
@@ -285,10 +305,11 @@ def paste_back_3zone(original_rgb, generated_pil, bbox, face_lm, W, H, lip_heigh
|
|
| 285 |
if mb_lips.any():
|
| 286 |
corrected_lab = lab_color_transfer(corrected_lab, orig_lab, mb_lips, 0.90)
|
| 287 |
|
| 288 |
-
# Zone C: skin extension β
|
|
|
|
| 289 |
mb_skin = zone_skin > 0
|
| 290 |
if mb_skin.any():
|
| 291 |
-
corrected_lab = lab_color_transfer(corrected_lab, orig_lab, mb_skin,
|
| 292 |
|
| 293 |
# Reconstruct corrected RGB
|
| 294 |
corrected_rgb = cv2.cvtColor(
|
|
@@ -296,15 +317,17 @@ def paste_back_3zone(original_rgb, generated_pil, bbox, face_lm, W, H, lip_heigh
|
|
| 296 |
)
|
| 297 |
|
| 298 |
# Build combined blend mask (all 3 zones together) for the final alpha blend
|
|
|
|
| 299 |
combined_mask = np.clip(
|
| 300 |
zone_teeth.astype(np.float32) +
|
| 301 |
zone_lips.astype(np.float32) +
|
| 302 |
zone_skin.astype(np.float32),
|
| 303 |
0, 255
|
| 304 |
)
|
|
|
|
| 305 |
if feather > 0:
|
| 306 |
-
k = feather *
|
| 307 |
-
combined_mask = cv2.GaussianBlur(combined_mask, (k, k), feather *
|
| 308 |
combined_mask = np.clip(combined_mask / 255.0, 0, 1)[:, :, np.newaxis]
|
| 309 |
|
| 310 |
# Alpha blend: inside combined zone β corrected_rgb; outside β original
|
|
|
|
| 6 |
3. 3-zone paste-back:
|
| 7 |
Zone A β inner mouth (teeth): AI result, light color correction
|
| 8 |
Zone B β lip tissue ring: AI shape + 90% color forced to original
|
| 9 |
+
Zone C β skin extension area: Gaussian falloff blend, 100% color original
|
| 10 |
4. Only teeth change β lip color, texture, skin all stay original
|
| 11 |
+
5. FIX: Gaussian distance falloff on skin zone eliminates horizontal seam lines
|
| 12 |
"""
|
| 13 |
|
| 14 |
import os, io, base64, urllib.request, textwrap
|
|
|
|
| 102 |
# ββ Build 3 zone masks (in bbox-local coords) βββββββββββββββββββββββββββββββββ
|
| 103 |
def build_zone_masks(H, W, shifted_outer, shifted_inner, lip_height):
|
| 104 |
"""
|
| 105 |
+
Returns three masks:
|
| 106 |
+
zone_teeth β inner lip polygon (teeth area) β AI only, uint8 hard mask
|
| 107 |
+
zone_lips β outer minus inner (lip tissue ring) β shape AI, color original, uint8
|
| 108 |
+
zone_skin β upward/downward extension with GAUSSIAN FALLOFF (float32 0-255)
|
| 109 |
+
eliminates the hard horizontal seam lines at the zone boundary
|
| 110 |
"""
|
| 111 |
# Outer polygon filled
|
| 112 |
outer_mask = np.zeros((H, W), np.uint8)
|
|
|
|
| 119 |
# Lip tissue = outer minus inner
|
| 120 |
lip_tissue = cv2.subtract(outer_mask, inner_mask)
|
| 121 |
|
| 122 |
+
# Bounding box of outer lip
|
| 123 |
lx1 = int(shifted_outer[:, 0].min())
|
| 124 |
lx2 = int(shifted_outer[:, 0].max())
|
| 125 |
ly1 = int(shifted_outer[:, 1].min())
|
|
|
|
| 132 |
ext_left = max(0, lx1 - 4)
|
| 133 |
ext_right = min(W, lx2 + 4)
|
| 134 |
|
| 135 |
+
# ββ FIX: Gaussian falloff instead of hard rectangular zone ββββββββββββ
|
| 136 |
+
# This removes the visible horizontal seam lines above/below lips.
|
| 137 |
+
# Influence drops exponentially with distance from the lip boundary.
|
| 138 |
+
skin_ext = np.zeros((H, W), np.float32)
|
| 139 |
+
|
| 140 |
+
# Above lips β falloff away from lip top edge (ly1)
|
| 141 |
+
sigma_up = max(1.0, upward_ext * 0.35)
|
| 142 |
+
for row in range(ext_top, ly1):
|
| 143 |
+
dist = ly1 - row # pixels above lip edge
|
| 144 |
+
alpha = np.exp(-(dist ** 2) / (2 * sigma_up ** 2))
|
| 145 |
+
skin_ext[row, ext_left:ext_right] = alpha * 255.0
|
| 146 |
+
|
| 147 |
+
# Below lips β falloff away from lip bottom edge (ly2)
|
| 148 |
+
sigma_dn = max(1.0, downward_ext * 0.5)
|
| 149 |
+
for row in range(ly2, ext_bot):
|
| 150 |
+
dist = row - ly2
|
| 151 |
+
alpha = np.exp(-(dist ** 2) / (2 * sigma_dn ** 2))
|
| 152 |
+
skin_ext[row, ext_left:ext_right] = alpha * 255.0
|
| 153 |
+
|
| 154 |
+
skin_ext = np.clip(skin_ext, 0, 255).astype(np.uint8)
|
| 155 |
+
# Remove any overlap with the lip polygon itself
|
| 156 |
skin_ext = cv2.subtract(skin_ext, outer_mask)
|
| 157 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
|
| 159 |
+
# Dilate teeth and lip tissue for smooth edges (skin_ext already soft, skip)
|
| 160 |
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 161 |
inner_mask = cv2.dilate(inner_mask, k, iterations=1)
|
| 162 |
lip_tissue = cv2.dilate(lip_tissue, k, iterations=1)
|
|
|
|
| 163 |
|
| 164 |
return inner_mask, lip_tissue, skin_ext
|
| 165 |
|
|
|
|
| 267 |
Result: lips keep their original skin tone, texture, gradients.
|
| 268 |
|
| 269 |
Zone C β skin_ext (area outside lip polygon, inside the extended edit zone):
|
| 270 |
+
Full original color (100% correction).
|
| 271 |
+
Uses Gaussian falloff mask β NO hard horizontal boundary line.
|
| 272 |
+
The seam is completely invisible.
|
| 273 |
"""
|
| 274 |
x1, y1, x2, y2 = bbox
|
| 275 |
bw, bh = x2 - x1, y2 - y1
|
|
|
|
| 305 |
if mb_lips.any():
|
| 306 |
corrected_lab = lab_color_transfer(corrected_lab, orig_lab, mb_lips, 0.90)
|
| 307 |
|
| 308 |
+
# Zone C: skin extension β 100% color correction β identical skin color
|
| 309 |
+
# (The soft Gaussian mask already handles the blend; full correction here)
|
| 310 |
mb_skin = zone_skin > 0
|
| 311 |
if mb_skin.any():
|
| 312 |
+
corrected_lab = lab_color_transfer(corrected_lab, orig_lab, mb_skin, 1.0)
|
| 313 |
|
| 314 |
# Reconstruct corrected RGB
|
| 315 |
corrected_rgb = cv2.cvtColor(
|
|
|
|
| 317 |
)
|
| 318 |
|
| 319 |
# Build combined blend mask (all 3 zones together) for the final alpha blend
|
| 320 |
+
# zone_skin already has Gaussian falloff baked in β no hard edge
|
| 321 |
combined_mask = np.clip(
|
| 322 |
zone_teeth.astype(np.float32) +
|
| 323 |
zone_lips.astype(np.float32) +
|
| 324 |
zone_skin.astype(np.float32),
|
| 325 |
0, 255
|
| 326 |
)
|
| 327 |
+
# Use a wider kernel for more aggressive feathering on the teeth/lip hard edges
|
| 328 |
if feather > 0:
|
| 329 |
+
k = feather * 4 + 1 # wider kernel: was feather*2+1
|
| 330 |
+
combined_mask = cv2.GaussianBlur(combined_mask, (k, k), feather * 1.5)
|
| 331 |
combined_mask = np.clip(combined_mask / 255.0, 0, 1)[:, :, np.newaxis]
|
| 332 |
|
| 333 |
# Alpha blend: inside combined zone β corrected_rgb; outside β original
|