Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -106,23 +106,27 @@ def get_hair_and_exclude_masks(pil_image: Image.Image):
|
|
| 106 |
|
| 107 |
hair = cv2.resize(hair, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 108 |
|
| 109 |
-
# ======================
|
| 110 |
exclude = np.zeros((128, 128), dtype=np.float32)
|
| 111 |
-
# Mouth / lips classes
|
| 112 |
-
|
| 113 |
-
exclude = np.maximum(exclude, (probs[
|
| 114 |
-
exclude = np.maximum(exclude, (probs[
|
| 115 |
-
|
| 116 |
-
# Eyes protection (optional but safe)
|
| 117 |
exclude = np.maximum(exclude, (probs[4] > 0.4).cpu().numpy().astype(np.float32)) # left eye
|
| 118 |
exclude = np.maximum(exclude, (probs[5] > 0.4).cpu().numpy().astype(np.float32)) # right eye
|
| 119 |
-
# Original nose (class 2) – keep as is (beard can overlap)
|
| 120 |
-
# But we already have exclude from lips/eyes only.
|
| 121 |
|
| 122 |
exclude = cv2.resize(exclude, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST)
|
| 123 |
exclude = cv2.dilate(exclude, kernel, iterations=1)
|
| 124 |
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
@timed("Beard Mask")
|
| 128 |
def get_beard_mask_fast(pil_image: Image.Image, exclude_mask: np.ndarray):
|
|
@@ -131,12 +135,11 @@ def get_beard_mask_fast(pil_image: Image.Image, exclude_mask: np.ndarray):
|
|
| 131 |
img_small = pil_image.resize((128, 128), Image.BILINEAR)
|
| 132 |
img_array = np.array(img_small)
|
| 133 |
|
| 134 |
-
# ====================== LOWER CONFIDENCE FOR MORE DETECTION ======================
|
| 135 |
results = model.predict(
|
| 136 |
img_array,
|
| 137 |
device=DEVICE.type,
|
| 138 |
-
conf=0.20, #
|
| 139 |
-
iou=0.45,
|
| 140 |
imgsz=128,
|
| 141 |
half=(DEVICE.type == "cuda"),
|
| 142 |
verbose=False,
|
|
@@ -149,22 +152,20 @@ def get_beard_mask_fast(pil_image: Image.Image, exclude_mask: np.ndarray):
|
|
| 149 |
if int(cls) == 0: # beard class
|
| 150 |
m = results[0].masks.data[i].cpu().numpy()
|
| 151 |
m = cv2.resize(m, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 152 |
-
|
| 153 |
-
mask = np.maximum(mask, (m > 0.35).astype(np.float32)) # was 0.42
|
| 154 |
|
| 155 |
-
# Remove
|
| 156 |
mask = np.maximum(mask - exclude_mask, 0)
|
| 157 |
|
| 158 |
if mask.sum() > 30:
|
| 159 |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
|
| 160 |
-
|
| 161 |
-
mask = cv2.dilate(mask, kernel, iterations=1) # NEW: expand beard coverage
|
| 162 |
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
|
| 163 |
mask = cv2.GaussianBlur(mask, (3,3), 0.8)
|
| 164 |
|
| 165 |
return mask
|
| 166 |
|
| 167 |
-
# ====================== COLOR TRANSFER
|
| 168 |
@timed("Color Transfer")
|
| 169 |
def apply_strong_grey_hair(image: Image.Image, hair_mask: np.ndarray, beard_mask: np.ndarray):
|
| 170 |
comb = np.maximum(hair_mask, beard_mask)
|
|
@@ -175,13 +176,9 @@ def apply_strong_grey_hair(image: Image.Image, hair_mask: np.ndarray, beard_mask
|
|
| 175 |
orig_lab = cv2.cvtColor((img * 255).astype(np.uint8), cv2.COLOR_RGB2LAB).astype(np.float32)
|
| 176 |
hsv = cv2.cvtColor((img * 255).astype(np.uint8), cv2.COLOR_RGB2HSV).astype(np.float32)
|
| 177 |
|
| 178 |
-
#
|
| 179 |
hsv_hair = hsv.copy()
|
| 180 |
-
|
| 181 |
-
# Desaturation (grey effect)
|
| 182 |
hsv_hair[..., 1] = hsv_hair[..., 1] * (1 - 0.78 * hair_mask)
|
| 183 |
-
|
| 184 |
-
# Balanced Brightness
|
| 185 |
original_v = hsv[..., 2]
|
| 186 |
boost_amount = 89 * hair_mask
|
| 187 |
hsv_hair[..., 2] = np.clip(
|
|
@@ -200,7 +197,7 @@ def apply_strong_grey_hair(image: Image.Image, hair_mask: np.ndarray, beard_mask
|
|
| 200 |
mean_hair = np.array([130., 0., 0.])
|
| 201 |
std_hair = np.array([30., 10., 10.])
|
| 202 |
|
| 203 |
-
# Beard
|
| 204 |
beard_bin = beard_mask > 0.5
|
| 205 |
if np.sum(beard_bin) > 30:
|
| 206 |
beard_pix = orig_lab[beard_bin]
|
|
@@ -219,12 +216,9 @@ def apply_strong_grey_hair(image: Image.Image, hair_mask: np.ndarray, beard_mask
|
|
| 219 |
final = hair_grey * hair_mask_3ch + final * (1 - hair_mask_3ch)
|
| 220 |
final = final * comb_3ch + img * (1 - comb_3ch)
|
| 221 |
|
| 222 |
-
# Soft cool tint
|
| 223 |
final = final + (np.array([9, 7, 5], dtype=np.float32)/255.0 * comb[..., None] * 0.18)
|
| 224 |
-
|
| 225 |
final = np.clip(final * 255, 0, 255).astype(np.uint8)
|
| 226 |
result = Image.fromarray(final)
|
| 227 |
-
|
| 228 |
result = result.filter(ImageFilter.UnsharpMask(radius=0.7, percent=70, threshold=2))
|
| 229 |
|
| 230 |
return result
|
|
@@ -241,9 +235,12 @@ def process_face_whitening(input_image: Image.Image):
|
|
| 241 |
|
| 242 |
img_resized = orig.resize((target, target), Image.BILINEAR)
|
| 243 |
|
| 244 |
-
hair_mask, exclude_mask = get_hair_and_exclude_masks(img_resized)
|
| 245 |
beard_mask = get_beard_mask_fast(img_resized, exclude_mask)
|
| 246 |
|
|
|
|
|
|
|
|
|
|
| 247 |
final_resized = apply_strong_grey_hair(img_resized, hair_mask, beard_mask)
|
| 248 |
|
| 249 |
final_img = final_resized.resize((ow, oh), Image.LANCZOS)
|
|
|
|
| 106 |
|
| 107 |
hair = cv2.resize(hair, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 108 |
|
| 109 |
+
# ====================== EXCLUDE MASK (LIPS + EYES) ======================
|
| 110 |
exclude = np.zeros((128, 128), dtype=np.float32)
|
| 111 |
+
# Mouth / lips classes
|
| 112 |
+
exclude = np.maximum(exclude, (probs[10] > 0.4).cpu().numpy().astype(np.float32)) # mouth
|
| 113 |
+
exclude = np.maximum(exclude, (probs[11] > 0.4).cpu().numpy().astype(np.float32)) # upper lip
|
| 114 |
+
exclude = np.maximum(exclude, (probs[12] > 0.4).cpu().numpy().astype(np.float32)) # lower lip
|
| 115 |
+
# Eyes protection
|
|
|
|
| 116 |
exclude = np.maximum(exclude, (probs[4] > 0.4).cpu().numpy().astype(np.float32)) # left eye
|
| 117 |
exclude = np.maximum(exclude, (probs[5] > 0.4).cpu().numpy().astype(np.float32)) # right eye
|
|
|
|
|
|
|
| 118 |
|
| 119 |
exclude = cv2.resize(exclude, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST)
|
| 120 |
exclude = cv2.dilate(exclude, kernel, iterations=1)
|
| 121 |
|
| 122 |
+
# ====================== MUSTACHE MASK (from upper lip region) ======================
|
| 123 |
+
mustache = (probs[11].cpu().numpy() > 0.35).astype(np.float32) # upper lip as mustache
|
| 124 |
+
mustache = cv2.resize(mustache, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 125 |
+
mustache = cv2.dilate(mustache, kernel, iterations=1)
|
| 126 |
+
# Remove overlap with exclude (lips) to avoid coloring lips
|
| 127 |
+
mustache = np.maximum(mustache - exclude, 0)
|
| 128 |
+
|
| 129 |
+
return hair, exclude, mustache
|
| 130 |
|
| 131 |
@timed("Beard Mask")
|
| 132 |
def get_beard_mask_fast(pil_image: Image.Image, exclude_mask: np.ndarray):
|
|
|
|
| 135 |
img_small = pil_image.resize((128, 128), Image.BILINEAR)
|
| 136 |
img_array = np.array(img_small)
|
| 137 |
|
|
|
|
| 138 |
results = model.predict(
|
| 139 |
img_array,
|
| 140 |
device=DEVICE.type,
|
| 141 |
+
conf=0.20, # lower confidence for more beard
|
| 142 |
+
iou=0.45,
|
| 143 |
imgsz=128,
|
| 144 |
half=(DEVICE.type == "cuda"),
|
| 145 |
verbose=False,
|
|
|
|
| 152 |
if int(cls) == 0: # beard class
|
| 153 |
m = results[0].masks.data[i].cpu().numpy()
|
| 154 |
m = cv2.resize(m, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 155 |
+
mask = np.maximum(mask, (m > 0.35).astype(np.float32))
|
|
|
|
| 156 |
|
| 157 |
+
# Remove lips/eyes
|
| 158 |
mask = np.maximum(mask - exclude_mask, 0)
|
| 159 |
|
| 160 |
if mask.sum() > 30:
|
| 161 |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
|
| 162 |
+
mask = cv2.dilate(mask, kernel, iterations=1)
|
|
|
|
| 163 |
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
|
| 164 |
mask = cv2.GaussianBlur(mask, (3,3), 0.8)
|
| 165 |
|
| 166 |
return mask
|
| 167 |
|
| 168 |
+
# ====================== COLOR TRANSFER ======================
|
| 169 |
@timed("Color Transfer")
|
| 170 |
def apply_strong_grey_hair(image: Image.Image, hair_mask: np.ndarray, beard_mask: np.ndarray):
|
| 171 |
comb = np.maximum(hair_mask, beard_mask)
|
|
|
|
| 176 |
orig_lab = cv2.cvtColor((img * 255).astype(np.uint8), cv2.COLOR_RGB2LAB).astype(np.float32)
|
| 177 |
hsv = cv2.cvtColor((img * 255).astype(np.uint8), cv2.COLOR_RGB2HSV).astype(np.float32)
|
| 178 |
|
| 179 |
+
# Hair greying
|
| 180 |
hsv_hair = hsv.copy()
|
|
|
|
|
|
|
| 181 |
hsv_hair[..., 1] = hsv_hair[..., 1] * (1 - 0.78 * hair_mask)
|
|
|
|
|
|
|
| 182 |
original_v = hsv[..., 2]
|
| 183 |
boost_amount = 89 * hair_mask
|
| 184 |
hsv_hair[..., 2] = np.clip(
|
|
|
|
| 197 |
mean_hair = np.array([130., 0., 0.])
|
| 198 |
std_hair = np.array([30., 10., 10.])
|
| 199 |
|
| 200 |
+
# Beard transfer
|
| 201 |
beard_bin = beard_mask > 0.5
|
| 202 |
if np.sum(beard_bin) > 30:
|
| 203 |
beard_pix = orig_lab[beard_bin]
|
|
|
|
| 216 |
final = hair_grey * hair_mask_3ch + final * (1 - hair_mask_3ch)
|
| 217 |
final = final * comb_3ch + img * (1 - comb_3ch)
|
| 218 |
|
|
|
|
| 219 |
final = final + (np.array([9, 7, 5], dtype=np.float32)/255.0 * comb[..., None] * 0.18)
|
|
|
|
| 220 |
final = np.clip(final * 255, 0, 255).astype(np.uint8)
|
| 221 |
result = Image.fromarray(final)
|
|
|
|
| 222 |
result = result.filter(ImageFilter.UnsharpMask(radius=0.7, percent=70, threshold=2))
|
| 223 |
|
| 224 |
return result
|
|
|
|
| 235 |
|
| 236 |
img_resized = orig.resize((target, target), Image.BILINEAR)
|
| 237 |
|
| 238 |
+
hair_mask, exclude_mask, mustache_mask = get_hair_and_exclude_masks(img_resized)
|
| 239 |
beard_mask = get_beard_mask_fast(img_resized, exclude_mask)
|
| 240 |
|
| 241 |
+
# ====================== MERGE MUSTACHE INTO BEARD MASK ======================
|
| 242 |
+
beard_mask = np.maximum(beard_mask, mustache_mask)
|
| 243 |
+
|
| 244 |
final_resized = apply_strong_grey_hair(img_resized, hair_mask, beard_mask)
|
| 245 |
|
| 246 |
final_img = final_resized.resize((ow, oh), Image.LANCZOS)
|