ArmanRV commited on
Commit
d59a1ed
·
verified ·
1 Parent(s): 4f2b108

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -101
app.py CHANGED
@@ -183,27 +183,23 @@ def allow_call(min_interval_sec: float = 2.5) -> Tuple[bool, str]:
183
  return True, ""
184
 
185
 
 
 
 
186
  def check_photo_quality(img: Optional[Image.Image]) -> Tuple[str, str]:
187
- """
188
- 2) Авто-проверка фото. Только подсказки, ничего не блокируем.
189
- Возвращает: (markdown_tips, diagnostics_line)
190
- """
191
  if img is None:
192
  return ("Загрузите фото — здесь появятся советы.", "")
193
 
194
  img = img.convert("RGB")
195
  w, h = img.size
196
 
197
- # яркость
198
  gray = np.array(img.convert("L"))
199
  brightness = float(gray.mean())
200
 
201
- # резкость (простая эвристика без OpenCV)
202
  gy, gx = np.gradient(gray.astype(np.float32))
203
  sharpness = float((gx * gx + gy * gy).mean())
204
 
205
  warnings = []
206
-
207
  if min(w, h) < 768:
208
  warnings.append("Фото маленькое: лучше **1024px+** по меньшей стороне.")
209
  if h < w:
@@ -215,20 +211,17 @@ def check_photo_quality(img: Optional[Image.Image]) -> Tuple[str, str]:
215
  if sharpness < 15:
216
  warnings.append("Фото может быть размытым — сделайте снимок **без движения** и с фокусом.")
217
 
218
- # короткий гайд всегда
219
  base = [
220
  "Стоять прямо, камера примерно на уровне груди",
221
  "Руки не закрывают торс",
222
  "Однотонный фон, без зеркал и сильных теней",
223
- "Лучше фото **по пояс или в полный рост**"
224
  ]
225
 
226
- tips_md = "### 📸 Как получить лучший результат\n"
227
- tips_md += "\n".join([f"- {x}" for x in base])
228
 
229
  if warnings:
230
- tips_md += "\n\n### ⚠️ Что можно улучшить именно в вашем фото\n"
231
- tips_md += "\n".join([f"- {w}" for w in warnings])
232
  else:
233
  tips_md += "\n\n✅ Фото выглядит хорошо — можно примерять."
234
 
@@ -236,18 +229,15 @@ def check_photo_quality(img: Optional[Image.Image]) -> Tuple[str, str]:
236
  return tips_md, diag
237
 
238
 
 
 
 
239
  def fit_to_params(fit: str) -> Tuple[int, float]:
240
- """
241
- 4) Посадка -> параметры. Подбираем мягко, чтобы не ломать качество.
242
- Возвращает: (denoise_steps, guidance_scale)
243
- """
244
- # ВАЖНО: в твоём пайплайне guidance_scale сейчас 2.0 — держим около этого.
245
- # Меняем аккуратно, чтобы пользователь видел разницу, но без деградации.
246
  if fit == "По фигуре":
247
  return 28, 2.2
248
  if fit == "Свободная":
249
  return 25, 2.0
250
- # Оверсайз
251
  return 22, 1.8
252
 
253
 
@@ -260,9 +250,7 @@ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
260
  DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
261
  print("DEVICE:", DEVICE, "DTYPE:", DTYPE, flush=True)
262
 
263
- tensor_transfrom = transforms.Compose(
264
- [transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
265
- )
266
 
267
  unet = UNet2DConditionModel.from_pretrained(base_path, subfolder="unet", torch_dtype=DTYPE)
268
  unet.requires_grad_(False)
@@ -316,11 +304,9 @@ def start_tryon(
316
  seed: int = 42,
317
  guidance_scale: float = 2.0,
318
  ) -> Image.Image:
319
-
320
  device = "cuda" if torch.cuda.is_available() else "cpu"
321
  dtype = torch.float16 if device == "cuda" else torch.float32
322
 
323
- # Move models
324
  if device == "cuda":
325
  openpose_model.preprocessor.body_estimation.model.to(device)
326
  pipe.to(device)
@@ -329,7 +315,6 @@ def start_tryon(
329
  garm_img = garm_img.convert("RGB").resize((768, 1024))
330
  human_img_orig = human_pil.convert("RGB")
331
 
332
- # Crop
333
  if crop_center:
334
  width, height = human_img_orig.size
335
  target_width = int(min(width, height * (3 / 4)))
@@ -343,8 +328,9 @@ def start_tryon(
343
  human_img = cropped_img.resize((768, 1024))
344
  else:
345
  human_img = human_img_orig.resize((768, 1024))
 
 
346
 
347
- # Mask
348
  if auto_mask:
349
  keypoints = openpose_model(human_img.resize((384, 512)))
350
  model_parse, _ = parsing_model(human_img.resize((384, 512)))
@@ -353,7 +339,6 @@ def start_tryon(
353
  else:
354
  mask = Image.new("L", (768, 1024), 0)
355
 
356
- # DensePose
357
  human_img_arg = _apply_exif_orientation(human_img.resize((384, 512)))
358
  human_img_arg = convert_PIL_to_numpy(human_img_arg, format="BGR")
359
 
@@ -371,7 +356,6 @@ def start_tryon(
371
  pose_img = pose_img[:, :, ::-1]
372
  pose_img = Image.fromarray(pose_img).resize((768, 1024))
373
 
374
- # Fixed prompts
375
  garment_des = "a garment"
376
  prompt_main = "model is wearing " + garment_des
377
  prompt_cloth = "a photo of " + garment_des
@@ -440,7 +424,7 @@ def start_tryon(
440
  )[0]
441
 
442
  out_img = images[0]
443
- if crop_center:
444
  out_img_rs = out_img.resize(crop_size)
445
  human_img_orig.paste(out_img_rs, (int(left), int(top)))
446
  return human_img_orig
@@ -475,56 +459,37 @@ def on_person_change(person_pil):
475
  tips_md, diag = check_photo_quality(person_pil)
476
  return tips_md, diag
477
 
478
- def build_compare_components():
479
- """
480
- 5) До/После: если доступен ImageSlider — используем.
481
- Если нет — fallback на 2 изображения.
482
- """
483
- if hasattr(gr, "ImageSlider"):
484
- return gr.ImageSlider(label="До / После"), "imageslider"
485
- # fallback
486
- with gr.Row():
487
- before = gr.Image(label="До", type="pil", height=360)
488
- after = gr.Image(label="После", type="pil", height=360)
489
- return (before, after), "pair"
490
-
491
- def pack_compare_output(mode: str, before_img: Image.Image, after_img: Image.Image):
492
- if mode == "imageslider":
493
- return (before_img, after_img)
494
- return before_img, after_img
495
 
496
- def tryon_ui(person_pil, selected_filename, fit_choice):
497
- # 3) “магический” прогресс — этапы через status (надежнее, чем гонять Progress в генераторе)
498
- yield _empty_compare_payload(), "⏳ Проверяем ввод..."
499
 
500
  ok, msg = allow_call(2.5)
501
  if not ok:
502
- yield _empty_compare_payload(), msg
503
  return
504
 
505
  if person_pil is None:
506
- yield _empty_compare_payload(), "❌ Загрузите фото человека"
507
  return
508
  if not selected_filename:
509
- yield _empty_compare_payload(), "❌ Выберите одежду (клик по превью)"
510
  return
511
 
512
  garm = load_garment_pil(selected_filename)
513
  if garm is None:
514
- yield _empty_compare_payload(), "❌ Не удалось загрузить выбранную одежду"
515
  return
516
 
517
- # параметры от посадки
518
  denoise_steps, guidance_scale = fit_to_params(fit_choice)
 
519
 
520
- # 2) подсказки (не блокируем)
521
- tips_md, diag = check_photo_quality(person_pil)
522
-
523
- yield _empty_compare_payload(), f"🧠 Анализируем позу и силуэт... {diag}"
524
  time.sleep(0.05)
525
- yield _empty_compare_payload(), "🧵 Подгоняем посадку..."
526
  time.sleep(0.05)
527
- yield _empty_compare_payload(), "✨ Примеряем ткань..."
528
 
529
  try:
530
  out = start_tryon(
@@ -536,16 +501,54 @@ def tryon_ui(person_pil, selected_filename, fit_choice):
536
  seed=42,
537
  guidance_scale=guidance_scale,
538
  )
539
- payload = pack_compare_output(COMPARE_MODE, person_pil, out)
540
- yield payload, "✅ Готово"
541
  except Exception as e:
542
- yield _empty_compare_payload(), f"❌ Ошибка: {type(e).__name__}: {str(e)[:220]}"
 
543
 
544
- def _empty_compare_payload():
545
- # Нужно возвращать "правильной формы" значения, иначе Gradio ругается на типы.
546
- if COMPARE_MODE == "imageslider":
547
- return (None, None)
548
- return (None, None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
 
551
  # Preload garments
@@ -563,11 +566,8 @@ with gr.Blocks(title="Virtual Try-On Rendez-vous", css=CUSTOM_CSS) as demo:
563
  with gr.Column():
564
  person = gr.Image(label="Фото человека", type="pil", height=420)
565
 
566
- # 2) Подсказки + диагностика
567
  tips = gr.Markdown("Загрузите фото — здесь появятся советы.")
568
  diag = gr.Markdown("")
569
-
570
- # обновляем подсказки при загрузке/смене фото
571
  person.change(fn=on_person_change, inputs=[person], outputs=[tips, diag])
572
 
573
  with gr.Row():
@@ -582,7 +582,6 @@ with gr.Blocks(title="Virtual Try-On Rendez-vous", css=CUSTOM_CSS) as demo:
582
  allow_preview=True,
583
  )
584
 
585
- # 4) Посадка
586
  fit = gr.Radio(
587
  ["По фигуре", "Свободная", "Оверсайз"],
588
  value="Свободная",
@@ -593,16 +592,26 @@ with gr.Blocks(title="Virtual Try-On Rendez-vous", css=CUSTOM_CSS) as demo:
593
  status = gr.Textbox(value="Ожидание...", interactive=False)
594
 
595
  with gr.Column():
596
- # 5) До/После
597
- compare_component, mode = build_compare_components()
598
- COMPARE_MODE = mode # задаём глобально, чтобы generator правильно формировал payload
599
-
600
- # если есть ImageSlider, он один компонент; если fallback — два
601
- if COMPARE_MODE == "imageslider":
602
- compare_out = compare_component
 
 
 
603
  else:
604
- before_img, after_img = compare_component
605
- compare_out = (before_img, after_img)
 
 
 
 
 
 
 
606
 
607
  garment_gallery.select(
608
  fn=on_gallery_select,
@@ -616,25 +625,8 @@ with gr.Blocks(title="Virtual Try-On Rendez-vous", css=CUSTOM_CSS) as demo:
616
  outputs=[garment_gallery, garment_files_state, selected_garment_state, status],
617
  )
618
 
619
- # Выходы зависят от режима сравнения
620
- if COMPARE_MODE == "imageslider":
621
- run.click(
622
- fn=tryon_ui,
623
- inputs=[person, selected_garment_state, fit],
624
- outputs=[compare_out, status],
625
- concurrency_limit=1,
626
- )
627
- else:
628
- before_img, after_img = compare_out
629
- run.click(
630
- fn=tryon_ui,
631
- inputs=[person, selected_garment_state, fit],
632
- outputs=[before_img, after_img, status],
633
- concurrency_limit=1,
634
- )
635
-
636
- # Важно для L4: очередь + 1 параллельный инференс
637
- demo.queue(concurrency_count=1, max_size=20)
638
 
639
  if __name__ == "__main__":
640
  demo.launch(
 
183
  return True, ""
184
 
185
 
186
+ # =========================
187
+ # 2) Photo quality tips (CPU-only)
188
+ # =========================
189
  def check_photo_quality(img: Optional[Image.Image]) -> Tuple[str, str]:
 
 
 
 
190
  if img is None:
191
  return ("Загрузите фото — здесь появятся советы.", "")
192
 
193
  img = img.convert("RGB")
194
  w, h = img.size
195
 
 
196
  gray = np.array(img.convert("L"))
197
  brightness = float(gray.mean())
198
 
 
199
  gy, gx = np.gradient(gray.astype(np.float32))
200
  sharpness = float((gx * gx + gy * gy).mean())
201
 
202
  warnings = []
 
203
  if min(w, h) < 768:
204
  warnings.append("Фото маленькое: лучше **1024px+** по меньшей стороне.")
205
  if h < w:
 
211
  if sharpness < 15:
212
  warnings.append("Фото может быть размытым — сделайте снимок **без движения** и с фокусом.")
213
 
 
214
  base = [
215
  "Стоять прямо, камера примерно на уровне груди",
216
  "Руки не закрывают торс",
217
  "Однотонный фон, без зеркал и сильных теней",
218
+ "Лучше фото **по пояс или в полный рост**",
219
  ]
220
 
221
+ tips_md = "### 📸 Как получить лучший результат\n" + "\n".join([f"- {x}" for x in base])
 
222
 
223
  if warnings:
224
+ tips_md += "\n\n### ⚠️ Что можно улучшить именно в вашем фото\n" + "\n".join([f"- {w}" for w in warnings])
 
225
  else:
226
  tips_md += "\n\n✅ Фото выглядит хорошо — можно примерять."
227
 
 
229
  return tips_md, diag
230
 
231
 
232
+ # =========================
233
+ # 4) Fit option -> params
234
+ # =========================
235
  def fit_to_params(fit: str) -> Tuple[int, float]:
236
+ # Поддерживаем вокруг твоего базового guidance=2.0
 
 
 
 
 
237
  if fit == "По фигуре":
238
  return 28, 2.2
239
  if fit == "Свободная":
240
  return 25, 2.0
 
241
  return 22, 1.8
242
 
243
 
 
250
  DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
251
  print("DEVICE:", DEVICE, "DTYPE:", DTYPE, flush=True)
252
 
253
+ tensor_transfrom = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
 
 
254
 
255
  unet = UNet2DConditionModel.from_pretrained(base_path, subfolder="unet", torch_dtype=DTYPE)
256
  unet.requires_grad_(False)
 
304
  seed: int = 42,
305
  guidance_scale: float = 2.0,
306
  ) -> Image.Image:
 
307
  device = "cuda" if torch.cuda.is_available() else "cpu"
308
  dtype = torch.float16 if device == "cuda" else torch.float32
309
 
 
310
  if device == "cuda":
311
  openpose_model.preprocessor.body_estimation.model.to(device)
312
  pipe.to(device)
 
315
  garm_img = garm_img.convert("RGB").resize((768, 1024))
316
  human_img_orig = human_pil.convert("RGB")
317
 
 
318
  if crop_center:
319
  width, height = human_img_orig.size
320
  target_width = int(min(width, height * (3 / 4)))
 
328
  human_img = cropped_img.resize((768, 1024))
329
  else:
330
  human_img = human_img_orig.resize((768, 1024))
331
+ crop_size = None
332
+ left = top = None
333
 
 
334
  if auto_mask:
335
  keypoints = openpose_model(human_img.resize((384, 512)))
336
  model_parse, _ = parsing_model(human_img.resize((384, 512)))
 
339
  else:
340
  mask = Image.new("L", (768, 1024), 0)
341
 
 
342
  human_img_arg = _apply_exif_orientation(human_img.resize((384, 512)))
343
  human_img_arg = convert_PIL_to_numpy(human_img_arg, format="BGR")
344
 
 
356
  pose_img = pose_img[:, :, ::-1]
357
  pose_img = Image.fromarray(pose_img).resize((768, 1024))
358
 
 
359
  garment_des = "a garment"
360
  prompt_main = "model is wearing " + garment_des
361
  prompt_cloth = "a photo of " + garment_des
 
424
  )[0]
425
 
426
  out_img = images[0]
427
+ if crop_center and crop_size is not None:
428
  out_img_rs = out_img.resize(crop_size)
429
  human_img_orig.paste(out_img_rs, (int(left), int(top)))
430
  return human_img_orig
 
459
  tips_md, diag = check_photo_quality(person_pil)
460
  return tips_md, diag
461
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
 
463
+ def tryon_ui_imageslider(person_pil, selected_filename, fit_choice):
464
+ # outputs: (slider_tuple, status)
465
+ yield (None, None), "⏳ Проверяем ввод..."
466
 
467
  ok, msg = allow_call(2.5)
468
  if not ok:
469
+ yield (None, None), msg
470
  return
471
 
472
  if person_pil is None:
473
+ yield (None, None), "❌ Загрузите фото человека"
474
  return
475
  if not selected_filename:
476
+ yield (None, None), "❌ Выберите одежду (клик по превью)"
477
  return
478
 
479
  garm = load_garment_pil(selected_filename)
480
  if garm is None:
481
+ yield (None, None), "❌ Не удалось загрузить выбранную одежду"
482
  return
483
 
 
484
  denoise_steps, guidance_scale = fit_to_params(fit_choice)
485
+ _, diag = check_photo_quality(person_pil)
486
 
487
+ # 3) progress-stages
488
+ yield (None, None), f"🧠 Анализируем позу и силуэт... {diag}"
 
 
489
  time.sleep(0.05)
490
+ yield (None, None), "🧵 Подгоняем посадку..."
491
  time.sleep(0.05)
492
+ yield (None, None), "✨ Примеряем ткань..."
493
 
494
  try:
495
  out = start_tryon(
 
501
  seed=42,
502
  guidance_scale=guidance_scale,
503
  )
504
+ yield (person_pil, out), "✅ Готово"
 
505
  except Exception as e:
506
+ yield (None, None), f"❌ Ошибка: {type(e).__name__}: {str(e)[:220]}"
507
+
508
 
509
+ def tryon_ui_pair(person_pil, selected_filename, fit_choice):
510
+ # outputs: (before, after, status)
511
+ yield None, None, "⏳ Проверяем ввод..."
512
+
513
+ ok, msg = allow_call(2.5)
514
+ if not ok:
515
+ yield None, None, msg
516
+ return
517
+
518
+ if person_pil is None:
519
+ yield None, None, "❌ Загрузите фото человека"
520
+ return
521
+ if not selected_filename:
522
+ yield None, None, "❌ Выберите одежду (клик по превью)"
523
+ return
524
+
525
+ garm = load_garment_pil(selected_filename)
526
+ if garm is None:
527
+ yield None, None, "❌ Не удалось загрузить выбранную одежду"
528
+ return
529
+
530
+ denoise_steps, guidance_scale = fit_to_params(fit_choice)
531
+ _, diag = check_photo_quality(person_pil)
532
+
533
+ yield None, None, f"🧠 Анализируем позу и силуэт... {diag}"
534
+ time.sleep(0.05)
535
+ yield None, None, "🧵 Подгоняем посадку..."
536
+ time.sleep(0.05)
537
+ yield None, None, "✨ Примеряем ткань..."
538
+
539
+ try:
540
+ out = start_tryon(
541
+ human_pil=person_pil,
542
+ garm_img=garm,
543
+ auto_mask=True,
544
+ crop_center=True,
545
+ denoise_steps=denoise_steps,
546
+ seed=42,
547
+ guidance_scale=guidance_scale,
548
+ )
549
+ yield person_pil, out, "✅ Готово"
550
+ except Exception as e:
551
+ yield None, None, f"❌ Ошибка: {type(e).__name__}: {str(e)[:220]}"
552
 
553
 
554
  # Preload garments
 
566
  with gr.Column():
567
  person = gr.Image(label="Фото человека", type="pil", height=420)
568
 
 
569
  tips = gr.Markdown("Загрузите фото — здесь появятся советы.")
570
  diag = gr.Markdown("")
 
 
571
  person.change(fn=on_person_change, inputs=[person], outputs=[tips, diag])
572
 
573
  with gr.Row():
 
582
  allow_preview=True,
583
  )
584
 
 
585
  fit = gr.Radio(
586
  ["По фигуре", "Свободная", "Оверсайз"],
587
  value="Свободная",
 
592
  status = gr.Textbox(value="Ожидание...", interactive=False)
593
 
594
  with gr.Column():
595
+ gr.Markdown("### Результат (До / После)")
596
+
597
+ if hasattr(gr, "ImageSlider"):
598
+ compare = gr.ImageSlider(label="До / После")
599
+ run.click(
600
+ fn=tryon_ui_imageslider,
601
+ inputs=[person, selected_garment_state, fit],
602
+ outputs=[compare, status],
603
+ concurrency_limit=1,
604
+ )
605
  else:
606
+ with gr.Row():
607
+ before_img = gr.Image(label="До", type="pil", height=360)
608
+ after_img = gr.Image(label="После", type="pil", height=360)
609
+ run.click(
610
+ fn=tryon_ui_pair,
611
+ inputs=[person, selected_garment_state, fit],
612
+ outputs=[before_img, after_img, status],
613
+ concurrency_limit=1,
614
+ )
615
 
616
  garment_gallery.select(
617
  fn=on_gallery_select,
 
625
  outputs=[garment_gallery, garment_files_state, selected_garment_state, status],
626
  )
627
 
628
+ # Gradio 4.24: НЕ используем concurrency_count, иначе падает
629
+ demo.queue(max_size=20)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
 
631
  if __name__ == "__main__":
632
  demo.launch(