Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -73,13 +73,46 @@ def load_beard_model():
|
|
| 73 |
beard_model = YOLO(BEARD_MODEL_PATH)
|
| 74 |
return beard_model
|
| 75 |
|
| 76 |
-
# ======================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
@timed("Hair + Exclude + Lip Mask")
|
| 78 |
def get_hair_and_exclude_masks(pil_image: Image.Image):
|
| 79 |
load_face_parser()
|
| 80 |
orig_w, orig_h = pil_image.size
|
| 81 |
-
img_small = pil_image.resize((128, 128), Image.BILINEAR)
|
| 82 |
|
|
|
|
| 83 |
inputs = face_processor(images=img_small, return_tensors="pt").to(DEVICE)
|
| 84 |
|
| 85 |
with torch.inference_mode():
|
|
@@ -92,11 +125,10 @@ def get_hair_and_exclude_masks(pil_image: Image.Image):
|
|
| 92 |
hair = (probs[13].numpy() > 0.035).astype(np.float32)
|
| 93 |
hair = cv2.GaussianBlur(hair, (3, 3), 1.0)
|
| 94 |
|
| 95 |
-
# Face mask
|
| 96 |
parsing = up.argmax(dim=1).squeeze(0).cpu().numpy()
|
| 97 |
face_cls = list(range(1,6)) + list(range(8,13)) + [17,18]
|
| 98 |
face_m = np.isin(parsing, face_cls).astype(np.float32)
|
| 99 |
-
|
| 100 |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
|
| 101 |
face_m = cv2.dilate(face_m, kernel, iterations=1)
|
| 102 |
|
|
@@ -105,6 +137,7 @@ def get_hair_and_exclude_masks(pil_image: Image.Image):
|
|
| 105 |
forehead[:int(h * 0.32)] = 1.0
|
| 106 |
face_m = face_m * (1 - forehead * 0.45)
|
| 107 |
hair = hair * (1 - face_m)
|
|
|
|
| 108 |
hair = cv2.resize(hair, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 109 |
|
| 110 |
# ====================== EXCLUDE MASK ======================
|
|
@@ -114,56 +147,37 @@ def get_hair_and_exclude_masks(pil_image: Image.Image):
|
|
| 114 |
exclude = np.maximum(exclude, (probs[12].numpy() > 0.35).astype(np.float32))
|
| 115 |
exclude = np.maximum(exclude, (probs[4].numpy() > 0.35).astype(np.float32))
|
| 116 |
exclude = np.maximum(exclude, (probs[5].numpy() > 0.35).astype(np.float32))
|
| 117 |
-
|
| 118 |
exclude = cv2.dilate(exclude, kernel, iterations=2)
|
| 119 |
exclude = cv2.GaussianBlur(exclude, (5, 5), 1.2)
|
| 120 |
exclude = cv2.resize(exclude, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 121 |
|
| 122 |
-
# ======================
|
| 123 |
-
mustache_upper = (probs[11].numpy() > 0.18).astype(np.float32)
|
| 124 |
-
mustache_lower = (probs[12].numpy() > 0.18).astype(np.float32)
|
| 125 |
-
mouth = (probs[10].numpy() > 0.18).astype(np.float32)
|
| 126 |
-
|
| 127 |
-
mustache = np.maximum(mustache_upper, mustache_lower)
|
| 128 |
-
mustache = np.maximum(mustache, mouth)
|
| 129 |
-
|
| 130 |
-
kernel_must = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
| 131 |
-
mustache = cv2.morphologyEx(mustache, cv2.MORPH_CLOSE, kernel_must, iterations=3)
|
| 132 |
-
mustache = cv2.dilate(mustache, kernel_must, iterations=2)
|
| 133 |
-
mustache = cv2.morphologyEx(mustache, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3)), iterations=1)
|
| 134 |
-
mustache = cv2.GaussianBlur(mustache, (5, 5), 1.2)
|
| 135 |
-
|
| 136 |
-
# --- Shift mustache mask downward by 3 pixels (at 128x128 scale) ---
|
| 137 |
-
shift_y = 3 # small downward shift
|
| 138 |
-
M = np.float32([[1, 0, 0], [0, 1, shift_y]])
|
| 139 |
-
mustache = cv2.warpAffine(mustache, M, (mustache.shape[1], mustache.shape[0]), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
| 140 |
-
|
| 141 |
-
mustache = cv2.resize(mustache, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 142 |
-
mustache = np.maximum(mustache - exclude, 0)
|
| 143 |
-
|
| 144 |
-
# ====================== LIP MASK (HARD PROTECTION) ======================
|
| 145 |
lip_mask = np.zeros((128, 128), dtype=np.float32)
|
| 146 |
-
lip_mask = np.maximum(lip_mask, (probs[10].numpy() > 0.
|
| 147 |
-
lip_mask = np.maximum(lip_mask, (probs[11].numpy() > 0.
|
| 148 |
-
lip_mask = np.maximum(lip_mask, (probs[12].numpy() > 0.
|
| 149 |
-
lip_mask = cv2.dilate(lip_mask, kernel, iterations=
|
| 150 |
lip_mask = cv2.resize(lip_mask, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST)
|
| 151 |
lip_mask = (lip_mask > 0.5).astype(np.float32)
|
| 152 |
|
| 153 |
-
|
|
|
|
| 154 |
|
|
|
|
| 155 |
|
|
|
|
| 156 |
@timed("Beard Mask")
|
| 157 |
def get_beard_mask_fast(pil_image: Image.Image, exclude_mask: np.ndarray, lip_mask: np.ndarray):
|
| 158 |
model = load_beard_model()
|
| 159 |
orig_w, orig_h = pil_image.size
|
|
|
|
| 160 |
img_small = pil_image.resize((128, 128), Image.BILINEAR)
|
| 161 |
img_array = np.array(img_small)
|
| 162 |
|
| 163 |
results = model.predict(
|
| 164 |
img_array,
|
| 165 |
device=DEVICE.type,
|
| 166 |
-
conf=0.
|
| 167 |
iou=0.45,
|
| 168 |
imgsz=128,
|
| 169 |
half=False,
|
|
@@ -172,15 +186,14 @@ def get_beard_mask_fast(pil_image: Image.Image, exclude_mask: np.ndarray, lip_ma
|
|
| 172 |
)
|
| 173 |
|
| 174 |
mask = np.zeros((orig_h, orig_w), dtype=np.float32)
|
| 175 |
-
|
| 176 |
if results[0].masks is not None:
|
| 177 |
for i, cls in enumerate(results[0].boxes.cls):
|
| 178 |
if int(cls) == 0: # beard class
|
| 179 |
m = results[0].masks.data[i].cpu().numpy()
|
| 180 |
m = cv2.resize(m, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 181 |
-
mask = np.maximum(mask, (m > 0.
|
| 182 |
|
| 183 |
-
mask = np.maximum(mask - exclude_mask, 0)
|
| 184 |
|
| 185 |
if mask.sum() > 25:
|
| 186 |
kernel_erode = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
|
@@ -209,7 +222,6 @@ def get_beard_mask_fast(pil_image: Image.Image, exclude_mask: np.ndarray, lip_ma
|
|
| 209 |
mask[lip_mask > 0] = 0
|
| 210 |
return mask
|
| 211 |
|
| 212 |
-
|
| 213 |
# ====================== COLOR TRANSFER ======================
|
| 214 |
@timed("Color Transfer")
|
| 215 |
def apply_strong_grey_hair(image: Image.Image, hair_mask: np.ndarray, beard_mask: np.ndarray):
|
|
@@ -259,27 +271,34 @@ def apply_strong_grey_hair(image: Image.Image, hair_mask: np.ndarray, beard_mask
|
|
| 259 |
final = hair_grey * hair_mask_3ch + final * (1 - hair_mask_3ch)
|
| 260 |
final = final * comb_3ch + img * (1 - comb_3ch)
|
| 261 |
final = final + (np.array([9, 7, 5], dtype=np.float32)/255.0 * comb[..., None] * 0.18)
|
| 262 |
-
final = np.clip(final * 255, 0, 255).astype(np.uint8)
|
| 263 |
|
|
|
|
| 264 |
result = Image.fromarray(final)
|
| 265 |
result = result.filter(ImageFilter.UnsharpMask(radius=0.7, percent=70, threshold=2))
|
| 266 |
return result
|
| 267 |
|
| 268 |
-
|
| 269 |
# ====================== MAIN PROCESSING ======================
|
| 270 |
@timed("Total Processing")
|
| 271 |
def process_face_whitening(input_image: Image.Image):
|
| 272 |
orig = input_image.convert("RGB")
|
| 273 |
ow, oh = orig.size
|
|
|
|
| 274 |
target = min(SAFE_IMG_SIZE, max(ow, oh))
|
| 275 |
if target % 2 != 0:
|
| 276 |
target -= 1
|
|
|
|
| 277 |
img_resized = orig.resize((target, target), Image.BILINEAR)
|
| 278 |
|
| 279 |
hair_mask, exclude_mask, mustache_mask, lip_mask = get_hair_and_exclude_masks(img_resized)
|
| 280 |
beard_mask = get_beard_mask_fast(img_resized, exclude_mask, lip_mask)
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
beard_mask[lip_mask > 0] = 0
|
| 284 |
|
| 285 |
final_resized = apply_strong_grey_hair(img_resized, hair_mask, beard_mask)
|
|
@@ -291,7 +310,6 @@ def process_face_whitening(input_image: Image.Image):
|
|
| 291 |
|
| 292 |
return final_img
|
| 293 |
|
| 294 |
-
|
| 295 |
# ====================== FASTAPI ======================
|
| 296 |
app = FastAPI()
|
| 297 |
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
|
@@ -307,7 +325,6 @@ async def startup():
|
|
| 307 |
_ = process_face_whitening(dummy)
|
| 308 |
logger.info("✅ Server Ready!")
|
| 309 |
|
| 310 |
-
|
| 311 |
@app.post("/age-face")
|
| 312 |
async def age_face(file: UploadFile = File(...)):
|
| 313 |
start_total = time.perf_counter()
|
|
@@ -326,7 +343,6 @@ async def age_face(file: UploadFile = File(...)):
|
|
| 326 |
|
| 327 |
return StreamingResponse(buf, media_type="image/jpeg")
|
| 328 |
|
| 329 |
-
|
| 330 |
if __name__ == "__main__":
|
| 331 |
import uvicorn
|
| 332 |
uvicorn.run(app, host="0.0.0.0", port=7860, workers=1)
|
|
|
|
| 73 |
beard_model = YOLO(BEARD_MODEL_PATH)
|
| 74 |
return beard_model
|
| 75 |
|
| 76 |
+
# ====================== IMPROVED MUSTACHE MASK ======================
|
| 77 |
+
@timed("Mustache Mask")
|
| 78 |
+
def get_mustache_mask(probs, orig_w, orig_h, exclude_mask):
|
| 79 |
+
# More sensitive for thin, light or sparse mustache
|
| 80 |
+
u_lip = (probs[11].numpy() > 0.16).astype(np.float32)
|
| 81 |
+
l_lip = (probs[12].numpy() > 0.16).astype(np.float32)
|
| 82 |
+
mouth = (probs[10].numpy() > 0.20).astype(np.float32)
|
| 83 |
+
|
| 84 |
+
mustache = np.maximum(u_lip, l_lip)
|
| 85 |
+
mustache = np.maximum(mustache, mouth * 0.55)
|
| 86 |
+
|
| 87 |
+
# Better morphology for mustache shape (horizontal bias)
|
| 88 |
+
kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 89 |
+
kernel_horizontal = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 3))
|
| 90 |
+
|
| 91 |
+
mustache = cv2.dilate(mustache, kernel_small, iterations=1)
|
| 92 |
+
mustache = cv2.morphologyEx(mustache, cv2.MORPH_CLOSE, kernel_horizontal, iterations=2)
|
| 93 |
+
mustache = cv2.GaussianBlur(mustache, (5, 5), 1.0)
|
| 94 |
+
|
| 95 |
+
# Small downward shift
|
| 96 |
+
shift_y = 1
|
| 97 |
+
M = np.float32([[1, 0, 0], [0, 1, shift_y]])
|
| 98 |
+
mustache = cv2.warpAffine(mustache, M, (mustache.shape[1], mustache.shape[0]),
|
| 99 |
+
flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
|
| 100 |
+
|
| 101 |
+
mustache = cv2.resize(mustache, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 102 |
+
|
| 103 |
+
# Less aggressive exclude
|
| 104 |
+
mustache = np.maximum(mustache - exclude_mask * 0.65, 0)
|
| 105 |
+
mustache = (mustache > 0.20).astype(np.float32)
|
| 106 |
+
|
| 107 |
+
return mustache
|
| 108 |
+
|
| 109 |
+
# ====================== HAIR + EXCLUDE + LIP MASK ======================
|
| 110 |
@timed("Hair + Exclude + Lip Mask")
|
| 111 |
def get_hair_and_exclude_masks(pil_image: Image.Image):
|
| 112 |
load_face_parser()
|
| 113 |
orig_w, orig_h = pil_image.size
|
|
|
|
| 114 |
|
| 115 |
+
img_small = pil_image.resize((128, 128), Image.BILINEAR)
|
| 116 |
inputs = face_processor(images=img_small, return_tensors="pt").to(DEVICE)
|
| 117 |
|
| 118 |
with torch.inference_mode():
|
|
|
|
| 125 |
hair = (probs[13].numpy() > 0.035).astype(np.float32)
|
| 126 |
hair = cv2.GaussianBlur(hair, (3, 3), 1.0)
|
| 127 |
|
| 128 |
+
# Face mask (exclude forehead & other areas)
|
| 129 |
parsing = up.argmax(dim=1).squeeze(0).cpu().numpy()
|
| 130 |
face_cls = list(range(1,6)) + list(range(8,13)) + [17,18]
|
| 131 |
face_m = np.isin(parsing, face_cls).astype(np.float32)
|
|
|
|
| 132 |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
|
| 133 |
face_m = cv2.dilate(face_m, kernel, iterations=1)
|
| 134 |
|
|
|
|
| 137 |
forehead[:int(h * 0.32)] = 1.0
|
| 138 |
face_m = face_m * (1 - forehead * 0.45)
|
| 139 |
hair = hair * (1 - face_m)
|
| 140 |
+
|
| 141 |
hair = cv2.resize(hair, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 142 |
|
| 143 |
# ====================== EXCLUDE MASK ======================
|
|
|
|
| 147 |
exclude = np.maximum(exclude, (probs[12].numpy() > 0.35).astype(np.float32))
|
| 148 |
exclude = np.maximum(exclude, (probs[4].numpy() > 0.35).astype(np.float32))
|
| 149 |
exclude = np.maximum(exclude, (probs[5].numpy() > 0.35).astype(np.float32))
|
|
|
|
| 150 |
exclude = cv2.dilate(exclude, kernel, iterations=2)
|
| 151 |
exclude = cv2.GaussianBlur(exclude, (5, 5), 1.2)
|
| 152 |
exclude = cv2.resize(exclude, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 153 |
|
| 154 |
+
# ====================== LIP MASK (Hard Protection) ======================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
lip_mask = np.zeros((128, 128), dtype=np.float32)
|
| 156 |
+
lip_mask = np.maximum(lip_mask, (probs[10].numpy() > 0.42).astype(np.float32))
|
| 157 |
+
lip_mask = np.maximum(lip_mask, (probs[11].numpy() > 0.42).astype(np.float32))
|
| 158 |
+
lip_mask = np.maximum(lip_mask, (probs[12].numpy() > 0.42).astype(np.float32))
|
| 159 |
+
lip_mask = cv2.dilate(lip_mask, kernel, iterations=1)
|
| 160 |
lip_mask = cv2.resize(lip_mask, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST)
|
| 161 |
lip_mask = (lip_mask > 0.5).astype(np.float32)
|
| 162 |
|
| 163 |
+
# Get improved mustache
|
| 164 |
+
mustache = get_mustache_mask(probs, orig_w, orig_h, exclude)
|
| 165 |
|
| 166 |
+
return hair, exclude, mustache, lip_mask
|
| 167 |
|
| 168 |
+
# ====================== BEARD MASK ======================
|
| 169 |
@timed("Beard Mask")
|
| 170 |
def get_beard_mask_fast(pil_image: Image.Image, exclude_mask: np.ndarray, lip_mask: np.ndarray):
|
| 171 |
model = load_beard_model()
|
| 172 |
orig_w, orig_h = pil_image.size
|
| 173 |
+
|
| 174 |
img_small = pil_image.resize((128, 128), Image.BILINEAR)
|
| 175 |
img_array = np.array(img_small)
|
| 176 |
|
| 177 |
results = model.predict(
|
| 178 |
img_array,
|
| 179 |
device=DEVICE.type,
|
| 180 |
+
conf=0.18, # thoda lower for better mustache detection
|
| 181 |
iou=0.45,
|
| 182 |
imgsz=128,
|
| 183 |
half=False,
|
|
|
|
| 186 |
)
|
| 187 |
|
| 188 |
mask = np.zeros((orig_h, orig_w), dtype=np.float32)
|
|
|
|
| 189 |
if results[0].masks is not None:
|
| 190 |
for i, cls in enumerate(results[0].boxes.cls):
|
| 191 |
if int(cls) == 0: # beard class
|
| 192 |
m = results[0].masks.data[i].cpu().numpy()
|
| 193 |
m = cv2.resize(m, (orig_w, orig_h), interpolation=cv2.INTER_LINEAR)
|
| 194 |
+
mask = np.maximum(mask, (m > 0.26).astype(np.float32)) # lower threshold
|
| 195 |
|
| 196 |
+
mask = np.maximum(mask - exclude_mask * 0.6, 0)
|
| 197 |
|
| 198 |
if mask.sum() > 25:
|
| 199 |
kernel_erode = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
|
|
|
| 222 |
mask[lip_mask > 0] = 0
|
| 223 |
return mask
|
| 224 |
|
|
|
|
| 225 |
# ====================== COLOR TRANSFER ======================
|
| 226 |
@timed("Color Transfer")
|
| 227 |
def apply_strong_grey_hair(image: Image.Image, hair_mask: np.ndarray, beard_mask: np.ndarray):
|
|
|
|
| 271 |
final = hair_grey * hair_mask_3ch + final * (1 - hair_mask_3ch)
|
| 272 |
final = final * comb_3ch + img * (1 - comb_3ch)
|
| 273 |
final = final + (np.array([9, 7, 5], dtype=np.float32)/255.0 * comb[..., None] * 0.18)
|
|
|
|
| 274 |
|
| 275 |
+
final = np.clip(final * 255, 0, 255).astype(np.uint8)
|
| 276 |
result = Image.fromarray(final)
|
| 277 |
result = result.filter(ImageFilter.UnsharpMask(radius=0.7, percent=70, threshold=2))
|
| 278 |
return result
|
| 279 |
|
|
|
|
| 280 |
# ====================== MAIN PROCESSING ======================
|
| 281 |
@timed("Total Processing")
|
| 282 |
def process_face_whitening(input_image: Image.Image):
|
| 283 |
orig = input_image.convert("RGB")
|
| 284 |
ow, oh = orig.size
|
| 285 |
+
|
| 286 |
target = min(SAFE_IMG_SIZE, max(ow, oh))
|
| 287 |
if target % 2 != 0:
|
| 288 |
target -= 1
|
| 289 |
+
|
| 290 |
img_resized = orig.resize((target, target), Image.BILINEAR)
|
| 291 |
|
| 292 |
hair_mask, exclude_mask, mustache_mask, lip_mask = get_hair_and_exclude_masks(img_resized)
|
| 293 |
beard_mask = get_beard_mask_fast(img_resized, exclude_mask, lip_mask)
|
| 294 |
+
|
| 295 |
+
# Improved mustache blending
|
| 296 |
+
beard_mask = np.maximum(beard_mask, mustache_mask * 0.96)
|
| 297 |
+
|
| 298 |
+
# Extra boost for thin mustache areas
|
| 299 |
+
thin_mustache = (mustache_mask > 0.25) & (beard_mask < 0.45)
|
| 300 |
+
beard_mask[thin_mustache] = np.maximum(beard_mask[thin_mustache], 0.68)
|
| 301 |
+
|
| 302 |
beard_mask[lip_mask > 0] = 0
|
| 303 |
|
| 304 |
final_resized = apply_strong_grey_hair(img_resized, hair_mask, beard_mask)
|
|
|
|
| 310 |
|
| 311 |
return final_img
|
| 312 |
|
|
|
|
| 313 |
# ====================== FASTAPI ======================
|
| 314 |
app = FastAPI()
|
| 315 |
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
|
|
|
| 325 |
_ = process_face_whitening(dummy)
|
| 326 |
logger.info("✅ Server Ready!")
|
| 327 |
|
|
|
|
| 328 |
@app.post("/age-face")
|
| 329 |
async def age_face(file: UploadFile = File(...)):
|
| 330 |
start_total = time.perf_counter()
|
|
|
|
| 343 |
|
| 344 |
return StreamingResponse(buf, media_type="image/jpeg")
|
| 345 |
|
|
|
|
| 346 |
if __name__ == "__main__":
|
| 347 |
import uvicorn
|
| 348 |
uvicorn.run(app, host="0.0.0.0", port=7860, workers=1)
|