Upload app.py
Browse files
app.py
CHANGED
|
@@ -42,7 +42,6 @@ pipe = StableDiffusionXLInpaintPipeline.from_pretrained(
|
|
| 42 |
use_safetensors=True
|
| 43 |
)
|
| 44 |
|
| 45 |
-
# Rimuovi il limitatore di sicurezza se crea problemi (opzionale)
|
| 46 |
pipe.watermark = None
|
| 47 |
|
| 48 |
# ==============================================================================
|
|
@@ -77,7 +76,7 @@ STYLES = {
|
|
| 77 |
# ==============================================================================
|
| 78 |
|
| 79 |
def extract_head_crop(image_pil, face_bbox):
|
| 80 |
-
"""Ritaglia
|
| 81 |
x1, y1, x2, y2 = face_bbox
|
| 82 |
w = x2 - x1
|
| 83 |
h = y2 - y1
|
|
@@ -88,66 +87,101 @@ def extract_head_crop(image_pil, face_bbox):
|
|
| 88 |
crop_x1 = max(0, int(cx - crop_size/1.5))
|
| 89 |
crop_y1 = max(0, int(cy - crop_size/1.2)) # Più spazio sopra per i capelli
|
| 90 |
crop_x2 = min(image_pil.width, int(cx + crop_size/1.5))
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
return image_pil.crop((crop_x1, crop_y1, crop_x2, crop_y2))
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
@spaces.GPU(duration=60)
|
| 96 |
def generate_avatar(user_image, selected_style):
|
| 97 |
if user_image is None:
|
| 98 |
raise gr.Error("Per favore, carica un'immagine del volto.")
|
| 99 |
|
| 100 |
-
# 1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
cv_user_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR)
|
| 102 |
user_faces = face_app.get(cv_user_image)
|
| 103 |
if len(user_faces) == 0:
|
| 104 |
raise gr.Error("Nessun volto rilevato nella tua foto. Riprova con un selfie più chiaro.")
|
| 105 |
-
|
| 106 |
user_face = sorted(user_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 107 |
|
| 108 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
print("Estrazione della testa con Rembg...")
|
| 110 |
cropped_user = extract_head_crop(user_image, user_face.bbox)
|
| 111 |
head_rgba = rembg.remove(cropped_user, session=rembg_session)
|
|
|
|
| 112 |
|
| 113 |
-
#
|
| 114 |
head_rgb_cv = cv2.cvtColor(np.array(head_rgba.convert("RGB")), cv2.COLOR_RGB2BGR)
|
| 115 |
head_faces = face_app.get(head_rgb_cv)
|
| 116 |
if len(head_faces) == 0:
|
| 117 |
-
|
| 118 |
-
head_face = user_face
|
| 119 |
else:
|
| 120 |
-
|
| 121 |
|
| 122 |
-
H_bbox =
|
| 123 |
H_w = H_bbox[2] - H_bbox[0]
|
| 124 |
H_h = H_bbox[3] - H_bbox[1]
|
| 125 |
H_cx = H_bbox[0] + H_w/2
|
| 126 |
H_cy = H_bbox[1] + H_h/2
|
| 127 |
-
|
| 128 |
-
# 4. Caricamento e analisi del Template
|
| 129 |
-
style_config = STYLES[selected_style]
|
| 130 |
-
template_path = style_config["template"]
|
| 131 |
-
|
| 132 |
-
if not os.path.exists(template_path):
|
| 133 |
-
raise gr.Error(f"Errore: il file di template {template_path} non esiste.")
|
| 134 |
-
|
| 135 |
-
template_pil = Image.open(template_path).convert("RGB")
|
| 136 |
-
cv_template_image = cv2.cvtColor(np.array(template_pil), cv2.COLOR_RGB2BGR)
|
| 137 |
-
|
| 138 |
-
template_faces = face_app.get(cv_template_image)
|
| 139 |
-
if len(template_faces) == 0:
|
| 140 |
-
raise gr.Error("Impossibile trovare un volto nel template di base.")
|
| 141 |
-
template_face = sorted(template_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 142 |
|
| 143 |
T_bbox = template_face.bbox
|
| 144 |
T_w = T_bbox[2] - T_bbox[0]
|
| 145 |
T_cx = T_bbox[0] + T_w/2
|
| 146 |
T_cy = T_bbox[1] + (T_bbox[3] - T_bbox[1])/2
|
| 147 |
|
| 148 |
-
#
|
| 149 |
print("Compositing della testa sul template...")
|
| 150 |
-
# Scaliamo la testa in modo che la larghezza del volto coincida (con un piccolo margine in più per coprire bene)
|
| 151 |
scale = (T_w / H_w) * 1.05
|
| 152 |
new_size = (int(head_rgba.width * scale), int(head_rgba.height * scale))
|
| 153 |
scaled_head = head_rgba.resize(new_size, Image.LANCZOS)
|
|
@@ -161,14 +195,13 @@ def generate_avatar(user_image, selected_style):
|
|
| 161 |
composited_pil = template_pil.copy()
|
| 162 |
composited_pil.paste(scaled_head, (paste_x, paste_y), scaled_head)
|
| 163 |
|
| 164 |
-
#
|
| 165 |
mask_pil = Image.new("L", composited_pil.size, 0)
|
| 166 |
mask_pil.paste(scaled_head.split()[3], (paste_x, paste_y))
|
| 167 |
-
# Espandi leggermente la maschera per coprire bene i bordi e sfumala
|
| 168 |
mask_pil = mask_pil.filter(ImageFilter.MaxFilter(7))
|
| 169 |
mask_pil = mask_pil.filter(ImageFilter.GaussianBlur(15))
|
| 170 |
|
| 171 |
-
#
|
| 172 |
repo_id = style_config["repo_id"]
|
| 173 |
|
| 174 |
if repo_id:
|
|
@@ -186,7 +219,7 @@ def generate_avatar(user_image, selected_style):
|
|
| 186 |
negative_prompt=style_config["negative_prompt"],
|
| 187 |
image=composited_pil,
|
| 188 |
mask_image=mask_pil,
|
| 189 |
-
strength=0.45,
|
| 190 |
guidance_scale=8.0,
|
| 191 |
num_inference_steps=35,
|
| 192 |
).images[0]
|
|
|
|
| 42 |
use_safetensors=True
|
| 43 |
)
|
| 44 |
|
|
|
|
| 45 |
pipe.watermark = None
|
| 46 |
|
| 47 |
# ==============================================================================
|
|
|
|
| 76 |
# ==============================================================================
|
| 77 |
|
| 78 |
def extract_head_crop(image_pil, face_bbox):
|
| 79 |
+
"""Ritaglia l'area della testa, fermandosi alla base del collo per escludere le spalle."""
|
| 80 |
x1, y1, x2, y2 = face_bbox
|
| 81 |
w = x2 - x1
|
| 82 |
h = y2 - y1
|
|
|
|
| 87 |
crop_x1 = max(0, int(cx - crop_size/1.5))
|
| 88 |
crop_y1 = max(0, int(cy - crop_size/1.2)) # Più spazio sopra per i capelli
|
| 89 |
crop_x2 = min(image_pil.width, int(cx + crop_size/1.5))
|
| 90 |
+
|
| 91 |
+
# Meno spazio sotto: fermiamoci alla base del collo, evitando le spalle
|
| 92 |
+
# y2 è circa il mento, y2 + h*0.5 è la fine del collo
|
| 93 |
+
crop_y2 = min(image_pil.height, int(y2 + h * 0.45))
|
| 94 |
|
| 95 |
return image_pil.crop((crop_x1, crop_y1, crop_x2, crop_y2))
|
| 96 |
|
| 97 |
+
def fade_bottom(image_rgba, fade_height_ratio=0.15):
|
| 98 |
+
"""Applica una sfumatura trasparente alla base dell'immagine per fondere dolcemente il collo."""
|
| 99 |
+
width, height = image_rgba.size
|
| 100 |
+
fade_height = int(height * fade_height_ratio)
|
| 101 |
+
if fade_height == 0:
|
| 102 |
+
return image_rgba
|
| 103 |
+
|
| 104 |
+
r, g, b, a = image_rgba.split()
|
| 105 |
+
alpha_np = np.array(a)
|
| 106 |
+
|
| 107 |
+
# Crea un gradiente da 1.0 (opaco) a 0.0 (trasparente)
|
| 108 |
+
gradient = np.linspace(1, 0, fade_height)
|
| 109 |
+
gradient = np.tile(gradient[:, None], (1, width))
|
| 110 |
+
|
| 111 |
+
# Applica il gradiente alla parte bassa del canale alpha
|
| 112 |
+
alpha_np[-fade_height:, :] = (alpha_np[-fade_height:, :] * gradient).astype(np.uint8)
|
| 113 |
+
|
| 114 |
+
a = Image.fromarray(alpha_np)
|
| 115 |
+
image_rgba.putalpha(a)
|
| 116 |
+
return image_rgba
|
| 117 |
+
|
| 118 |
@spaces.GPU(duration=60)
|
| 119 |
def generate_avatar(user_image, selected_style):
|
| 120 |
if user_image is None:
|
| 121 |
raise gr.Error("Per favore, carica un'immagine del volto.")
|
| 122 |
|
| 123 |
+
# 1. Caricamento e analisi del Template (lo facciamo prima per sapere dove guarda)
|
| 124 |
+
style_config = STYLES[selected_style]
|
| 125 |
+
template_path = style_config["template"]
|
| 126 |
+
|
| 127 |
+
if not os.path.exists(template_path):
|
| 128 |
+
raise gr.Error(f"Errore: il file di template {template_path} non esiste.")
|
| 129 |
+
|
| 130 |
+
template_pil = Image.open(template_path).convert("RGB")
|
| 131 |
+
cv_template_image = cv2.cvtColor(np.array(template_pil), cv2.COLOR_RGB2BGR)
|
| 132 |
+
|
| 133 |
+
template_faces = face_app.get(cv_template_image)
|
| 134 |
+
if len(template_faces) == 0:
|
| 135 |
+
raise gr.Error("Impossibile trovare un volto nel template di base.")
|
| 136 |
+
template_face = sorted(template_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 137 |
+
|
| 138 |
+
# 2. Analisi iniziale del volto utente per controllare l'orientamento
|
| 139 |
cv_user_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR)
|
| 140 |
user_faces = face_app.get(cv_user_image)
|
| 141 |
if len(user_faces) == 0:
|
| 142 |
raise gr.Error("Nessun volto rilevato nella tua foto. Riprova con un selfie più chiaro.")
|
|
|
|
| 143 |
user_face = sorted(user_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 144 |
|
| 145 |
+
# 3. Controllo Yaw (Orientamento Destra/Sinistra)
|
| 146 |
+
user_yaw = user_face.pose[1]
|
| 147 |
+
template_yaw = template_face.pose[1]
|
| 148 |
+
|
| 149 |
+
# Se guardano in direzioni opposte, ribaltiamo il selfie
|
| 150 |
+
if (user_yaw * template_yaw) < 0:
|
| 151 |
+
print("Orientamenti opposti! Eseguo specchio (FLIP_LEFT_RIGHT) del selfie.")
|
| 152 |
+
user_image = user_image.transpose(Image.FLIP_LEFT_RIGHT)
|
| 153 |
+
# Rianalizziamo il volto ribaltato per avere le nuove coordinate perfette
|
| 154 |
+
cv_user_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR)
|
| 155 |
+
user_faces = face_app.get(cv_user_image)
|
| 156 |
+
user_face = sorted(user_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 157 |
+
|
| 158 |
+
# 4. Ritaglio, rimozione sfondo e sfumatura collo
|
| 159 |
print("Estrazione della testa con Rembg...")
|
| 160 |
cropped_user = extract_head_crop(user_image, user_face.bbox)
|
| 161 |
head_rgba = rembg.remove(cropped_user, session=rembg_session)
|
| 162 |
+
head_rgba = fade_bottom(head_rgba, fade_height_ratio=0.15) # Sfuma il collo
|
| 163 |
|
| 164 |
+
# 5. Analisi della testa scontornata per trovare le nuove coordinate del volto
|
| 165 |
head_rgb_cv = cv2.cvtColor(np.array(head_rgba.convert("RGB")), cv2.COLOR_RGB2BGR)
|
| 166 |
head_faces = face_app.get(head_rgb_cv)
|
| 167 |
if len(head_faces) == 0:
|
| 168 |
+
head_face_crop = user_face
|
|
|
|
| 169 |
else:
|
| 170 |
+
head_face_crop = sorted(head_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 171 |
|
| 172 |
+
H_bbox = head_face_crop.bbox
|
| 173 |
H_w = H_bbox[2] - H_bbox[0]
|
| 174 |
H_h = H_bbox[3] - H_bbox[1]
|
| 175 |
H_cx = H_bbox[0] + H_w/2
|
| 176 |
H_cy = H_bbox[1] + H_h/2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
T_bbox = template_face.bbox
|
| 179 |
T_w = T_bbox[2] - T_bbox[0]
|
| 180 |
T_cx = T_bbox[0] + T_w/2
|
| 181 |
T_cy = T_bbox[1] + (T_bbox[3] - T_bbox[1])/2
|
| 182 |
|
| 183 |
+
# 6. Compositing (Incolla la testa sul template)
|
| 184 |
print("Compositing della testa sul template...")
|
|
|
|
| 185 |
scale = (T_w / H_w) * 1.05
|
| 186 |
new_size = (int(head_rgba.width * scale), int(head_rgba.height * scale))
|
| 187 |
scaled_head = head_rgba.resize(new_size, Image.LANCZOS)
|
|
|
|
| 195 |
composited_pil = template_pil.copy()
|
| 196 |
composited_pil.paste(scaled_head, (paste_x, paste_y), scaled_head)
|
| 197 |
|
| 198 |
+
# 7. Creazione della Maschera per l'Inpainting
|
| 199 |
mask_pil = Image.new("L", composited_pil.size, 0)
|
| 200 |
mask_pil.paste(scaled_head.split()[3], (paste_x, paste_y))
|
|
|
|
| 201 |
mask_pil = mask_pil.filter(ImageFilter.MaxFilter(7))
|
| 202 |
mask_pil = mask_pil.filter(ImageFilter.GaussianBlur(15))
|
| 203 |
|
| 204 |
+
# 8. Stylization tramite Inpainting
|
| 205 |
repo_id = style_config["repo_id"]
|
| 206 |
|
| 207 |
if repo_id:
|
|
|
|
| 219 |
negative_prompt=style_config["negative_prompt"],
|
| 220 |
image=composited_pil,
|
| 221 |
mask_image=mask_pil,
|
| 222 |
+
strength=0.45,
|
| 223 |
guidance_scale=8.0,
|
| 224 |
num_inference_steps=35,
|
| 225 |
).images[0]
|