Astridkraft commited on
Commit
a2fba2c
·
verified ·
1 Parent(s): cbb4469

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +115 -101
app.py CHANGED
@@ -5,8 +5,7 @@ from PIL import Image, ImageDraw
5
  import time
6
  import os
7
  import tempfile
8
- import cv2
9
- import numpy as np
10
 
11
  # === OPTIMIERTE EINSTELLUNGEN ===
12
  device = "cuda" if torch.cuda.is_available() else "cpu"
@@ -15,45 +14,38 @@ IMG_SIZE = 512
15
 
16
  print(f"Running on: {device}")
17
 
18
- # === GESICHTSERKENNUNG ===
19
- CASCADE_PATH = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
20
-
21
- def detect_face_bbox(pil_image):
22
- """Gibt (x1, y1, x2, y2) für das erkannte Gesicht zurück oder None."""
23
- try:
24
- cv_image = np.array(pil_image.convert("RGB"))[:, :, ::-1] # PIL → OpenCV (BGR)
25
- gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
26
- face_cascade = cv2.CascadeClassifier(CASCADE_PATH)
27
- faces = face_cascade.detectMultiScale(gray, 1.2, 6)
28
-
29
- if len(faces) == 0:
30
- print("⚠️ Kein Gesicht erkannt – keine Maske angewendet.")
31
- return None
32
-
33
- # Nimm das größte erkannte Gesicht
34
- (x, y, w, h) = sorted(faces, key=lambda f: f[2]*f[3], reverse=True)[0]
35
-
36
- # Erweitere den Bereich leicht, um Haare / Stirn einzuschließen
37
- pad = int(h * 0.25)
38
- bbox = (max(0, x - pad), max(0, y - pad),
39
- min(cv_image.shape[1], x + w + pad),
40
- min(cv_image.shape[0], y + h + pad))
41
-
42
- print(f"✅ Gesicht erkannt: {bbox}")
43
- return bbox
44
-
45
- except Exception as e:
46
- print(f"❌ Fehler bei Gesichtserkennung: {e}")
47
- return None
48
-
49
- def create_face_mask(image, bbox):
50
- """Erzeugt eine runde Gesichtsmaske aus Koordinaten."""
51
- mask = Image.new("L", image.size, 0)
52
- if bbox is not None:
53
  draw = ImageDraw.Draw(mask)
54
- draw.ellipse(bbox, fill=255)
 
 
 
 
 
 
 
 
 
 
 
55
  return mask
56
 
 
 
 
 
 
 
 
 
 
 
57
  # === PIPELINES ===
58
  pipe_txt2img = None
59
  pipe_img2img = None
@@ -70,10 +62,8 @@ def load_txt2img():
70
  requires_safety_checker=False
71
  ).to(device)
72
 
73
- # DPMSolver für Text-to-Image
74
  from diffusers import DPMSolverMultistepScheduler
75
  pipe_txt2img.scheduler = DPMSolverMultistepScheduler.from_config(pipe_txt2img.scheduler.config)
76
-
77
  pipe_txt2img.enable_attention_slicing()
78
  return pipe_txt2img
79
 
@@ -89,21 +79,16 @@ def load_img2img():
89
  requires_safety_checker=False
90
  ).to(device)
91
 
92
- # --- OPTIMIERTER SAMPLER ---
93
  from diffusers import DPMSolverMultistepScheduler
94
  pipe_img2img.scheduler = DPMSolverMultistepScheduler.from_config(
95
  pipe_img2img.scheduler.config,
96
  algorithm_type="sde-dpmsolver++",
97
  use_karras_sigmas=True,
98
- # wichtig: linear_multistep statt linspace ergibt natürlichere Übergänge
99
  timestep_spacing="trailing"
100
  )
101
 
102
- # Effiziente Speicherverwaltung
103
  pipe_img2img.enable_attention_slicing()
104
  pipe_img2img.enable_vae_tiling()
105
-
106
- # Optional: für stabilere Farberhaltung
107
  pipe_img2img.vae_slicing = True
108
 
109
  return pipe_img2img
@@ -115,13 +100,14 @@ def text_to_image(prompt, steps, guidance_scale):
115
  return None
116
 
117
  print(f"Starting generation for: {prompt}")
118
- print(f"Parameters - Steps: {steps}, Guidance Scale: {guidance_scale}")
119
  start_time = time.time()
120
 
121
  pipe = load_txt2img()
122
 
123
- # Seed für Reproduzierbarkeit (-1 = zufällig)
124
- generator = torch.Generator(device=device).manual_seed(-1)
 
 
125
 
126
  image = pipe(
127
  prompt=prompt,
@@ -135,7 +121,7 @@ def text_to_image(prompt, steps, guidance_scale):
135
  end_time = time.time()
136
  print(f"✅ Bild generiert in {end_time - start_time:.2f} Sekunden")
137
 
138
- return image # Direkte Rückgabe aus RAM
139
 
140
  except Exception as e:
141
  print(f"❌ Fehler: {e}")
@@ -143,46 +129,62 @@ def text_to_image(prompt, steps, guidance_scale):
143
  traceback.print_exc()
144
  return None
145
 
146
- def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, face_preserve):
147
  try:
148
  if image is None:
149
  return None
150
 
151
  print(f"🧩 Img2Img Start → Strength: {strength}, Steps: {steps}, Guidance: {guidance_scale}")
152
  print(f"Prompt: {prompt}")
153
- print(f"Negative: {neg_prompt}")
154
  print(f"Gesicht beibehalten: {face_preserve}")
155
  start_time = time.time()
156
 
157
  pipe = load_img2img()
158
-
159
- # --- PREPROCESSING ---
160
  img_resized = image.convert("RGB").resize((IMG_SIZE, IMG_SIZE))
161
 
162
  # --- PARAMETER-TUNING ---
163
- # Anpassung, um sde-dpmsolver++ realistischer wirken zu lassen:
164
- adj_strength = min(0.85, strength * 1.3) # mehr effektives Rauschen
165
- adj_guidance = min(guidance_scale, 7.0) # CLIP-Zwang begrenzen
166
-
167
- # --- REPRODUZIERBARER SEED (optional -1 = zufällig) ---
168
- generator = torch.Generator(device=device).manual_seed(-1)
 
169
 
170
- # --- AUTOMATISCHE GESICHTSMASKE ---
171
  mask = None
172
- if face_preserve:
173
- bbox = detect_face_bbox(img_resized)
174
- mask = create_face_mask(img_resized, bbox)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  if mask:
176
- print("✅ Gesichtsmaske erstellt")
177
- else:
178
- print("⚠️ Keine Gesichtsmaske verfügbar")
 
179
 
180
  # --- PIPELINE-AUFRUF ---
181
  result = pipe(
182
  prompt=prompt,
183
  negative_prompt=neg_prompt,
184
  image=img_resized,
185
- mask_image=mask, # Wird None sein wenn face_preserve=False oder kein Gesicht erkannt
186
  strength=adj_strength,
187
  num_inference_steps=int(steps),
188
  guidance_scale=adj_guidance,
@@ -192,27 +194,20 @@ def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, fac
192
  end_time = time.time()
193
  print(f"✅ Bild transformiert in {end_time - start_time:.2f} Sekunden")
194
 
195
- generated_image = result.images[0] # Bild aus RAM
196
-
197
- # === TEMP-SPEICHERUNG ALS FALLBACK ===
198
  try:
199
- # Erstelle Temp-Verzeichnis falls nicht vorhanden
200
  temp_dir = "/tmp/gradio_fallback"
201
  os.makedirs(temp_dir, exist_ok=True)
202
-
203
- # Speichere Bild temporär
204
  temp_path = os.path.join(temp_dir, f"generated_{int(time.time())}.png")
205
  generated_image.save(temp_path, "PNG")
206
- print(f"💾 Bild temporär gespeichert: {temp_path}")
207
-
208
- # Lade Bild wieder für konsistente Rückgabe
209
  saved_image = Image.open(temp_path)
210
-
211
  except Exception as temp_error:
212
  print(f"⚠️ Temp-Speicherung fehlgeschlagen: {temp_error}")
213
- saved_image = generated_image # Fallback auf RAM-Bild
214
 
215
- return saved_image # Rückgabe des gespeicherten oder RAM-Bildes
216
 
217
  except Exception as e:
218
  print(f"❌ Fehler: {e}")
@@ -220,6 +215,14 @@ def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, fac
220
  traceback.print_exc()
221
  return None
222
 
 
 
 
 
 
 
 
 
223
  # === UI ===
224
  with gr.Blocks() as app:
225
  gr.Markdown("# 🎨 AI Bild Generator")
@@ -246,13 +249,6 @@ with gr.Blocks() as app:
246
  label="Guidance Scale (Prompt-Treue vs. Verzerrung)"
247
  )
248
 
249
- with gr.Row():
250
- gr.Markdown(
251
- "**Parameter-Erklärung:** "
252
- "• **Steps:** Mehr = bessere Qualität, aber langsamer "
253
- "• **Guidance:** Niedrig = weniger Verzerrung, Hoch = mehr KI-Fantasie "
254
- )
255
-
256
  generate_btn = gr.Button("🎨 Bild generieren", variant="primary")
257
  txt_output = gr.Image(
258
  label="Generiertes Bild",
@@ -279,15 +275,15 @@ with gr.Blocks() as app:
279
  with gr.Row():
280
  with gr.Column():
281
  img_prompt = gr.Textbox(
282
- placeholder="background only: winter forest, keep girl and snowman unchanged",
283
  lines=2,
284
  label="Transformations-Prompt (Englisch)"
285
  )
286
  with gr.Column():
287
  img_neg_prompt = gr.Textbox(
288
- placeholder="blurry, deformed, ugly, bad anatomy, distorted faces",
289
  lines=2,
290
- label="Negativ-Prompt (was vermieden werden soll)"
291
  )
292
 
293
  with gr.Row():
@@ -299,30 +295,37 @@ with gr.Blocks() as app:
299
  with gr.Column():
300
  img_steps = gr.Slider(
301
  minimum=10, maximum=100, value=35, step=1,
302
- label="Steps (Qualität vs. Geschwindigkeit)"
303
  )
304
  with gr.Column():
305
  img_guidance = gr.Slider(
306
  minimum=1.0, maximum=20.0, value=7.5, step=0.5,
307
- label="Guidance Scale (Prompt-Treue vs. Verzerrung)"
308
  )
309
 
310
- # NEUE CHECKBOX FÜR GESICHTSERHALTUNG
311
  with gr.Row():
312
  face_preserve = gr.Checkbox(
313
- label="👤 Gesicht automatisch erkennen und beibehalten",
314
  value=True,
315
- info="Erkennt Gesichter und schützt sie vor Veränderung"
316
  )
317
 
 
 
 
 
 
 
 
 
 
318
  with gr.Row():
319
  gr.Markdown(
320
- "**Parameter-Erklärung:** "
321
- "• **Strength:** Niedrig = behält Original, Hoch = starke Veränderung "
322
- "• **Steps:** Mehr = bessere Qualität, aber langsamer "
323
- "• **Guidance:** Niedrig = weniger Verzerrung, Hoch = mehr KI-Fantasie "
324
- "• **Negativ-Prompt:** Beschreibt was NICHT im Bild sein soll "
325
- "• **Gesicht beibehalten:** Automatische Gesichtserkennung schützt Porträts "
326
  )
327
 
328
  transform_btn = gr.Button("🔄 Bild transformieren", variant="primary")
@@ -333,9 +336,20 @@ with gr.Blocks() as app:
333
  show_download_button=True
334
  )
335
 
 
 
 
 
 
 
 
336
  transform_btn.click(
337
  fn=img_to_image,
338
- inputs=[img_input, img_prompt, img_neg_prompt, strength_slider, img_steps, img_guidance, face_preserve],
 
 
 
 
339
  outputs=img_output,
340
  concurrency_limit=1
341
  )
 
5
  import time
6
  import os
7
  import tempfile
8
+ import random
 
9
 
10
  # === OPTIMIERTE EINSTELLUNGEN ===
11
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
14
 
15
  print(f"Running on: {device}")
16
 
17
+ # === GESICHTSMASKEN-FUNKTIONEN ===
18
+ def create_face_mask(image, bbox_coords):
19
+ """Erzeugt eine Gesichtsmaske - WEIßE Bereiche werden VERÄNDERT, SCHWARZE BLEIBEN"""
20
+ mask = Image.new("L", image.size, 0) # Start mit komplett schwarzer Maske (alles geschützt)
21
+
22
+ if bbox_coords and all(coord is not None for coord in bbox_coords):
23
+ x1, y1, x2, y2 = bbox_coords
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  draw = ImageDraw.Draw(mask)
25
+
26
+ if face_preserve:
27
+ # GESICHTSERHALTUNG: Maske um das Gesicht herum zeichnen
28
+ # Das Gesicht bleibt schwarz (geschützt), der Rest wird weiß (verändert)
29
+ draw.rectangle([0, 0, image.size[0], image.size[1]], fill=255) # Alles weiß = alles verändern
30
+ draw.ellipse([x1, y1, x2, y2], fill=0) # Gesicht schwarz = geschützt
31
+ print("✅ Gesicht wird GESCHÜTZT - Umgebung wird verändert")
32
+ else:
33
+ # NUR GESICHT VERÄNDERN: Nur das Gesicht wird weiß (verändert), Rest schwarz (geschützt)
34
+ draw.ellipse([x1, y1, x2, y2], fill=255) # Gesicht weiß = verändern
35
+ print("✅ Nur Gesicht wird verändert - Umgebung bleibt erhalten")
36
+
37
  return mask
38
 
39
+ def auto_detect_face_area(image):
40
+ """Vorschlag für Gesichtsbereich"""
41
+ width, height = image.size
42
+ face_size = min(width, height) * 0.4
43
+ x1 = (width - face_size) / 2
44
+ y1 = (height - face_size) / 3
45
+ x2 = x1 + face_size
46
+ y2 = y1 + face_size
47
+ return [int(x1), int(y1), int(x2), int(y2)]
48
+
49
  # === PIPELINES ===
50
  pipe_txt2img = None
51
  pipe_img2img = None
 
62
  requires_safety_checker=False
63
  ).to(device)
64
 
 
65
  from diffusers import DPMSolverMultistepScheduler
66
  pipe_txt2img.scheduler = DPMSolverMultistepScheduler.from_config(pipe_txt2img.scheduler.config)
 
67
  pipe_txt2img.enable_attention_slicing()
68
  return pipe_txt2img
69
 
 
79
  requires_safety_checker=False
80
  ).to(device)
81
 
 
82
  from diffusers import DPMSolverMultistepScheduler
83
  pipe_img2img.scheduler = DPMSolverMultistepScheduler.from_config(
84
  pipe_img2img.scheduler.config,
85
  algorithm_type="sde-dpmsolver++",
86
  use_karras_sigmas=True,
 
87
  timestep_spacing="trailing"
88
  )
89
 
 
90
  pipe_img2img.enable_attention_slicing()
91
  pipe_img2img.enable_vae_tiling()
 
 
92
  pipe_img2img.vae_slicing = True
93
 
94
  return pipe_img2img
 
100
  return None
101
 
102
  print(f"Starting generation for: {prompt}")
 
103
  start_time = time.time()
104
 
105
  pipe = load_txt2img()
106
 
107
+ # ZUFÄLLIGER SEED für Variation
108
+ seed = random.randint(0, 2**32 - 1)
109
+ generator = torch.Generator(device=device).manual_seed(seed)
110
+ print(f"🎲 Using seed: {seed}")
111
 
112
  image = pipe(
113
  prompt=prompt,
 
121
  end_time = time.time()
122
  print(f"✅ Bild generiert in {end_time - start_time:.2f} Sekunden")
123
 
124
+ return image
125
 
126
  except Exception as e:
127
  print(f"❌ Fehler: {e}")
 
129
  traceback.print_exc()
130
  return None
131
 
132
+ def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, face_preserve, bbox_x1, bbox_y1, bbox_x2, bbox_y2):
133
  try:
134
  if image is None:
135
  return None
136
 
137
  print(f"🧩 Img2Img Start → Strength: {strength}, Steps: {steps}, Guidance: {guidance_scale}")
138
  print(f"Prompt: {prompt}")
 
139
  print(f"Gesicht beibehalten: {face_preserve}")
140
  start_time = time.time()
141
 
142
  pipe = load_img2img()
 
 
143
  img_resized = image.convert("RGB").resize((IMG_SIZE, IMG_SIZE))
144
 
145
  # --- PARAMETER-TUNING ---
146
+ adj_strength = min(0.85, strength * 1.3)
147
+ adj_guidance = min(guidance_scale, 7.0)
148
+
149
+ # ZUFÄLLIGER SEED für Variation
150
+ seed = random.randint(0, 2**32 - 1)
151
+ generator = torch.Generator(device=device).manual_seed(seed)
152
+ print(f"🎲 Using seed: {seed}")
153
 
154
+ # --- GESICHTSMASKE ---
155
  mask = None
156
+ bbox_coords = None
157
+
158
+ if bbox_x1 is not None and bbox_y1 is not None and bbox_x2 is not None and bbox_y2 is not None:
159
+ # Skaliere Koordinaten auf die neue Bildgröße
160
+ orig_width, orig_height = image.size
161
+ scale_x = IMG_SIZE / orig_width
162
+ scale_y = IMG_SIZE / orig_height
163
+
164
+ scaled_coords = [
165
+ int(bbox_x1 * scale_x),
166
+ int(bbox_y1 * scale_y),
167
+ int(bbox_x2 * scale_x),
168
+ int(bbox_y2 * scale_y)
169
+ ]
170
+ bbox_coords = scaled_coords
171
+ print(f"📐 Skalierte Koordinaten: {scaled_coords}")
172
+
173
+ # Maskenlogik basierend auf face_preserve
174
+ if bbox_coords:
175
+ mask = create_face_mask(img_resized, bbox_coords)
176
  if mask:
177
+ print("✅ Maske erfolgreich erstellt")
178
+ else:
179
+ print("⚠️ Keine gültigen Koordinaten - keine Maske angewendet")
180
+ mask = None
181
 
182
  # --- PIPELINE-AUFRUF ---
183
  result = pipe(
184
  prompt=prompt,
185
  negative_prompt=neg_prompt,
186
  image=img_resized,
187
+ mask_image=mask, # None = gesamtes Bild verändern
188
  strength=adj_strength,
189
  num_inference_steps=int(steps),
190
  guidance_scale=adj_guidance,
 
194
  end_time = time.time()
195
  print(f"✅ Bild transformiert in {end_time - start_time:.2f} Sekunden")
196
 
197
+ generated_image = result.images[0]
198
+
199
+ # Temp-Speicherung
200
  try:
 
201
  temp_dir = "/tmp/gradio_fallback"
202
  os.makedirs(temp_dir, exist_ok=True)
 
 
203
  temp_path = os.path.join(temp_dir, f"generated_{int(time.time())}.png")
204
  generated_image.save(temp_path, "PNG")
 
 
 
205
  saved_image = Image.open(temp_path)
 
206
  except Exception as temp_error:
207
  print(f"⚠️ Temp-Speicherung fehlgeschlagen: {temp_error}")
208
+ saved_image = generated_image
209
 
210
+ return saved_image
211
 
212
  except Exception as e:
213
  print(f"❌ Fehler: {e}")
 
215
  traceback.print_exc()
216
  return None
217
 
218
+ def update_bbox_from_image(image):
219
+ """Aktualisiert die Bounding-Box-Koordinaten wenn ein Bild hochgeladen wird"""
220
+ if image is None:
221
+ return None, None, None, None
222
+
223
+ bbox = auto_detect_face_area(image)
224
+ return bbox[0], bbox[1], bbox[2], bbox[3]
225
+
226
  # === UI ===
227
  with gr.Blocks() as app:
228
  gr.Markdown("# 🎨 AI Bild Generator")
 
249
  label="Guidance Scale (Prompt-Treue vs. Verzerrung)"
250
  )
251
 
 
 
 
 
 
 
 
252
  generate_btn = gr.Button("🎨 Bild generieren", variant="primary")
253
  txt_output = gr.Image(
254
  label="Generiertes Bild",
 
275
  with gr.Row():
276
  with gr.Column():
277
  img_prompt = gr.Textbox(
278
+ placeholder="change background to forest, keep face unchanged",
279
  lines=2,
280
  label="Transformations-Prompt (Englisch)"
281
  )
282
  with gr.Column():
283
  img_neg_prompt = gr.Textbox(
284
+ placeholder="blurry, deformed, ugly, bad anatomy",
285
  lines=2,
286
+ label="Negativ-Prompt"
287
  )
288
 
289
  with gr.Row():
 
295
  with gr.Column():
296
  img_steps = gr.Slider(
297
  minimum=10, maximum=100, value=35, step=1,
298
+ label="Steps"
299
  )
300
  with gr.Column():
301
  img_guidance = gr.Slider(
302
  minimum=1.0, maximum=20.0, value=7.5, step=0.5,
303
+ label="Guidance Scale"
304
  )
305
 
306
+ # GESICHTSOPTIONEN
307
  with gr.Row():
308
  face_preserve = gr.Checkbox(
309
+ label="👤 Gesicht beibehalten (Umgebung verändern)",
310
  value=True,
311
+ info="Gesicht bleibt erhalten, Hintergrund wird verändert"
312
  )
313
 
314
+ with gr.Row():
315
+ gr.Markdown("**Gesichtsbereich definieren (x1, y1, x2, y2):**")
316
+
317
+ with gr.Row():
318
+ bbox_x1 = gr.Number(label="x1 (links)", value=100, precision=0)
319
+ bbox_y1 = gr.Number(label="y1 (oben)", value=100, precision=0)
320
+ bbox_x2 = gr.Number(label="x2 (rechts)", value=300, precision=0)
321
+ bbox_y2 = gr.Number(label="y2 (unten)", value=300, precision=0)
322
+
323
  with gr.Row():
324
  gr.Markdown(
325
+ "**Anleitung:** "
326
+ "• **Gesicht beibehalten** = Gesicht bleibt, Hintergrund ändert sich "
327
+ "• **Nicht aktiviert** = Nur Gesicht ändert sich, Hintergrund bleibt "
328
+ "• **Koordinaten anpassen** um den genauen Bereich zu definieren "
 
 
329
  )
330
 
331
  transform_btn = gr.Button("🔄 Bild transformieren", variant="primary")
 
336
  show_download_button=True
337
  )
338
 
339
+ # Event-Handler für Bild-Upload
340
+ img_input.change(
341
+ fn=update_bbox_from_image,
342
+ inputs=[img_input],
343
+ outputs=[bbox_x1, bbox_y1, bbox_x2, bbox_y2]
344
+ )
345
+
346
  transform_btn.click(
347
  fn=img_to_image,
348
+ inputs=[
349
+ img_input, img_prompt, img_neg_prompt,
350
+ strength_slider, img_steps, img_guidance,
351
+ face_preserve, bbox_x1, bbox_y1, bbox_x2, bbox_y2
352
+ ],
353
  outputs=img_output,
354
  concurrency_limit=1
355
  )