Upload 2 files
Browse files- app.py +71 -52
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -9,6 +9,7 @@ from diffusers import StableDiffusionXLInpaintPipeline
|
|
| 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
|
|
@@ -26,14 +27,8 @@ print("Inizializzazione FaceAnalysis...")
|
|
| 26 |
face_app = FaceAnalysis(name='antelopev2', root='./', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
|
| 27 |
face_app.prepare(ctx_id=0, det_size=(640, 640))
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
|
| 31 |
-
try:
|
| 32 |
-
inswapper_path = hf_hub_download(repo_id="ezioruan/inswapper_128.onnx", filename="inswapper_128.onnx")
|
| 33 |
-
face_swapper = insightface.model_zoo.get_model(inswapper_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
|
| 34 |
-
except Exception as e:
|
| 35 |
-
print(f"Errore caricamento Inswapper: {e}")
|
| 36 |
-
face_swapper = None
|
| 37 |
|
| 38 |
# Caricamento Pipeline Inpainting SDXL
|
| 39 |
print("Caricamento pipeline SDXL Inpaint...")
|
|
@@ -53,27 +48,26 @@ pipe.watermark = None
|
|
| 53 |
# ==============================================================================
|
| 54 |
# 2. CONFIGURAZIONE STILI E TEMPLATE
|
| 55 |
# ==============================================================================
|
| 56 |
-
# I template sono le immagini pre-generate fornite dall'utente.
|
| 57 |
STYLES = {
|
| 58 |
"Stile 1: Bit-Builder (8-Bit/Pixel Art)": {
|
| 59 |
"template": "esempi/8bit.png",
|
| 60 |
"repo_id": "nerijs/pixel-art-xl",
|
| 61 |
"weight_name": "pixel-art-xl.safetensors",
|
| 62 |
-
"prompt": "pixelart style, 8-bit game style face
|
| 63 |
"negative_prompt": "ugly, deformed, lowres, realistic, photographic, 3d render, photo, photorealistic"
|
| 64 |
},
|
| 65 |
"Stile 2: Il Costruttore Rinascimentale": {
|
| 66 |
"template": "esempi/rinascimentale.png",
|
| 67 |
"repo_id": "davidmoref/sdxl-lora-adapter-renaissance",
|
| 68 |
"weight_name": "pytorch_lora_weights.safetensors",
|
| 69 |
-
"prompt": "renaissance oil painting, oil painting face, elegant brush strokes, chiaroscuro lighting, classic masterpiece, perfectly integrated
|
| 70 |
"negative_prompt": "ugly, deformed, modern, photography, bad anatomy, worst quality, low quality, pixel art, cartoon"
|
| 71 |
},
|
| 72 |
"Stile 3: Genio Creativo (Pop-Art)": {
|
| 73 |
"template": "esempi/popart.png",
|
| 74 |
"repo_id": None,
|
| 75 |
"weight_name": None,
|
| 76 |
-
"prompt": "vintage pop art, Comic book style pop-art face, vibrant colors, halftone dots, graphic illustration, bold lines",
|
| 77 |
"negative_prompt": "ugly, deformed, photorealistic, realistic photography, oil painting, 3d render"
|
| 78 |
}
|
| 79 |
}
|
|
@@ -82,37 +76,26 @@ STYLES = {
|
|
| 82 |
# 3. FUNZIONI CORE DELL'APPLICAZIONE
|
| 83 |
# ==============================================================================
|
| 84 |
|
| 85 |
-
def
|
| 86 |
-
"""
|
| 87 |
-
|
| 88 |
-
"""
|
| 89 |
-
mask = Image.new("L", image.size, 0)
|
| 90 |
-
draw = ImageDraw.Draw(mask)
|
| 91 |
-
|
| 92 |
-
x1, y1, x2, y2 = bbox
|
| 93 |
w = x2 - x1
|
| 94 |
h = y2 - y1
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
ny1 = max(0, y1 - h * expansion)
|
| 99 |
-
nx2 = min(image.width, x2 + w * expansion)
|
| 100 |
-
ny2 = min(image.height, y2 + h * (expansion * 1.5)) # Espandi un po' di più verso il basso per il mento
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
mask = mask.filter(ImageFilter.GaussianBlur(blur_radius))
|
| 107 |
-
return mask
|
| 108 |
|
| 109 |
@spaces.GPU(duration=60)
|
| 110 |
def generate_avatar(user_image, selected_style):
|
| 111 |
if user_image is None:
|
| 112 |
raise gr.Error("Per favore, carica un'immagine del volto.")
|
| 113 |
-
|
| 114 |
-
if face_swapper is None:
|
| 115 |
-
raise gr.Error("Modello Inswapper non caricato correttamente.")
|
| 116 |
|
| 117 |
# 1. Analisi del volto utente
|
| 118 |
cv_user_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR)
|
|
@@ -120,10 +103,29 @@ def generate_avatar(user_image, selected_style):
|
|
| 120 |
if len(user_faces) == 0:
|
| 121 |
raise gr.Error("Nessun volto rilevato nella tua foto. Riprova con un selfie più chiaro.")
|
| 122 |
|
| 123 |
-
# Prendi il volto più grande
|
| 124 |
user_face = sorted(user_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 125 |
|
| 126 |
-
# 2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
style_config = STYLES[selected_style]
|
| 128 |
template_path = style_config["template"]
|
| 129 |
|
|
@@ -137,19 +139,38 @@ def generate_avatar(user_image, selected_style):
|
|
| 137 |
if len(template_faces) == 0:
|
| 138 |
raise gr.Error("Impossibile trovare un volto nel template di base.")
|
| 139 |
template_face = sorted(template_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 140 |
-
|
| 141 |
-
# 3. Swap del Volto (Fotorealistico)
|
| 142 |
-
print("Eseguendo Face Swap...")
|
| 143 |
-
swapped_cv = face_swapper.get(cv_template_image, template_face, user_face, paste_back=True)
|
| 144 |
-
swapped_pil = Image.fromarray(cv2.cvtColor(swapped_cv, cv2.COLOR_BGR2RGB))
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
#
|
| 150 |
repo_id = style_config["repo_id"]
|
| 151 |
|
| 152 |
-
# Carica il LoRA dello stile se necessario
|
| 153 |
if repo_id:
|
| 154 |
try:
|
| 155 |
print(f"Scaricamento/Caricamento LoRA {repo_id}...")
|
|
@@ -157,21 +178,19 @@ def generate_avatar(user_image, selected_style):
|
|
| 157 |
except Exception as e:
|
| 158 |
print(f"Errore caricamento LoRA da HF: {e}")
|
| 159 |
|
| 160 |
-
# Sposta la pipeline su GPU per l'inferenza
|
| 161 |
pipe.to(device)
|
| 162 |
|
| 163 |
-
print("Inizio Inpainting per fondere
|
| 164 |
stylized_image = pipe(
|
| 165 |
prompt=style_config["prompt"],
|
| 166 |
negative_prompt=style_config["negative_prompt"],
|
| 167 |
-
image=
|
| 168 |
mask_image=mask_pil,
|
| 169 |
-
strength=0.
|
| 170 |
-
guidance_scale=
|
| 171 |
-
num_inference_steps=
|
| 172 |
).images[0]
|
| 173 |
|
| 174 |
-
# Scarica il LoRA per evitare conflitti al prossimo giro
|
| 175 |
if repo_id:
|
| 176 |
try:
|
| 177 |
pipe.unload_lora_weights()
|
|
@@ -204,7 +223,7 @@ with gr.Blocks(css=custom_css, theme=theme) as demo:
|
|
| 204 |
|
| 205 |
with gr.Row():
|
| 206 |
with gr.Column(scale=1):
|
| 207 |
-
input_image = gr.Image(type="pil", label="1. Carica il tuo Selfie")
|
| 208 |
|
| 209 |
with gr.Column(scale=1):
|
| 210 |
style_selector = gr.Radio(
|
|
|
|
| 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
|
|
|
|
| 27 |
face_app = FaceAnalysis(name='antelopev2', root='./', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
|
| 28 |
face_app.prepare(ctx_id=0, det_size=(640, 640))
|
| 29 |
|
| 30 |
+
# Inizializza sessione Rembg
|
| 31 |
+
rembg_session = rembg.new_session()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
# Caricamento Pipeline Inpainting SDXL
|
| 34 |
print("Caricamento pipeline SDXL Inpaint...")
|
|
|
|
| 48 |
# ==============================================================================
|
| 49 |
# 2. CONFIGURAZIONE STILI E TEMPLATE
|
| 50 |
# ==============================================================================
|
|
|
|
| 51 |
STYLES = {
|
| 52 |
"Stile 1: Bit-Builder (8-Bit/Pixel Art)": {
|
| 53 |
"template": "esempi/8bit.png",
|
| 54 |
"repo_id": "nerijs/pixel-art-xl",
|
| 55 |
"weight_name": "pixel-art-xl.safetensors",
|
| 56 |
+
"prompt": "pixelart style, 8-bit game style face and hair, perfect pixel art shading, 8-bit retro aesthetic, matching lighting",
|
| 57 |
"negative_prompt": "ugly, deformed, lowres, realistic, photographic, 3d render, photo, photorealistic"
|
| 58 |
},
|
| 59 |
"Stile 2: Il Costruttore Rinascimentale": {
|
| 60 |
"template": "esempi/rinascimentale.png",
|
| 61 |
"repo_id": "davidmoref/sdxl-lora-adapter-renaissance",
|
| 62 |
"weight_name": "pytorch_lora_weights.safetensors",
|
| 63 |
+
"prompt": "renaissance oil painting, oil painting face and hair, elegant brush strokes, chiaroscuro lighting, classic masterpiece, perfectly integrated head",
|
| 64 |
"negative_prompt": "ugly, deformed, modern, photography, bad anatomy, worst quality, low quality, pixel art, cartoon"
|
| 65 |
},
|
| 66 |
"Stile 3: Genio Creativo (Pop-Art)": {
|
| 67 |
"template": "esempi/popart.png",
|
| 68 |
"repo_id": None,
|
| 69 |
"weight_name": None,
|
| 70 |
+
"prompt": "vintage pop art, Comic book style pop-art face and hair, vibrant colors, halftone dots, graphic illustration, bold lines",
|
| 71 |
"negative_prompt": "ugly, deformed, photorealistic, realistic photography, oil painting, 3d render"
|
| 72 |
}
|
| 73 |
}
|
|
|
|
| 76 |
# 3. FUNZIONI CORE DELL'APPLICAZIONE
|
| 77 |
# ==============================================================================
|
| 78 |
|
| 79 |
+
def extract_head_crop(image_pil, face_bbox):
|
| 80 |
+
"""Ritaglia un'area abbondante attorno al volto per catturare capelli e collo."""
|
| 81 |
+
x1, y1, x2, y2 = face_bbox
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
w = x2 - x1
|
| 83 |
h = y2 - y1
|
| 84 |
|
| 85 |
+
cx, cy = x1 + w/2, y1 + h/2
|
| 86 |
+
crop_size = max(w, h) * 3
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 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 |
+
crop_y2 = min(image_pil.height, int(cy + crop_size/2.5)) # Meno spazio sotto per evitare troppe spalle
|
| 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. Analisi del volto utente
|
| 101 |
cv_user_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR)
|
|
|
|
| 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 |
+
# 2. Ritaglio e rimozione sfondo (Head Extraction)
|
| 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 |
+
# 3. Analisi della testa scontornata per trovare le nuove coordinate del volto
|
| 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 |
+
# Fallback se non trova il volto dopo il ritaglio
|
| 118 |
+
head_face = user_face
|
| 119 |
+
else:
|
| 120 |
+
head_face = sorted(head_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 121 |
+
|
| 122 |
+
H_bbox = head_face.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 |
|
|
|
|
| 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 |
+
# 5. Compositing (Incolla la testa sul template)
|
| 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)
|
| 154 |
+
|
| 155 |
+
new_H_cx = H_cx * scale
|
| 156 |
+
new_H_cy = H_cy * scale
|
| 157 |
+
|
| 158 |
+
paste_x = int(T_cx - new_H_cx)
|
| 159 |
+
paste_y = int(T_cy - new_H_cy)
|
| 160 |
+
|
| 161 |
+
composited_pil = template_pil.copy()
|
| 162 |
+
composited_pil.paste(scaled_head, (paste_x, paste_y), scaled_head)
|
| 163 |
+
|
| 164 |
+
# 6. Creazione della Maschera per l'Inpainting
|
| 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 |
+
# 7. Stylization tramite Inpainting
|
| 172 |
repo_id = style_config["repo_id"]
|
| 173 |
|
|
|
|
| 174 |
if repo_id:
|
| 175 |
try:
|
| 176 |
print(f"Scaricamento/Caricamento LoRA {repo_id}...")
|
|
|
|
| 178 |
except Exception as e:
|
| 179 |
print(f"Errore caricamento LoRA da HF: {e}")
|
| 180 |
|
|
|
|
| 181 |
pipe.to(device)
|
| 182 |
|
| 183 |
+
print("Inizio Inpainting per fondere stile e testa...")
|
| 184 |
stylized_image = pipe(
|
| 185 |
prompt=style_config["prompt"],
|
| 186 |
negative_prompt=style_config["negative_prompt"],
|
| 187 |
+
image=composited_pil,
|
| 188 |
mask_image=mask_pil,
|
| 189 |
+
strength=0.45, # Aumentato leggermente per permettere all'IA di ridisegnare capelli e occhiali nello stile
|
| 190 |
+
guidance_scale=8.0,
|
| 191 |
+
num_inference_steps=35,
|
| 192 |
).images[0]
|
| 193 |
|
|
|
|
| 194 |
if repo_id:
|
| 195 |
try:
|
| 196 |
pipe.unload_lora_weights()
|
|
|
|
| 223 |
|
| 224 |
with gr.Row():
|
| 225 |
with gr.Column(scale=1):
|
| 226 |
+
input_image = gr.Image(type="pil", label="1. Carica il tuo Selfie (Ben illuminato)")
|
| 227 |
|
| 228 |
with gr.Column(scale=1):
|
| 229 |
style_selector = gr.Radio(
|
requirements.txt
CHANGED
|
@@ -9,3 +9,4 @@ onnxruntime
|
|
| 9 |
opencv-python
|
| 10 |
huggingface-hub
|
| 11 |
peft
|
|
|
|
|
|
| 9 |
opencv-python
|
| 10 |
huggingface-hub
|
| 11 |
peft
|
| 12 |
+
rembg
|