Spaces:
Running on Zero
Running on Zero
Upload 2 files
Browse files- app.py +66 -98
- requirements.txt +0 -1
app.py
CHANGED
|
@@ -9,7 +9,6 @@ from diffusers import StableDiffusionXLInpaintPipeline
|
|
| 9 |
from insightface.app import FaceAnalysis
|
| 10 |
import insightface
|
| 11 |
from huggingface_hub import hf_hub_download, snapshot_download
|
| 12 |
-
import rembg
|
| 13 |
|
| 14 |
# ==============================================================================
|
| 15 |
# 1. SETUP DELL'AMBIENTE E DOWNLOAD MODELLI
|
|
@@ -22,13 +21,18 @@ try:
|
|
| 22 |
except Exception as e:
|
| 23 |
print(f"Errore download AntelopeV2: {e}")
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# Inizializza l'analizzatore di volti (InsightFace)
|
| 26 |
print("Inizializzazione FaceAnalysis...")
|
| 27 |
face_app = FaceAnalysis(name='antelopev2', root='./', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
|
| 28 |
face_app.prepare(ctx_id=0, det_size=(640, 640))
|
| 29 |
|
| 30 |
-
# Inizializza
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
# Caricamento Pipeline Inpainting SDXL
|
| 34 |
print("Caricamento pipeline SDXL Inpaint...")
|
|
@@ -42,6 +46,11 @@ pipe = StableDiffusionXLInpaintPipeline.from_pretrained(
|
|
| 42 |
use_safetensors=True
|
| 43 |
)
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
pipe.watermark = None
|
| 46 |
|
| 47 |
# ==============================================================================
|
|
@@ -75,47 +84,33 @@ STYLES = {
|
|
| 75 |
# 3. FUNZIONI CORE DELL'APPLICAZIONE
|
| 76 |
# ==============================================================================
|
| 77 |
|
| 78 |
-
def
|
| 79 |
-
"""
|
| 80 |
x1, y1, x2, y2 = face_bbox
|
| 81 |
w = x2 - x1
|
| 82 |
h = y2 - y1
|
| 83 |
|
| 84 |
cx, cy = x1 + w/2, y1 + h/2
|
| 85 |
-
|
| 86 |
-
|
| 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 |
-
#
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def cut_bottom(image_rgba, cut_height_ratio=0.20):
|
| 97 |
-
"""Rende completamente trasparente la base dell'immagine (Hard Cut) per eliminare il collo inferiore."""
|
| 98 |
-
width, height = image_rgba.size
|
| 99 |
-
cut_height = int(height * cut_height_ratio)
|
| 100 |
-
if cut_height == 0:
|
| 101 |
-
return image_rgba
|
| 102 |
-
|
| 103 |
-
r, g, b, a = image_rgba.split()
|
| 104 |
-
alpha_np = np.array(a)
|
| 105 |
|
| 106 |
-
|
| 107 |
-
alpha_np[-cut_height:, :] = 0
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
return image_rgba
|
| 112 |
|
| 113 |
@spaces.GPU(duration=60)
|
| 114 |
def generate_avatar(user_image, selected_style):
|
| 115 |
if user_image is None:
|
| 116 |
raise gr.Error("Per favore, carica un'immagine del volto.")
|
| 117 |
|
| 118 |
-
# 1. Caricamento e analisi del Template
|
| 119 |
style_config = STYLES[selected_style]
|
| 120 |
template_path = style_config["template"]
|
| 121 |
|
|
@@ -130,91 +125,49 @@ def generate_avatar(user_image, selected_style):
|
|
| 130 |
raise gr.Error("Impossibile trovare un volto nel template di base.")
|
| 131 |
template_face = sorted(template_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 132 |
|
| 133 |
-
# 2. Analisi
|
| 134 |
cv_user_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR)
|
| 135 |
user_faces = face_app.get(cv_user_image)
|
| 136 |
if len(user_faces) == 0:
|
| 137 |
raise gr.Error("Nessun volto rilevato nella tua foto. Riprova con un selfie più chiaro.")
|
| 138 |
user_face = sorted(user_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 139 |
|
| 140 |
-
# 3.
|
| 141 |
-
|
| 142 |
-
|
| 143 |
|
| 144 |
-
#
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
head_rgba = cut_bottom(head_rgba, cut_height_ratio=0.20) # Taglio netto trasparente
|
| 158 |
-
|
| 159 |
-
# 5. Analisi della testa scontornata per trovare le nuove coordinate del volto
|
| 160 |
-
head_rgb_cv = cv2.cvtColor(np.array(head_rgba.convert("RGB")), cv2.COLOR_RGB2BGR)
|
| 161 |
-
head_faces = face_app.get(head_rgb_cv)
|
| 162 |
-
if len(head_faces) == 0:
|
| 163 |
-
head_face_crop = user_face
|
| 164 |
-
else:
|
| 165 |
-
head_face_crop = sorted(head_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 166 |
-
|
| 167 |
-
H_bbox = head_face_crop.bbox
|
| 168 |
-
H_w = H_bbox[2] - H_bbox[0]
|
| 169 |
-
H_h = H_bbox[3] - H_bbox[1]
|
| 170 |
-
H_cx = H_bbox[0] + H_w/2
|
| 171 |
-
H_cy = H_bbox[1] + H_h/2
|
| 172 |
-
|
| 173 |
-
T_bbox = template_face.bbox
|
| 174 |
-
T_w = T_bbox[2] - T_bbox[0]
|
| 175 |
-
T_cx = T_bbox[0] + T_w/2
|
| 176 |
-
T_cy = T_bbox[1] + (T_bbox[3] - T_bbox[1])/2
|
| 177 |
-
|
| 178 |
-
# 6. Compositing (Incolla la testa sul template)
|
| 179 |
-
print("Compositing della testa sul template...")
|
| 180 |
-
scale = (T_w / H_w) * 1.05
|
| 181 |
-
new_size = (int(head_rgba.width * scale), int(head_rgba.height * scale))
|
| 182 |
-
scaled_head = head_rgba.resize(new_size, Image.LANCZOS)
|
| 183 |
-
|
| 184 |
-
new_H_cx = H_cx * scale
|
| 185 |
-
new_H_cy = H_cy * scale
|
| 186 |
-
|
| 187 |
-
paste_x = int(T_cx - new_H_cx)
|
| 188 |
-
paste_y = int(T_cy - new_H_cy)
|
| 189 |
-
|
| 190 |
-
composited_pil = template_pil.copy()
|
| 191 |
-
composited_pil.paste(scaled_head, (paste_x, paste_y), scaled_head)
|
| 192 |
-
|
| 193 |
-
# 7. Creazione della Maschera per l'Inpainting
|
| 194 |
-
mask_pil = Image.new("L", composited_pil.size, 0)
|
| 195 |
-
mask_pil.paste(scaled_head.split()[3], (paste_x, paste_y))
|
| 196 |
-
mask_pil = mask_pil.filter(ImageFilter.MaxFilter(7))
|
| 197 |
-
mask_pil = mask_pil.filter(ImageFilter.GaussianBlur(15))
|
| 198 |
-
|
| 199 |
-
# 8. Stylization tramite Inpainting
|
| 200 |
repo_id = style_config["repo_id"]
|
| 201 |
-
|
| 202 |
if repo_id:
|
| 203 |
try:
|
| 204 |
-
print(f"
|
| 205 |
pipe.load_lora_weights(repo_id, weight_name=style_config["weight_name"])
|
| 206 |
except Exception as e:
|
| 207 |
-
print(f"Errore caricamento LoRA
|
| 208 |
|
| 209 |
pipe.to(device)
|
| 210 |
|
| 211 |
-
print("Inizio
|
| 212 |
-
|
|
|
|
| 213 |
prompt=style_config["prompt"],
|
| 214 |
negative_prompt=style_config["negative_prompt"],
|
| 215 |
-
image=
|
| 216 |
mask_image=mask_pil,
|
| 217 |
-
|
|
|
|
| 218 |
guidance_scale=8.0,
|
| 219 |
num_inference_steps=35,
|
| 220 |
).images[0]
|
|
@@ -225,7 +178,22 @@ def generate_avatar(user_image, selected_style):
|
|
| 225 |
except:
|
| 226 |
pass
|
| 227 |
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
# ==============================================================================
|
| 231 |
# 4. INTERFACCIA GRADIO (FRONTEND)
|
|
|
|
| 9 |
from insightface.app import FaceAnalysis
|
| 10 |
import insightface
|
| 11 |
from huggingface_hub import hf_hub_download, snapshot_download
|
|
|
|
| 12 |
|
| 13 |
# ==============================================================================
|
| 14 |
# 1. SETUP DELL'AMBIENTE E DOWNLOAD MODELLI
|
|
|
|
| 21 |
except Exception as e:
|
| 22 |
print(f"Errore download AntelopeV2: {e}")
|
| 23 |
|
| 24 |
+
if not os.path.exists("./models/inswapper_128.onnx"):
|
| 25 |
+
print("Download Inswapper...")
|
| 26 |
+
hf_hub_download(repo_id="ezioruan/inswapper_128.onnx", filename="inswapper_128.onnx", local_dir="./models")
|
| 27 |
+
|
| 28 |
# Inizializza l'analizzatore di volti (InsightFace)
|
| 29 |
print("Inizializzazione FaceAnalysis...")
|
| 30 |
face_app = FaceAnalysis(name='antelopev2', root='./', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
|
| 31 |
face_app.prepare(ctx_id=0, det_size=(640, 640))
|
| 32 |
|
| 33 |
+
# Inizializza Inswapper (per fissare l'identità alla fine)
|
| 34 |
+
print("Inizializzazione Inswapper...")
|
| 35 |
+
swapper = insightface.model_zoo.get_model('./models/inswapper_128.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
|
| 36 |
|
| 37 |
# Caricamento Pipeline Inpainting SDXL
|
| 38 |
print("Caricamento pipeline SDXL Inpaint...")
|
|
|
|
| 46 |
use_safetensors=True
|
| 47 |
)
|
| 48 |
|
| 49 |
+
# Aggiunta IP-Adapter per l'analisi semantica (Capelli, Occhiali, Barba)
|
| 50 |
+
print("Caricamento IP-Adapter per SDXL...")
|
| 51 |
+
pipe.load_ip_adapter("h94/IP-Adapter", subfolder="sdxl_models", weight_name="ip-adapter_sdxl.bin")
|
| 52 |
+
pipe.set_ip_adapter_scale(0.7) # Bilanciamento tra fedeltà al selfie e stile del prompt
|
| 53 |
+
|
| 54 |
pipe.watermark = None
|
| 55 |
|
| 56 |
# ==============================================================================
|
|
|
|
| 84 |
# 3. FUNZIONI CORE DELL'APPLICAZIONE
|
| 85 |
# ==============================================================================
|
| 86 |
|
| 87 |
+
def create_head_mask(image_pil, face_bbox):
|
| 88 |
+
"""Crea una maschera che copre l'intera testa (inclusi capelli e barba) del template."""
|
| 89 |
x1, y1, x2, y2 = face_bbox
|
| 90 |
w = x2 - x1
|
| 91 |
h = y2 - y1
|
| 92 |
|
| 93 |
cx, cy = x1 + w/2, y1 + h/2
|
| 94 |
+
mask = Image.new("L", image_pil.size, 0)
|
| 95 |
+
draw = ImageDraw.Draw(mask)
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
# Area ovale molto abbondante per coprire capelli in alto e barba in basso
|
| 98 |
+
mask_x1 = max(0, cx - w*1.1)
|
| 99 |
+
mask_x2 = min(image_pil.width, cx + w*1.1)
|
| 100 |
+
mask_y1 = max(0, cy - h*1.4) # Su per i capelli/cappelli
|
| 101 |
+
mask_y2 = min(image_pil.height, cy + h*0.8) # Giù per la barba
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
+
draw.ellipse((mask_x1, mask_y1, mask_x2, mask_y2), fill=255)
|
|
|
|
| 104 |
|
| 105 |
+
mask = mask.filter(ImageFilter.GaussianBlur(15))
|
| 106 |
+
return mask
|
|
|
|
| 107 |
|
| 108 |
@spaces.GPU(duration=60)
|
| 109 |
def generate_avatar(user_image, selected_style):
|
| 110 |
if user_image is None:
|
| 111 |
raise gr.Error("Per favore, carica un'immagine del volto.")
|
| 112 |
|
| 113 |
+
# 1. Caricamento e analisi del Template
|
| 114 |
style_config = STYLES[selected_style]
|
| 115 |
template_path = style_config["template"]
|
| 116 |
|
|
|
|
| 125 |
raise gr.Error("Impossibile trovare un volto nel template di base.")
|
| 126 |
template_face = sorted(template_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 127 |
|
| 128 |
+
# 2. Analisi del volto utente
|
| 129 |
cv_user_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR)
|
| 130 |
user_faces = face_app.get(cv_user_image)
|
| 131 |
if len(user_faces) == 0:
|
| 132 |
raise gr.Error("Nessun volto rilevato nella tua foto. Riprova con un selfie più chiaro.")
|
| 133 |
user_face = sorted(user_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 134 |
|
| 135 |
+
# 3. Creazione maschera per rigenerazione della testa
|
| 136 |
+
print("Creazione maschera per svuotare la testa del template...")
|
| 137 |
+
mask_pil = create_head_mask(template_pil, template_face.bbox)
|
| 138 |
|
| 139 |
+
# Ritaglio del selfie (per dare all'IP-Adapter solo la testa e ignorare lo sfondo della stanza)
|
| 140 |
+
x1, y1, x2, y2 = user_face.bbox
|
| 141 |
+
w = x2 - x1
|
| 142 |
+
h = y2 - y1
|
| 143 |
+
cx, cy = x1 + w/2, y1 + h/2
|
| 144 |
+
crop_size = max(w, h) * 2.5
|
| 145 |
+
ip_x1 = max(0, int(cx - crop_size/2))
|
| 146 |
+
ip_y1 = max(0, int(cy - crop_size/1.5))
|
| 147 |
+
ip_x2 = min(user_image.width, int(cx + crop_size/2))
|
| 148 |
+
ip_y2 = min(user_image.height, int(cy + crop_size/2))
|
| 149 |
+
ip_image = user_image.crop((ip_x1, ip_y1, ip_x2, ip_y2))
|
| 150 |
+
|
| 151 |
+
# 4. Rigenerazione Semantica (SDXL + IP-Adapter)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
repo_id = style_config["repo_id"]
|
|
|
|
| 153 |
if repo_id:
|
| 154 |
try:
|
| 155 |
+
print(f"Caricamento LoRA {repo_id}...")
|
| 156 |
pipe.load_lora_weights(repo_id, weight_name=style_config["weight_name"])
|
| 157 |
except Exception as e:
|
| 158 |
+
print(f"Errore caricamento LoRA: {e}")
|
| 159 |
|
| 160 |
pipe.to(device)
|
| 161 |
|
| 162 |
+
print("Inizio Generazione Semantica della Testa (SDXL + IP-Adapter)...")
|
| 163 |
+
# Usa una strength quasi al 100% per ignorare i pixel della testa originale sotto la maschera
|
| 164 |
+
generated_image = pipe(
|
| 165 |
prompt=style_config["prompt"],
|
| 166 |
negative_prompt=style_config["negative_prompt"],
|
| 167 |
+
image=template_pil,
|
| 168 |
mask_image=mask_pil,
|
| 169 |
+
ip_adapter_image=ip_image,
|
| 170 |
+
strength=0.99,
|
| 171 |
guidance_scale=8.0,
|
| 172 |
num_inference_steps=35,
|
| 173 |
).images[0]
|
|
|
|
| 178 |
except:
|
| 179 |
pass
|
| 180 |
|
| 181 |
+
# 5. Iniezione dell'Identità pura (FaceSwap)
|
| 182 |
+
print("Iniezione dell'identità tramite Inswapper...")
|
| 183 |
+
cv_generated = cv2.cvtColor(np.array(generated_image), cv2.COLOR_RGB2BGR)
|
| 184 |
+
generated_faces = face_app.get(cv_generated)
|
| 185 |
+
|
| 186 |
+
if len(generated_faces) > 0:
|
| 187 |
+
# Trova la faccia appena generata dall'IA
|
| 188 |
+
gen_face = sorted(generated_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 189 |
+
# Incolla ESATTAMENTE gli occhi, naso e bocca originali dell'utente sopra la testa generata
|
| 190 |
+
cv_final = swapper.get(cv_generated, gen_face, user_face, paste_back=True)
|
| 191 |
+
final_image = Image.fromarray(cv2.cvtColor(cv_final, cv2.COLOR_BGR2RGB))
|
| 192 |
+
else:
|
| 193 |
+
print("Attenzione: SDXL non ha generato un volto rilevabile. Restituisco l'immagine senza Inswapper.")
|
| 194 |
+
final_image = generated_image
|
| 195 |
+
|
| 196 |
+
return final_image
|
| 197 |
|
| 198 |
# ==============================================================================
|
| 199 |
# 4. INTERFACCIA GRADIO (FRONTEND)
|
requirements.txt
CHANGED
|
@@ -9,4 +9,3 @@ onnxruntime
|
|
| 9 |
opencv-python
|
| 10 |
huggingface-hub
|
| 11 |
peft
|
| 12 |
-
rembg
|
|
|
|
| 9 |
opencv-python
|
| 10 |
huggingface-hub
|
| 11 |
peft
|
|
|