midman79 commited on
Commit
7479ec5
·
verified ·
1 Parent(s): 55340d5

Fix all 2

Browse files
Files changed (1) hide show
  1. app.py +864 -435
app.py CHANGED
@@ -1,11 +1,10 @@
1
  import spaces
2
 
3
  from typing import Tuple, Union, List
4
- import os
5
  import gc
6
 
7
  import numpy as np
8
- from PIL import Image, ImageFilter
9
 
10
  import torch
11
 
@@ -22,6 +21,7 @@ from transformers import (
22
  AutoImageProcessor,
23
  UperNetForSemanticSegmentation,
24
  AutoModelForDepthEstimation,
 
25
  )
26
 
27
  from colors import ade_palette
@@ -31,15 +31,19 @@ import gradio as gr
31
 
32
 
33
  # ============================================================
34
- # CONFIG
35
  # ============================================================
36
 
37
  device = "cuda"
38
  dtype = torch.float16
39
 
 
 
 
 
40
 
41
  # ============================================================
42
- # UI CSS
43
  # ============================================================
44
 
45
  css = """
@@ -58,26 +62,26 @@ css = """
58
 
59
 
60
  # ============================================================
61
- # UTILS
62
  # ============================================================
63
 
64
  def flush():
65
- """
66
- Release unused GPU memory.
67
- """
68
-
69
  gc.collect()
70
 
71
  if torch.cuda.is_available():
72
  torch.cuda.empty_cache()
73
 
74
 
 
 
 
 
75
  def resize_dimensions(
76
  dimensions,
77
  target_size
78
  ):
79
  """
80
- Resize while preserving the original aspect ratio.
81
  """
82
 
83
  width, height = dimensions
@@ -85,34 +89,31 @@ def resize_dimensions(
85
  if width <= target_size and height <= target_size:
86
  return dimensions
87
 
88
- if width > height:
89
 
90
- aspect_ratio = height / width
91
 
92
  return (
93
  target_size,
94
- max(64, int(target_size * aspect_ratio))
95
  )
96
 
97
- aspect_ratio = width / height
98
 
99
  return (
100
- max(64, int(target_size * aspect_ratio)),
101
  target_size
102
  )
103
 
104
 
105
- def resize_keep_aspect(
106
  image: Image.Image,
107
- target_width: int,
108
- target_height: int,
109
- background=(255, 255, 255)
110
  ):
111
  """
112
- Resize an image preserving its aspect ratio and place it
113
- in a canvas of the requested size.
114
-
115
- This is intentionally NOT a stretch resize.
116
  """
117
 
118
  image = image.convert("RGB")
@@ -120,12 +121,19 @@ def resize_keep_aspect(
120
  src_w, src_h = image.size
121
 
122
  scale = min(
123
- target_width / src_w,
124
- target_height / src_h
125
  )
126
 
127
- new_w = max(1, int(src_w * scale))
128
- new_h = max(1, int(src_h * scale))
 
 
 
 
 
 
 
129
 
130
  resized = image.resize(
131
  (new_w, new_h),
@@ -134,12 +142,12 @@ def resize_keep_aspect(
134
 
135
  canvas = Image.new(
136
  "RGB",
137
- (target_width, target_height),
138
  background
139
  )
140
 
141
- x = (target_width - new_w) // 2
142
- y = (target_height - new_h) // 2
143
 
144
  canvas.paste(
145
  resized,
@@ -149,93 +157,35 @@ def resize_keep_aspect(
149
  return canvas
150
 
151
 
152
- def create_binary_mask_from_segmentation(
153
- segmentation: Image.Image,
154
- wanted_items: List[str]
155
- ):
156
- """
157
- Create a binary mask from an ADE20K semantic segmentation.
158
-
159
- Pixels belonging to the requested classes become white.
160
- """
161
-
162
- seg_rgb = np.array(segmentation)
163
-
164
- unique_colors = np.unique(
165
- seg_rgb.reshape(
166
- -1,
167
- seg_rgb.shape[2]
168
- ),
169
- axis=0
170
- )
171
-
172
- mask = np.zeros(
173
- seg_rgb.shape[:2],
174
- dtype=np.uint8
175
- )
176
-
177
- for color in unique_colors:
178
-
179
- color_tuple = tuple(
180
- int(v)
181
- for v in color
182
- )
183
-
184
- item_name = map_colors_rgb(
185
- color_tuple
186
- )
187
-
188
- if item_name in wanted_items:
189
-
190
- matches = (
191
- seg_rgb == color
192
- ).all(axis=2)
193
-
194
- mask[matches] = 255
195
-
196
- return Image.fromarray(
197
- mask,
198
- mode="L"
199
- )
200
-
201
-
202
  # ============================================================
203
  # SEGMENTATION
204
  # ============================================================
205
 
206
  def get_segmentation_pipeline():
207
- """
208
- Load ADE20K semantic segmentation.
209
- """
210
 
211
- image_processor = AutoImageProcessor.from_pretrained(
212
  "openmmlab/upernet-convnext-small"
213
  )
214
 
215
- image_segmentor = (
216
- UperNetForSemanticSegmentation.from_pretrained(
217
- "openmmlab/upernet-convnext-small"
218
- )
219
  )
220
 
221
- return (
222
- image_processor,
223
- image_segmentor
224
- )
225
 
226
 
227
  @torch.inference_mode()
228
  @spaces.GPU
229
  def segment_image(
230
  image: Image.Image,
231
- image_processor: AutoImageProcessor,
232
- image_segmentor: UperNetForSemanticSegmentation
233
- ) -> Image.Image:
234
  """
235
- Generate an RGB ADE20K segmentation image.
236
  """
237
 
238
- pixel_values = image_processor(
239
  image,
240
  return_tensors="pt"
241
  ).pixel_values.to(device)
@@ -243,10 +193,10 @@ def segment_image(
243
  with torch.no_grad():
244
 
245
  outputs = image_segmentor(
246
- pixel_values
247
  )
248
 
249
- seg = (
250
  image_processor
251
  .post_process_semantic_segmentation(
252
  outputs,
@@ -255,17 +205,18 @@ def segment_image(
255
  )
256
 
257
  # IMPORTANT:
258
- # Move tensor from CUDA to CPU before NumPy.
259
- seg = (
260
- seg.detach()
 
261
  .cpu()
262
  .numpy()
263
  )
264
 
265
  color_seg = np.zeros(
266
  (
267
- seg.shape[0],
268
- seg.shape[1],
269
  3
270
  ),
271
  dtype=np.uint8
@@ -278,7 +229,7 @@ def segment_image(
278
  for label, color in enumerate(palette):
279
 
280
  color_seg[
281
- seg == label,
282
  :
283
  ] = color
284
 
@@ -313,79 +264,83 @@ def get_depth_pipeline():
313
  @spaces.GPU
314
  def get_depth_image(
315
  image: Image.Image,
316
- feature_extractor: AutoImageProcessor,
317
- depth_estimator: AutoModelForDepthEstimation
318
- ) -> Image.Image:
319
- """
320
- Generate normalized depth image.
321
- """
322
 
323
- image_to_depth = feature_extractor(
324
  images=image,
325
  return_tensors="pt"
326
  ).to(device)
327
 
328
  with torch.no_grad():
329
 
330
- depth_map = (
331
  depth_estimator(
332
- **image_to_depth
333
  )
334
  .predicted_depth
335
  )
336
 
337
  width, height = image.size
338
 
339
- depth_map = torch.nn.functional.interpolate(
340
- depth_map.unsqueeze(1).float(),
341
  size=(height, width),
342
  mode="bicubic",
343
  align_corners=False,
344
  )
345
 
346
  depth_min = torch.amin(
347
- depth_map,
348
  dim=[1, 2, 3],
349
  keepdim=True
350
  )
351
 
352
  depth_max = torch.amax(
353
- depth_map,
354
  dim=[1, 2, 3],
355
  keepdim=True
356
  )
357
 
358
- depth_map = (
359
- depth_map - depth_min
360
  ) / (
361
  depth_max - depth_min + 1e-8
362
  )
363
 
364
- image = torch.cat(
365
- [depth_map] * 3,
366
  dim=1
367
  )
368
 
369
- image = (
370
- image
371
  .permute(0, 2, 3, 1)
372
  .cpu()
373
  .numpy()[0]
374
  )
375
 
376
  return Image.fromarray(
377
- (image * 255.0)
 
 
378
  .clip(0, 255)
379
  .astype(np.uint8)
380
  )
381
 
382
 
383
  # ============================================================
384
- # REFERENCE ROOM -> FURNITURE EXTRACTION
385
  # ============================================================
386
 
387
- REFERENCE_FURNITURE_CLASSES = {
388
- # Main furniture
 
 
 
 
 
389
  "chair",
390
  "sofa",
391
  "table",
@@ -396,27 +351,19 @@ REFERENCE_FURNITURE_CLASSES = {
396
  "dresser",
397
  "bench",
398
  "armchair",
399
-
400
- # Small furniture / objects
401
  "ottoman",
402
  "stool",
403
- "pillow",
404
  "lamp",
405
- "television",
406
- "computer",
407
- "monitor",
408
-
409
- # Decoration
410
  "plant",
411
  "vase",
412
  "painting",
413
  "mirror",
 
 
 
414
  "clock",
415
-
416
- # Floor / textile elements
417
  "rug",
418
-
419
- # Additional ADE-like categories
420
  "book",
421
  "books",
422
  "chest of drawers",
@@ -424,136 +371,503 @@ REFERENCE_FURNITURE_CLASSES = {
424
  }
425
 
426
 
427
- def extract_furniture_reference(
428
- reference_image: Image.Image,
429
- segmentation_image: Image.Image
430
- ) -> Image.Image:
431
- """
432
- Remove the architectural/background part of the reference room.
433
-
434
- The goal is NOT to extract a single object.
435
 
436
- Instead we preserve the complete set of recognizable
437
- furniture/decor elements from the reference room.
 
 
 
 
438
  """
439
 
440
- mask = create_binary_mask_from_segmentation(
441
- segmentation_image,
442
- list(REFERENCE_FURNITURE_CLASSES)
443
  )
444
 
445
- # Slightly enlarge furniture regions so that thin objects
446
- # and object edges are not destroyed by semantic segmentation.
447
- mask = mask.filter(
448
- ImageFilter.MaxFilter(9)
 
 
449
  )
450
 
451
- reference_rgb = reference_image.convert(
452
- "RGB"
 
453
  )
454
 
455
- # Neutral background.
456
- #
457
- # A neutral light gray is preferable to black because black
458
- # can be interpreted as an actual dark object by the
459
- # image encoder.
460
- background = Image.new(
461
- "RGB",
462
- reference_rgb.size,
463
- (235, 235, 235)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  )
465
 
466
- furniture_reference = Image.composite(
467
- reference_rgb,
468
- background,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  mask
470
  )
471
 
472
- return furniture_reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
 
475
  # ============================================================
476
- # TARGET ROOM MASK
477
  # ============================================================
478
 
479
- ARCHITECTURE_CLASSES = {
480
- "wall",
481
- "floor",
482
- "ceiling",
483
- "window",
484
- "windowpane",
485
- "door",
486
- "door;double;door",
487
- "curtain",
488
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
 
490
 
491
- def create_room_edit_mask(
492
- segmentation_image: Image.Image
493
- ) -> Image.Image:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  """
495
- Create the region where furniture can be generated.
496
 
497
- Architecture is kept strongly constrained by ControlNet.
 
498
 
499
- The mask deliberately does not depend on a manually selected
500
- rectangle or furniture placement area.
501
  """
502
 
503
- seg_rgb = np.array(
504
- segmentation_image
505
  )
506
 
507
- unique_colors = np.unique(
508
- seg_rgb.reshape(
509
- -1,
510
- seg_rgb.shape[2]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  ),
512
- axis=0
513
  )
514
 
515
- # Start with everything editable.
516
- editable = np.ones(
517
- seg_rgb.shape[:2],
518
- dtype=np.uint8
 
 
519
  )
520
 
521
- for color in unique_colors:
 
 
 
 
 
522
 
523
- color_tuple = tuple(
524
- int(v)
525
- for v in color
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  )
527
 
528
- item_name = map_colors_rgb(
529
- color_tuple
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  )
 
531
 
532
- if item_name in ARCHITECTURE_CLASSES:
 
 
 
533
 
534
- matches = (
535
- seg_rgb == color
536
- ).all(axis=2)
 
537
 
538
- # Windows, doors and architectural areas are
539
- # protected from direct inpainting.
540
- editable[matches] = 0
 
541
 
542
- mask = Image.fromarray(
543
- editable * 255,
544
- mode="L"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  )
546
 
547
- # Keep furniture transitions soft.
548
- mask = mask.filter(
549
- ImageFilter.GaussianBlur(2)
 
550
  )
551
 
552
- return mask
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
 
554
 
555
  # ============================================================
556
- # MAIN MODEL
557
  # ============================================================
558
 
559
  class ControlNetDepthDesignModelMulti:
@@ -565,50 +879,67 @@ class ControlNetDepthDesignModelMulti:
565
  self.neg_prompt = (
566
  "deformed furniture, distorted furniture, "
567
  "warped furniture, melted furniture, "
568
- "wrong furniture geometry, duplicated furniture, "
569
- "extra furniture, floating furniture, "
570
- "impossible perspective, impossible proportions, "
571
- "bad architecture, distorted walls, distorted floor, "
 
572
  "changed windows, changed doors, "
573
- "window, door, text, logo, watermark, "
 
574
  "low resolution, blurry, out of focus, "
575
- "surreal, ugly, cartoon, illustration, "
576
- "CGI, plastic, oversaturated"
577
  )
578
 
579
  self.additional_quality_suffix = (
580
- "professional interior photography, "
581
- "photorealistic interior, realistic materials, "
582
- "natural lighting, realistic shadows, "
583
- "correct perspective, correct scale, "
584
- "high detail, 4K"
 
 
 
585
  )
586
 
587
  @spaces.GPU
588
  def generate_design(
589
  self,
590
- empty_room_image: Image.Image,
591
- furniture_image: Image.Image,
592
  prompt: str,
593
- guidance_scale: float = 7.5,
594
  num_steps: int = 35,
595
- strength: float = 0.62,
596
  img_size: int = 640,
597
- ip_scale: float = 0.90
598
- ) -> Image.Image:
 
 
 
 
 
 
 
 
599
 
600
  print(
601
- "Starting complete room furnishing transfer..."
602
  )
603
 
604
  flush()
605
 
606
- self.generator = torch.Generator(
607
  device=device
608
  ).manual_seed(
609
  int(self.seed)
610
  )
611
 
 
 
 
 
612
  if prompt is None:
613
  prompt = ""
614
 
@@ -617,15 +948,20 @@ class ControlNetDepthDesignModelMulti:
617
  if prompt == "":
618
 
619
  prompt = (
620
- "Transfer the complete furniture and decor "
621
- "ensemble from the reference room into the target room. "
622
- "Preserve the target room architecture, walls, floor, "
623
- "windows, doors and camera viewpoint. "
624
- "Recreate the same sofa, chairs, tables, cabinets, "
625
- "lamps, rugs, plants, artwork and decorative objects "
626
  "from the reference room. "
627
- "Adapt their size, position, perspective, lighting "
628
- "and shadows naturally to the target room."
 
 
 
 
 
 
 
 
629
  )
630
 
631
  pos_prompt = (
@@ -634,119 +970,151 @@ class ControlNetDepthDesignModelMulti:
634
  + self.additional_quality_suffix
635
  )
636
 
637
- # --------------------------------------------------------
638
- # TARGET ROOM
639
- # --------------------------------------------------------
640
 
641
- orig_w, orig_h = (
642
- empty_room_image.size
643
  )
644
 
645
- new_width, new_height = resize_dimensions(
646
- empty_room_image.size,
647
- img_size
 
 
648
  )
649
 
650
- input_image = (
651
- empty_room_image
652
  .convert("RGB")
653
  .resize(
654
- (new_width, new_height),
 
 
 
655
  Image.Resampling.LANCZOS
656
  )
657
  )
658
 
659
- # --------------------------------------------------------
660
- # REFERENCE ROOM
661
- # --------------------------------------------------------
662
 
663
- reference_image = (
664
- furniture_image
665
  .convert("RGB")
666
  )
667
 
668
- # Resize reference WITHOUT stretching it.
669
- reference_for_segmentation = resize_keep_aspect(
670
- reference_image,
671
- new_width,
672
- new_height,
673
  background=(235, 235, 235)
674
  )
675
 
676
- # --------------------------------------------------------
677
  # TARGET SEGMENTATION
678
- # --------------------------------------------------------
 
 
 
 
679
 
680
  target_segmentation = segment_image(
681
- input_image,
682
  seg_image_processor,
683
  image_segmentor
684
  )
685
 
686
- # --------------------------------------------------------
687
  # REFERENCE SEGMENTATION
688
- # --------------------------------------------------------
689
 
690
  print(
691
- "Segmenting reference room furniture..."
692
  )
693
 
694
  reference_segmentation = segment_image(
695
- reference_for_segmentation,
696
  seg_image_processor,
697
  image_segmentor
698
  )
699
 
700
- # --------------------------------------------------------
701
- # EXTRACT COMPLETE FURNITURE ENSEMBLE
702
- # --------------------------------------------------------
703
-
704
- furniture_only = extract_furniture_reference(
705
- reference_for_segmentation,
706
- reference_segmentation
707
- )
708
-
709
- # --------------------------------------------------------
710
- # SAVE/PRINT INFORMATION FOR DEBUGGING
711
- # --------------------------------------------------------
712
 
713
  print(
714
- "Furniture reference extracted successfully."
715
  )
716
 
717
- # --------------------------------------------------------
718
- # TARGET EDIT MASK
719
- # --------------------------------------------------------
 
 
 
 
 
720
 
721
- mask_image = create_room_edit_mask(
722
- target_segmentation
 
 
 
 
723
  )
724
 
725
- # --------------------------------------------------------
726
  # DEPTH
727
- # --------------------------------------------------------
728
 
729
- image_depth = get_depth_image(
730
- input_image,
 
 
 
 
731
  depth_feature_extractor,
732
  depth_estimator
733
  )
734
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  flush()
736
 
737
- # --------------------------------------------------------
738
  # IP-ADAPTER
739
- # --------------------------------------------------------
740
 
741
  pipe.set_ip_adapter_scale(
742
  float(ip_scale)
743
  )
744
 
745
- # --------------------------------------------------------
746
- # CONTROLNET
747
- # --------------------------------------------------------
 
 
 
 
748
 
749
- generated_image = pipe(
750
  prompt=pos_prompt,
751
 
752
  negative_prompt=self.neg_prompt,
@@ -763,80 +1131,96 @@ class ControlNetDepthDesignModelMulti:
763
  guidance_scale
764
  ),
765
 
766
- generator=self.generator,
767
 
768
- image=input_image,
769
 
770
  mask_image=mask_image,
771
 
772
  # IMPORTANT:
773
- # This is now NOT the entire reference room.
774
  #
775
- # It is a furniture/decor-focused version
776
- # of the reference room.
777
- ip_adapter_image=furniture_only,
778
 
779
  control_image=[
780
- image_depth,
781
  target_segmentation
782
  ],
783
 
 
 
784
  controlnet_conditioning_scale=[
785
- 0.70,
786
  0.65
787
  ]
788
  ).images[0]
789
 
790
  flush()
791
 
792
- design_image = generated_image.resize(
793
- (orig_w, orig_h),
 
 
 
794
  Image.Resampling.LANCZOS
795
  )
796
 
797
  print(
798
- "Furniture transfer completed."
799
  )
800
 
801
- return design_image
802
 
803
 
804
  # ============================================================
805
- # GRADIO UI
806
  # ============================================================
807
 
808
  def create_demo(model):
809
 
810
  gr.Markdown(
811
- "### StableDesign — Reference Room Furniture Transfer"
812
  )
813
 
814
  gr.Markdown(
815
  """
816
- Upload two room photographs:
817
-
818
- **1. Target room:** the room whose architecture,
819
- camera perspective, walls, floor, windows and doors
820
- must be preserved.
821
-
822
- **2. Reference room:** a completely furnished room.
823
- The AI extracts the furniture and decorative ensemble
824
- from this image and recreates it inside the target room.
 
 
 
 
 
 
 
 
825
  """
826
  )
827
 
828
  with gr.Row():
829
 
 
 
 
 
830
  with gr.Column():
831
 
832
- room_image = gr.Image(
833
- label="1. Target room",
834
  type="pil",
835
  elem_id="img-display-input"
836
  )
837
 
838
- furniture_image = gr.Image(
839
- label="2. Furnished reference room",
840
  type="pil",
841
  elem_id="img-display-input"
842
  )
@@ -844,14 +1228,15 @@ def create_demo(model):
844
  prompt = gr.Textbox(
845
  label="Instructions",
846
  value=(
847
- "Transfer the complete furniture and decor "
848
- "ensemble from the second image into the first room. "
849
- "Preserve the architecture, walls, floor, windows, "
850
- "doors and camera perspective of the first image. "
851
- "Recreate the same furniture, decorations, rugs, "
852
- "lamps, plants and artwork from the reference room, "
853
- "adapting scale, perspective, lighting and shadows "
854
- "to the target room."
 
855
  ),
856
  lines=6
857
  )
@@ -881,23 +1266,23 @@ def create_demo(model):
881
  label="Guidance Scale",
882
  minimum=4.0,
883
  maximum=12.0,
884
- value=7.5,
885
  step=0.5
886
  )
887
 
888
  strength = gr.Slider(
889
- label="Room editing strength",
890
- minimum=0.35,
891
- maximum=0.85,
892
- value=0.62,
893
  step=0.05
894
  )
895
 
896
  ip_scale = gr.Slider(
897
- label="Reference furniture strength",
898
- minimum=0.40,
899
  maximum=1.20,
900
- value=0.90,
901
  step=0.05
902
  )
903
 
@@ -913,12 +1298,16 @@ def create_demo(model):
913
  a_prompt = gr.Textbox(
914
  label="Added Prompt",
915
  value=(
916
- "professional interior photography, "
917
- "photorealistic interior, realistic materials, "
918
- "natural lighting, realistic shadows, "
919
- "correct perspective, correct scale, "
920
- "high detail, 4K"
921
- )
 
 
 
 
922
  )
923
 
924
  n_prompt = gr.Textbox(
@@ -926,99 +1315,118 @@ def create_demo(model):
926
  value=(
927
  "deformed furniture, distorted furniture, "
928
  "warped furniture, melted furniture, "
929
- "wrong furniture geometry, duplicated furniture, "
930
- "extra furniture, floating furniture, "
931
- "impossible perspective, impossible proportions, "
 
932
  "bad architecture, distorted walls, "
933
- "distorted floor, changed windows, changed doors, "
934
- "window, door, text, logo, watermark, "
935
- "low resolution, blurry, out of focus, "
936
- "surreal, ugly, cartoon, illustration, "
937
- "CGI, plastic, oversaturated"
 
938
  ),
939
  lines=6
940
  )
941
 
942
  submit = gr.Button(
943
- "Transfer furniture from reference room",
944
  variant="primary"
945
  )
946
 
 
 
 
 
947
  with gr.Column():
948
 
949
- design_image = gr.Image(
950
  label="Result",
951
  elem_id="img-display-output"
952
  )
953
 
 
 
 
 
954
  def on_submit(
955
- room,
956
- furniture,
957
  text,
958
- num_steps,
959
- guidance_scale,
960
- seed,
961
- strength,
962
- ip_scale,
963
- a_prompt,
964
- n_prompt,
965
- img_size
966
  ):
967
 
968
- if room is None:
969
 
970
  raise gr.Error(
971
- "Please upload the target room image."
972
  )
973
 
974
- if furniture is None:
975
 
976
  raise gr.Error(
977
- "Please upload a fully furnished reference room image."
 
978
  )
979
 
980
- model.seed = int(seed)
 
 
981
 
982
- model.neg_prompt = n_prompt
 
 
983
 
984
- model.additional_quality_suffix = a_prompt
 
 
985
 
986
  with torch.no_grad():
987
 
988
- out_img = model.generate_design(
989
- room,
990
- furniture,
991
- text,
 
 
992
 
993
  guidance_scale=float(
994
- guidance_scale
995
  ),
996
 
997
  num_steps=int(
998
- num_steps
999
  ),
1000
 
1001
  strength=float(
1002
- strength
1003
  ),
1004
 
1005
  img_size=int(
1006
- img_size
1007
  ),
1008
 
1009
  ip_scale=float(
1010
- ip_scale
1011
  )
1012
  )
1013
 
1014
- return out_img
1015
 
1016
  submit.click(
1017
  on_submit,
1018
 
1019
  inputs=[
1020
- room_image,
1021
- furniture_image,
1022
  prompt,
1023
  num_steps,
1024
  guidance_scale,
@@ -1030,12 +1438,12 @@ def create_demo(model):
1030
  img_size
1031
  ],
1032
 
1033
- outputs=design_image
1034
  )
1035
 
1036
 
1037
  # ============================================================
1038
- # LOAD MODELS
1039
  # ============================================================
1040
 
1041
  print(
@@ -1060,40 +1468,67 @@ controlnet_seg = ControlNetModel.from_pretrained(
1060
  )
1061
 
1062
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1063
  print(
1064
  "Loading Stable Diffusion..."
1065
  )
1066
 
1067
- pipe = StableDiffusionControlNetInpaintPipeline.from_pretrained(
1068
- "SG161222/Realistic_Vision_V5.1_noVAE",
 
 
1069
 
1070
- controlnet=[
1071
- controlnet_depth,
1072
- controlnet_seg
1073
- ],
1074
 
1075
- safety_checker=None,
1076
 
1077
- torch_dtype=dtype
 
 
 
1078
  )
1079
 
1080
 
1081
  # ============================================================
1082
- # IP-ADAPTER
1083
  # ============================================================
1084
 
1085
  print(
1086
- "Loading IP-Adapter..."
1087
  )
1088
 
1089
  pipe.load_ip_adapter(
1090
  "h94/IP-Adapter",
1091
  subfolder="models",
1092
- weight_name="ip-adapter_sd15.bin"
1093
  )
1094
 
1095
  pipe.set_ip_adapter_scale(
1096
- 0.90
1097
  )
1098
 
1099
 
@@ -1114,11 +1549,11 @@ pipe = pipe.to(
1114
 
1115
 
1116
  # ============================================================
1117
- # SEGMENTATION MODEL
1118
  # ============================================================
1119
 
1120
  print(
1121
- "Loading segmentation model..."
1122
  )
1123
 
1124
  (
@@ -1132,11 +1567,11 @@ image_segmentor = (
1132
 
1133
 
1134
  # ============================================================
1135
- # DEPTH MODEL
1136
  # ============================================================
1137
 
1138
  print(
1139
- "Loading depth model..."
1140
  )
1141
 
1142
  (
@@ -1160,42 +1595,36 @@ def main():
1160
  )
1161
 
1162
  print(
1163
- "StableDesign Furniture Transfer "
1164
- "models loaded successfully."
1165
  )
1166
 
1167
- title = (
1168
- "# StableDesign — Furniture Transfer"
1169
  )
1170
 
1171
- description = """
1172
- <p>
1173
- <b>Target room:</b> upload the room that must keep
1174
- its architecture and camera perspective.
1175
- </p>
1176
-
1177
- <p>
1178
- <b>Reference room:</b> upload a completely furnished
1179
- room. The system automatically extracts the visible
1180
- furniture and decorative elements and uses them as
1181
- visual references for the target room.
1182
- </p>
1183
-
1184
- <p>
1185
- No manual furniture placement area is required.
1186
- </p>
1187
- """
1188
 
1189
  with gr.Blocks(
1190
  css=css
1191
  ) as demo:
1192
 
1193
  gr.Markdown(
1194
- title
1195
  )
1196
 
1197
  gr.Markdown(
1198
- description
 
 
 
 
 
1199
  )
1200
 
1201
  create_demo(
 
1
  import spaces
2
 
3
  from typing import Tuple, Union, List
 
4
  import gc
5
 
6
  import numpy as np
7
+ from PIL import Image, ImageFilter, ImageOps
8
 
9
  import torch
10
 
 
21
  AutoImageProcessor,
22
  UperNetForSemanticSegmentation,
23
  AutoModelForDepthEstimation,
24
+ CLIPVisionModelWithProjection,
25
  )
26
 
27
  from colors import ade_palette
 
31
 
32
 
33
  # ============================================================
34
+ # GLOBAL CONFIG
35
  # ============================================================
36
 
37
  device = "cuda"
38
  dtype = torch.float16
39
 
40
+ # Maximum number of individual furniture regions that will be
41
+ # extracted from the reference image.
42
+ MAX_REFERENCE_OBJECTS = 8
43
+
44
 
45
  # ============================================================
46
+ # CSS
47
  # ============================================================
48
 
49
  css = """
 
62
 
63
 
64
  # ============================================================
65
+ # MEMORY
66
  # ============================================================
67
 
68
  def flush():
 
 
 
 
69
  gc.collect()
70
 
71
  if torch.cuda.is_available():
72
  torch.cuda.empty_cache()
73
 
74
 
75
+ # ============================================================
76
+ # IMAGE RESIZING
77
+ # ============================================================
78
+
79
  def resize_dimensions(
80
  dimensions,
81
  target_size
82
  ):
83
  """
84
+ Preserve aspect ratio.
85
  """
86
 
87
  width, height = dimensions
 
89
  if width <= target_size and height <= target_size:
90
  return dimensions
91
 
92
+ if width >= height:
93
 
94
+ ratio = height / width
95
 
96
  return (
97
  target_size,
98
+ max(64, int(target_size * ratio))
99
  )
100
 
101
+ ratio = width / height
102
 
103
  return (
104
+ max(64, int(target_size * ratio)),
105
  target_size
106
  )
107
 
108
 
109
+ def fit_image(
110
  image: Image.Image,
111
+ width: int,
112
+ height: int,
113
+ background=(235, 235, 235)
114
  ):
115
  """
116
+ Fit an image into a canvas without stretching it.
 
 
 
117
  """
118
 
119
  image = image.convert("RGB")
 
121
  src_w, src_h = image.size
122
 
123
  scale = min(
124
+ width / src_w,
125
+ height / src_h
126
  )
127
 
128
+ new_w = max(
129
+ 1,
130
+ int(src_w * scale)
131
+ )
132
+
133
+ new_h = max(
134
+ 1,
135
+ int(src_h * scale)
136
+ )
137
 
138
  resized = image.resize(
139
  (new_w, new_h),
 
142
 
143
  canvas = Image.new(
144
  "RGB",
145
+ (width, height),
146
  background
147
  )
148
 
149
+ x = (width - new_w) // 2
150
+ y = (height - new_h) // 2
151
 
152
  canvas.paste(
153
  resized,
 
157
  return canvas
158
 
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  # ============================================================
161
  # SEGMENTATION
162
  # ============================================================
163
 
164
  def get_segmentation_pipeline():
 
 
 
165
 
166
+ processor = AutoImageProcessor.from_pretrained(
167
  "openmmlab/upernet-convnext-small"
168
  )
169
 
170
+ model = UperNetForSemanticSegmentation.from_pretrained(
171
+ "openmmlab/upernet-convnext-small"
 
 
172
  )
173
 
174
+ return processor, model
 
 
 
175
 
176
 
177
  @torch.inference_mode()
178
  @spaces.GPU
179
  def segment_image(
180
  image: Image.Image,
181
+ image_processor,
182
+ image_segmentor
183
+ ):
184
  """
185
+ ADE20K semantic segmentation.
186
  """
187
 
188
+ inputs = image_processor(
189
  image,
190
  return_tensors="pt"
191
  ).pixel_values.to(device)
 
193
  with torch.no_grad():
194
 
195
  outputs = image_segmentor(
196
+ inputs
197
  )
198
 
199
+ segmentation = (
200
  image_processor
201
  .post_process_semantic_segmentation(
202
  outputs,
 
205
  )
206
 
207
  # IMPORTANT:
208
+ # CUDA -> CPU before NumPy.
209
+ segmentation = (
210
+ segmentation
211
+ .detach()
212
  .cpu()
213
  .numpy()
214
  )
215
 
216
  color_seg = np.zeros(
217
  (
218
+ segmentation.shape[0],
219
+ segmentation.shape[1],
220
  3
221
  ),
222
  dtype=np.uint8
 
229
  for label, color in enumerate(palette):
230
 
231
  color_seg[
232
+ segmentation == label,
233
  :
234
  ] = color
235
 
 
264
  @spaces.GPU
265
  def get_depth_image(
266
  image: Image.Image,
267
+ feature_extractor,
268
+ depth_estimator
269
+ ):
 
 
 
270
 
271
+ inputs = feature_extractor(
272
  images=image,
273
  return_tensors="pt"
274
  ).to(device)
275
 
276
  with torch.no_grad():
277
 
278
+ depth = (
279
  depth_estimator(
280
+ **inputs
281
  )
282
  .predicted_depth
283
  )
284
 
285
  width, height = image.size
286
 
287
+ depth = torch.nn.functional.interpolate(
288
+ depth.unsqueeze(1).float(),
289
  size=(height, width),
290
  mode="bicubic",
291
  align_corners=False,
292
  )
293
 
294
  depth_min = torch.amin(
295
+ depth,
296
  dim=[1, 2, 3],
297
  keepdim=True
298
  )
299
 
300
  depth_max = torch.amax(
301
+ depth,
302
  dim=[1, 2, 3],
303
  keepdim=True
304
  )
305
 
306
+ depth = (
307
+ depth - depth_min
308
  ) / (
309
  depth_max - depth_min + 1e-8
310
  )
311
 
312
+ depth = torch.cat(
313
+ [depth] * 3,
314
  dim=1
315
  )
316
 
317
+ depth = (
318
+ depth
319
  .permute(0, 2, 3, 1)
320
  .cpu()
321
  .numpy()[0]
322
  )
323
 
324
  return Image.fromarray(
325
+ (
326
+ depth * 255
327
+ )
328
  .clip(0, 255)
329
  .astype(np.uint8)
330
  )
331
 
332
 
333
  # ============================================================
334
+ # ADE20K OBJECT CLASSES
335
  # ============================================================
336
 
337
+ # These are the classes we WANT to transfer from the
338
+ # reference room.
339
+ #
340
+ # We intentionally do not include walls, floors, ceiling,
341
+ # windows, doors, etc.
342
+
343
+ FURNITURE_CLASSES = {
344
  "chair",
345
  "sofa",
346
  "table",
 
351
  "dresser",
352
  "bench",
353
  "armchair",
 
 
354
  "ottoman",
355
  "stool",
 
356
  "lamp",
357
+ "pillow",
 
 
 
 
358
  "plant",
359
  "vase",
360
  "painting",
361
  "mirror",
362
+ "television",
363
+ "computer",
364
+ "monitor",
365
  "clock",
 
 
366
  "rug",
 
 
367
  "book",
368
  "books",
369
  "chest of drawers",
 
371
  }
372
 
373
 
374
+ # ============================================================
375
+ # SEGMENTATION -> BINARY FURNITURE MASK
376
+ # ============================================================
 
 
 
 
 
377
 
378
+ def get_class_mask(
379
+ segmentation: Image.Image,
380
+ wanted_classes
381
+ ):
382
+ """
383
+ Convert ADE20K RGB segmentation into a binary mask.
384
  """
385
 
386
+ seg = np.array(
387
+ segmentation
 
388
  )
389
 
390
+ unique_colors = np.unique(
391
+ seg.reshape(
392
+ -1,
393
+ 3
394
+ ),
395
+ axis=0
396
  )
397
 
398
+ mask = np.zeros(
399
+ seg.shape[:2],
400
+ dtype=np.uint8
401
  )
402
 
403
+ for color in unique_colors:
404
+
405
+ color_tuple = tuple(
406
+ int(x)
407
+ for x in color
408
+ )
409
+
410
+ try:
411
+ item = map_colors_rgb(
412
+ color_tuple
413
+ )
414
+ except Exception:
415
+ continue
416
+
417
+ if item in wanted_classes:
418
+
419
+ pixels = (
420
+ seg == color
421
+ ).all(axis=2)
422
+
423
+ mask[pixels] = 255
424
+
425
+ return Image.fromarray(
426
+ mask,
427
+ mode="L"
428
  )
429
 
430
+
431
+ # ============================================================
432
+ # CONNECTED COMPONENTS
433
+ # ============================================================
434
+
435
+ def connected_components(
436
+ mask: Image.Image,
437
+ min_area=150
438
+ ):
439
+ """
440
+ Find connected regions in a binary mask.
441
+
442
+ Pure NumPy implementation so no additional dependency
443
+ such as OpenCV is required.
444
+ """
445
+
446
+ arr = np.array(
447
  mask
448
  )
449
 
450
+ binary = arr > 0
451
+
452
+ height, width = binary.shape
453
+
454
+ visited = np.zeros(
455
+ binary.shape,
456
+ dtype=bool
457
+ )
458
+
459
+ components = []
460
+
461
+ # 8-connected neighborhood
462
+ neighbors = [
463
+ (-1, -1),
464
+ (-1, 0),
465
+ (-1, 1),
466
+ (0, -1),
467
+ (0, 1),
468
+ (1, -1),
469
+ (1, 0),
470
+ (1, 1),
471
+ ]
472
+
473
+ for y in range(height):
474
+
475
+ for x in range(width):
476
+
477
+ if not binary[y, x]:
478
+ continue
479
+
480
+ if visited[y, x]:
481
+ continue
482
+
483
+ stack = [(x, y)]
484
+
485
+ visited[y, x] = True
486
+
487
+ points = []
488
+
489
+ while stack:
490
+
491
+ px, py = stack.pop()
492
+
493
+ points.append(
494
+ (px, py)
495
+ )
496
+
497
+ for dx, dy in neighbors:
498
+
499
+ nx = px + dx
500
+ ny = py + dy
501
+
502
+ if nx < 0 or nx >= width:
503
+ continue
504
+
505
+ if ny < 0 or ny >= height:
506
+ continue
507
+
508
+ if not binary[ny, nx]:
509
+ continue
510
+
511
+ if visited[ny, nx]:
512
+ continue
513
+
514
+ visited[ny, nx] = True
515
+
516
+ stack.append(
517
+ (nx, ny)
518
+ )
519
+
520
+ if len(points) < min_area:
521
+ continue
522
+
523
+ xs = [
524
+ p[0]
525
+ for p in points
526
+ ]
527
+
528
+ ys = [
529
+ p[1]
530
+ for p in points
531
+ ]
532
+
533
+ x0 = max(
534
+ 0,
535
+ min(xs)
536
+ )
537
+
538
+ y0 = max(
539
+ 0,
540
+ min(ys)
541
+ )
542
+
543
+ x1 = min(
544
+ width - 1,
545
+ max(xs)
546
+ )
547
+
548
+ y1 = min(
549
+ height - 1,
550
+ max(ys)
551
+ )
552
+
553
+ components.append(
554
+ {
555
+ "area": len(points),
556
+ "bbox": (
557
+ x0,
558
+ y0,
559
+ x1 + 1,
560
+ y1 + 1
561
+ )
562
+ }
563
+ )
564
+
565
+ components.sort(
566
+ key=lambda x: x["area"],
567
+ reverse=True
568
+ )
569
+
570
+ return components
571
 
572
 
573
  # ============================================================
574
+ # REFERENCE OBJECT EXTRACTION
575
  # ============================================================
576
 
577
+ def extract_reference_objects(
578
+ reference_image: Image.Image,
579
+ reference_segmentation: Image.Image
580
+ ):
581
+ """
582
+ Extract the largest furniture/decor regions from the
583
+ reference room.
584
+
585
+ Returns a list of cropped RGB object images.
586
+ """
587
+
588
+ furniture_mask = get_class_mask(
589
+ reference_segmentation,
590
+ FURNITURE_CLASSES
591
+ )
592
+
593
+ # Small dilation to avoid cutting chair legs, lamp edges,
594
+ # sofa edges, etc.
595
+ furniture_mask = furniture_mask.filter(
596
+ ImageFilter.MaxFilter(5)
597
+ )
598
+
599
+ components = connected_components(
600
+ furniture_mask,
601
+ min_area=120
602
+ )
603
+
604
+ objects = []
605
+
606
+ for component in components:
607
+
608
+ if len(objects) >= MAX_REFERENCE_OBJECTS:
609
+ break
610
 
611
+ x0, y0, x1, y1 = component["bbox"]
612
 
613
+ box_w = x1 - x0
614
+ box_h = y1 - y0
615
+
616
+ # Ignore extremely tiny regions.
617
+ if box_w < 20 or box_h < 20:
618
+ continue
619
+
620
+ # Add contextual margin around object.
621
+ margin_x = max(
622
+ 8,
623
+ int(box_w * 0.10)
624
+ )
625
+
626
+ margin_y = max(
627
+ 8,
628
+ int(box_h * 0.10)
629
+ )
630
+
631
+ x0 = max(
632
+ 0,
633
+ x0 - margin_x
634
+ )
635
+
636
+ y0 = max(
637
+ 0,
638
+ y0 - margin_y
639
+ )
640
+
641
+ x1 = min(
642
+ reference_image.width,
643
+ x1 + margin_x
644
+ )
645
+
646
+ y1 = min(
647
+ reference_image.height,
648
+ y1 + margin_y
649
+ )
650
+
651
+ crop = reference_image.crop(
652
+ (
653
+ x0,
654
+ y0,
655
+ x1,
656
+ y1
657
+ )
658
+ )
659
+
660
+ objects.append(
661
+ crop
662
+ )
663
+
664
+ return objects
665
+
666
+
667
+ # ============================================================
668
+ # BUILD REFERENCE BOARD
669
+ # ============================================================
670
+
671
+ def build_reference_board(
672
+ reference_image: Image.Image,
673
+ reference_segmentation: Image.Image,
674
+ board_size=640
675
+ ):
676
  """
677
+ Create a visual board containing:
678
 
679
+ 1. The complete reference room, slightly reduced.
680
+ 2. Individual furniture/decor crops.
681
 
682
+ This gives IP-Adapter both scene context and object detail.
 
683
  """
684
 
685
+ reference_image = reference_image.convert(
686
+ "RGB"
687
  )
688
 
689
+ # --------------------------------------------------------
690
+ # Extract individual objects
691
+ # --------------------------------------------------------
692
+
693
+ objects = extract_reference_objects(
694
+ reference_image,
695
+ reference_segmentation
696
+ )
697
+
698
+ # --------------------------------------------------------
699
+ # Board
700
+ # --------------------------------------------------------
701
+
702
+ board = Image.new(
703
+ "RGB",
704
+ (
705
+ board_size,
706
+ board_size
707
  ),
708
+ (245, 245, 245)
709
  )
710
 
711
+ # --------------------------------------------------------
712
+ # Main reference image
713
+ # --------------------------------------------------------
714
+
715
+ main_size = int(
716
+ board_size * 0.52
717
  )
718
 
719
+ main_image = fit_image(
720
+ reference_image,
721
+ main_size,
722
+ main_size,
723
+ background=(235, 235, 235)
724
+ )
725
 
726
+ board.paste(
727
+ main_image,
728
+ (10, 10)
729
+ )
730
+
731
+ # --------------------------------------------------------
732
+ # Object thumbnails
733
+ # --------------------------------------------------------
734
+
735
+ if len(objects) == 0:
736
+
737
+ # Fallback:
738
+ # use the complete reference image.
739
+ return fit_image(
740
+ reference_image,
741
+ board_size,
742
+ board_size,
743
+ background=(235, 235, 235)
744
  )
745
 
746
+ thumb_area_x = (
747
+ board_size
748
+ - main_size
749
+ - 30
750
+ )
751
+
752
+ thumb_area_y = (
753
+ board_size
754
+ - 20
755
+ )
756
+
757
+ columns = 2
758
+
759
+ rows = max(
760
+ 1,
761
+ int(
762
+ np.ceil(
763
+ len(objects) / columns
764
+ )
765
  )
766
+ )
767
 
768
+ cell_w = max(
769
+ 64,
770
+ thumb_area_x // columns
771
+ )
772
 
773
+ cell_h = max(
774
+ 64,
775
+ thumb_area_y // min(rows, 4)
776
+ )
777
 
778
+ start_x = (
779
+ main_size
780
+ + 20
781
+ )
782
 
783
+ start_y = 10
784
+
785
+ for index, obj in enumerate(objects):
786
+
787
+ row = index // columns
788
+ col = index % columns
789
+
790
+ if row >= 4:
791
+ break
792
+
793
+ x = (
794
+ start_x
795
+ + col * cell_w
796
+ )
797
+
798
+ y = (
799
+ start_y
800
+ + row * cell_h
801
+ )
802
+
803
+ thumb = fit_image(
804
+ obj,
805
+ max(32, cell_w - 8),
806
+ max(32, cell_h - 8),
807
+ background=(235, 235, 235)
808
+ )
809
+
810
+ board.paste(
811
+ thumb,
812
+ (x, y)
813
+ )
814
+
815
+ return board
816
+
817
+
818
+ # ============================================================
819
+ # TARGET INPAINT MASK
820
+ # ============================================================
821
+
822
+ def build_target_inpaint_mask(
823
+ target_segmentation: Image.Image
824
+ ):
825
+ """
826
+ The target room is intentionally allowed to change.
827
+
828
+ The architecture is preserved by ControlNet rather than
829
+ by freezing the entire target image.
830
+
831
+ We use a mostly-full mask with slightly protected borders.
832
+ """
833
+
834
+ width, height = target_segmentation.size
835
+
836
+ mask = Image.new(
837
+ "L",
838
+ (
839
+ width,
840
+ height
841
+ ),
842
+ 255
843
  )
844
 
845
+ # Protect a narrow outer border.
846
+ # This reduces accidental changes at image boundaries.
847
+ arr = np.array(
848
+ mask
849
  )
850
 
851
+ border = max(
852
+ 2,
853
+ int(
854
+ min(width, height) * 0.015
855
+ )
856
+ )
857
+
858
+ arr[:border, :] = 0
859
+ arr[-border:, :] = 0
860
+ arr[:, :border] = 0
861
+ arr[:, -border:] = 0
862
+
863
+ return Image.fromarray(
864
+ arr,
865
+ mode="L"
866
+ )
867
 
868
 
869
  # ============================================================
870
+ # MODEL
871
  # ============================================================
872
 
873
  class ControlNetDepthDesignModelMulti:
 
879
  self.neg_prompt = (
880
  "deformed furniture, distorted furniture, "
881
  "warped furniture, melted furniture, "
882
+ "duplicated furniture, extra furniture, "
883
+ "floating furniture, furniture without contact "
884
+ "with the floor, impossible perspective, "
885
+ "impossible proportions, bad architecture, "
886
+ "distorted walls, distorted floor, "
887
  "changed windows, changed doors, "
888
+ "new windows, new doors, "
889
+ "text, logo, watermark, "
890
  "low resolution, blurry, out of focus, "
891
+ "surreal, cartoon, illustration, CGI, "
892
+ "plastic materials, oversaturated"
893
  )
894
 
895
  self.additional_quality_suffix = (
896
+ "photorealistic interior photography, "
897
+ "realistic furniture materials, "
898
+ "realistic global illumination, "
899
+ "natural shadows, "
900
+ "correct perspective, "
901
+ "correct object scale, "
902
+ "high detail, "
903
+ "high resolution"
904
  )
905
 
906
  @spaces.GPU
907
  def generate_design(
908
  self,
909
+ target_room: Image.Image,
910
+ reference_room: Image.Image,
911
  prompt: str,
912
+ guidance_scale: float = 7.0,
913
  num_steps: int = 35,
914
+ strength: float = 0.72,
915
  img_size: int = 640,
916
+ ip_scale: float = 0.95
917
+ ):
918
+
919
+ print(
920
+ "=========================================="
921
+ )
922
+
923
+ print(
924
+ "Starting reference-room transfer..."
925
+ )
926
 
927
  print(
928
+ "=========================================="
929
  )
930
 
931
  flush()
932
 
933
+ generator = torch.Generator(
934
  device=device
935
  ).manual_seed(
936
  int(self.seed)
937
  )
938
 
939
+ # ----------------------------------------------------
940
+ # PROMPT
941
+ # ----------------------------------------------------
942
+
943
  if prompt is None:
944
  prompt = ""
945
 
 
948
  if prompt == "":
949
 
950
  prompt = (
951
+ "A professionally photographed interior "
952
+ "of the target room furnished with the "
953
+ "complete furniture and decoration ensemble "
 
 
 
954
  "from the reference room. "
955
+ "Transfer the sofa, chairs, tables, cabinets, "
956
+ "lamps, rugs, plants, artwork and decorative "
957
+ "objects from the reference room. "
958
+ "Keep the target room architecture, "
959
+ "walls, ceiling, floor, windows, doors and "
960
+ "camera viewpoint unchanged. "
961
+ "Adapt the transferred furniture naturally "
962
+ "to the target room geometry. "
963
+ "Preserve realistic proportions, "
964
+ "perspective, materials, lighting and shadows."
965
  )
966
 
967
  pos_prompt = (
 
970
  + self.additional_quality_suffix
971
  )
972
 
973
+ # ----------------------------------------------------
974
+ # TARGET SIZE
975
+ # ----------------------------------------------------
976
 
977
+ original_width, original_height = (
978
+ target_room.size
979
  )
980
 
981
+ target_width, target_height = (
982
+ resize_dimensions(
983
+ target_room.size,
984
+ img_size
985
+ )
986
  )
987
 
988
+ target = (
989
+ target_room
990
  .convert("RGB")
991
  .resize(
992
+ (
993
+ target_width,
994
+ target_height
995
+ ),
996
  Image.Resampling.LANCZOS
997
  )
998
  )
999
 
1000
+ # ----------------------------------------------------
1001
+ # REFERENCE
1002
+ # ----------------------------------------------------
1003
 
1004
+ reference = (
1005
+ reference_room
1006
  .convert("RGB")
1007
  )
1008
 
1009
+ reference = fit_image(
1010
+ reference,
1011
+ target_width,
1012
+ target_height,
 
1013
  background=(235, 235, 235)
1014
  )
1015
 
1016
+ # ----------------------------------------------------
1017
  # TARGET SEGMENTATION
1018
+ # ----------------------------------------------------
1019
+
1020
+ print(
1021
+ "1/5 - Target room segmentation..."
1022
+ )
1023
 
1024
  target_segmentation = segment_image(
1025
+ target,
1026
  seg_image_processor,
1027
  image_segmentor
1028
  )
1029
 
1030
+ # ----------------------------------------------------
1031
  # REFERENCE SEGMENTATION
1032
+ # ----------------------------------------------------
1033
 
1034
  print(
1035
+ "2/5 - Reference room segmentation..."
1036
  )
1037
 
1038
  reference_segmentation = segment_image(
1039
+ reference,
1040
  seg_image_processor,
1041
  image_segmentor
1042
  )
1043
 
1044
+ # ----------------------------------------------------
1045
+ # BUILD OBJECT-AWARE REFERENCE BOARD
1046
+ # ----------------------------------------------------
 
 
 
 
 
 
 
 
 
1047
 
1048
  print(
1049
+ "3/5 - Extracting furniture and decorations..."
1050
  )
1051
 
1052
+ reference_board = build_reference_board(
1053
+ reference,
1054
+ reference_segmentation,
1055
+ board_size=max(
1056
+ 512,
1057
+ target_width
1058
+ )
1059
+ )
1060
 
1061
+ reference_board = reference_board.resize(
1062
+ (
1063
+ target_width,
1064
+ target_height
1065
+ ),
1066
+ Image.Resampling.LANCZOS
1067
  )
1068
 
1069
+ # ----------------------------------------------------
1070
  # DEPTH
1071
+ # ----------------------------------------------------
1072
 
1073
+ print(
1074
+ "4/5 - Calculating target room depth..."
1075
+ )
1076
+
1077
+ target_depth = get_depth_image(
1078
+ target,
1079
  depth_feature_extractor,
1080
  depth_estimator
1081
  )
1082
 
1083
+ # ----------------------------------------------------
1084
+ # INPAINT MASK
1085
+ # ----------------------------------------------------
1086
+
1087
+ # IMPORTANT:
1088
+ #
1089
+ # We DO NOT freeze the target room.
1090
+ #
1091
+ # ControlNet provides the architectural structure.
1092
+ # The diffusion model must have enough freedom to
1093
+ # actually create the transferred furniture.
1094
+
1095
+ mask_image = build_target_inpaint_mask(
1096
+ target_segmentation
1097
+ )
1098
+
1099
  flush()
1100
 
1101
+ # ----------------------------------------------------
1102
  # IP-ADAPTER
1103
+ # ----------------------------------------------------
1104
 
1105
  pipe.set_ip_adapter_scale(
1106
  float(ip_scale)
1107
  )
1108
 
1109
+ # ----------------------------------------------------
1110
+ # GENERATION
1111
+ # ----------------------------------------------------
1112
+
1113
+ print(
1114
+ "5/5 - Generating furnished target room..."
1115
+ )
1116
 
1117
+ result = pipe(
1118
  prompt=pos_prompt,
1119
 
1120
  negative_prompt=self.neg_prompt,
 
1131
  guidance_scale
1132
  ),
1133
 
1134
+ generator=generator,
1135
 
1136
+ image=target,
1137
 
1138
  mask_image=mask_image,
1139
 
1140
  # IMPORTANT:
 
1141
  #
1142
+ # This is the reference board, not a manually
1143
+ # selected furniture image.
1144
+ ip_adapter_image=reference_board,
1145
 
1146
  control_image=[
1147
+ target_depth,
1148
  target_segmentation
1149
  ],
1150
 
1151
+ # Depth is slightly stronger because it carries
1152
+ # the target room's spatial structure.
1153
  controlnet_conditioning_scale=[
1154
+ 0.80,
1155
  0.65
1156
  ]
1157
  ).images[0]
1158
 
1159
  flush()
1160
 
1161
+ result = result.resize(
1162
+ (
1163
+ original_width,
1164
+ original_height
1165
+ ),
1166
  Image.Resampling.LANCZOS
1167
  )
1168
 
1169
  print(
1170
+ "Generation completed."
1171
  )
1172
 
1173
+ return result
1174
 
1175
 
1176
  # ============================================================
1177
+ # GRADIO
1178
  # ============================================================
1179
 
1180
  def create_demo(model):
1181
 
1182
  gr.Markdown(
1183
+ "### StableDesign — Reference Room Transfer"
1184
  )
1185
 
1186
  gr.Markdown(
1187
  """
1188
+ <p>
1189
+ <b>Image 1 — Target room:</b>
1190
+ the room that will be furnished. Its architecture,
1191
+ perspective, walls, floor, windows and doors are
1192
+ preserved using structural controls.
1193
+ </p>
1194
+
1195
+ <p>
1196
+ <b>Image 2 — Reference room:</b>
1197
+ a completely furnished room. The system automatically
1198
+ analyzes the furniture and decorative elements and
1199
+ uses them as the visual reference.
1200
+ </p>
1201
+
1202
+ <p>
1203
+ No furniture area or placement needs to be selected.
1204
+ </p>
1205
  """
1206
  )
1207
 
1208
  with gr.Row():
1209
 
1210
+ # ====================================================
1211
+ # INPUTS
1212
+ # ====================================================
1213
+
1214
  with gr.Column():
1215
 
1216
+ target_image = gr.Image(
1217
+ label="1. Target / empty room",
1218
  type="pil",
1219
  elem_id="img-display-input"
1220
  )
1221
 
1222
+ reference_image = gr.Image(
1223
+ label="2. Fully furnished reference room",
1224
  type="pil",
1225
  elem_id="img-display-input"
1226
  )
 
1228
  prompt = gr.Textbox(
1229
  label="Instructions",
1230
  value=(
1231
+ "Transfer the complete furniture and "
1232
+ "decoration ensemble from image 2 "
1233
+ "into image 1. Preserve the architecture, "
1234
+ "walls, floor, windows, doors and camera "
1235
+ "perspective of image 1. Recreate the "
1236
+ "same furniture and decorative elements "
1237
+ "from image 2, adapting their scale, "
1238
+ "perspective, lighting and shadows "
1239
+ "naturally to image 1."
1240
  ),
1241
  lines=6
1242
  )
 
1266
  label="Guidance Scale",
1267
  minimum=4.0,
1268
  maximum=12.0,
1269
+ value=7.0,
1270
  step=0.5
1271
  )
1272
 
1273
  strength = gr.Slider(
1274
+ label="Furniture generation strength",
1275
+ minimum=0.45,
1276
+ maximum=0.90,
1277
+ value=0.72,
1278
  step=0.05
1279
  )
1280
 
1281
  ip_scale = gr.Slider(
1282
+ label="Reference room strength",
1283
+ minimum=0.50,
1284
  maximum=1.20,
1285
+ value=0.95,
1286
  step=0.05
1287
  )
1288
 
 
1298
  a_prompt = gr.Textbox(
1299
  label="Added Prompt",
1300
  value=(
1301
+ "photorealistic interior photography, "
1302
+ "realistic furniture materials, "
1303
+ "realistic global illumination, "
1304
+ "natural shadows, "
1305
+ "correct perspective, "
1306
+ "correct object scale, "
1307
+ "high detail, "
1308
+ "high resolution"
1309
+ ),
1310
+ lines=4
1311
  )
1312
 
1313
  n_prompt = gr.Textbox(
 
1315
  value=(
1316
  "deformed furniture, distorted furniture, "
1317
  "warped furniture, melted furniture, "
1318
+ "duplicated furniture, extra furniture, "
1319
+ "floating furniture, furniture without "
1320
+ "contact with the floor, impossible "
1321
+ "perspective, impossible proportions, "
1322
  "bad architecture, distorted walls, "
1323
+ "distorted floor, changed windows, "
1324
+ "changed doors, new windows, new doors, "
1325
+ "text, logo, watermark, low resolution, "
1326
+ "blurry, out of focus, surreal, cartoon, "
1327
+ "illustration, CGI, plastic materials, "
1328
+ "oversaturated"
1329
  ),
1330
  lines=6
1331
  )
1332
 
1333
  submit = gr.Button(
1334
+ "Transfer complete furniture",
1335
  variant="primary"
1336
  )
1337
 
1338
+ # ====================================================
1339
+ # OUTPUT
1340
+ # ====================================================
1341
+
1342
  with gr.Column():
1343
 
1344
+ result_image = gr.Image(
1345
  label="Result",
1346
  elem_id="img-display-output"
1347
  )
1348
 
1349
+ # ========================================================
1350
+ # SUBMIT
1351
+ # ========================================================
1352
+
1353
  def on_submit(
1354
+ target,
1355
+ reference,
1356
  text,
1357
+ num_steps_value,
1358
+ guidance_value,
1359
+ seed_value,
1360
+ strength_value,
1361
+ ip_value,
1362
+ additional_prompt,
1363
+ negative_prompt,
1364
+ image_size
1365
  ):
1366
 
1367
+ if target is None:
1368
 
1369
  raise gr.Error(
1370
+ "Please upload image 1: the target room."
1371
  )
1372
 
1373
+ if reference is None:
1374
 
1375
  raise gr.Error(
1376
+ "Please upload image 2: the fully furnished "
1377
+ "reference room."
1378
  )
1379
 
1380
+ model.seed = int(
1381
+ seed_value
1382
+ )
1383
 
1384
+ model.neg_prompt = (
1385
+ negative_prompt
1386
+ )
1387
 
1388
+ model.additional_quality_suffix = (
1389
+ additional_prompt
1390
+ )
1391
 
1392
  with torch.no_grad():
1393
 
1394
+ output = model.generate_design(
1395
+ target_room=target,
1396
+
1397
+ reference_room=reference,
1398
+
1399
+ prompt=text,
1400
 
1401
  guidance_scale=float(
1402
+ guidance_value
1403
  ),
1404
 
1405
  num_steps=int(
1406
+ num_steps_value
1407
  ),
1408
 
1409
  strength=float(
1410
+ strength_value
1411
  ),
1412
 
1413
  img_size=int(
1414
+ image_size
1415
  ),
1416
 
1417
  ip_scale=float(
1418
+ ip_value
1419
  )
1420
  )
1421
 
1422
+ return output
1423
 
1424
  submit.click(
1425
  on_submit,
1426
 
1427
  inputs=[
1428
+ target_image,
1429
+ reference_image,
1430
  prompt,
1431
  num_steps,
1432
  guidance_scale,
 
1438
  img_size
1439
  ],
1440
 
1441
+ outputs=result_image
1442
  )
1443
 
1444
 
1445
  # ============================================================
1446
+ # LOAD CONTROLNET
1447
  # ============================================================
1448
 
1449
  print(
 
1468
  )
1469
 
1470
 
1471
+ # ============================================================
1472
+ # LOAD CLIP VISION FOR IP-ADAPTER PLUS
1473
+ # ============================================================
1474
+
1475
+ print(
1476
+ "Loading IP-Adapter Plus image encoder..."
1477
+ )
1478
+
1479
+ image_encoder = (
1480
+ CLIPVisionModelWithProjection
1481
+ .from_pretrained(
1482
+ "h94/IP-Adapter",
1483
+ subfolder="models/image_encoder",
1484
+ torch_dtype=dtype
1485
+ )
1486
+ )
1487
+
1488
+
1489
+ # ============================================================
1490
+ # LOAD STABLE DIFFUSION
1491
+ # ============================================================
1492
+
1493
  print(
1494
  "Loading Stable Diffusion..."
1495
  )
1496
 
1497
+ pipe = (
1498
+ StableDiffusionControlNetInpaintPipeline
1499
+ .from_pretrained(
1500
+ "SG161222/Realistic_Vision_V5.1_noVAE",
1501
 
1502
+ controlnet=[
1503
+ controlnet_depth,
1504
+ controlnet_seg
1505
+ ],
1506
 
1507
+ image_encoder=image_encoder,
1508
 
1509
+ safety_checker=None,
1510
+
1511
+ torch_dtype=dtype
1512
+ )
1513
  )
1514
 
1515
 
1516
  # ============================================================
1517
+ # IP-ADAPTER PLUS
1518
  # ============================================================
1519
 
1520
  print(
1521
+ "Loading IP-Adapter Plus SD1.5..."
1522
  )
1523
 
1524
  pipe.load_ip_adapter(
1525
  "h94/IP-Adapter",
1526
  subfolder="models",
1527
+ weight_name="ip-adapter-plus_sd15.safetensors"
1528
  )
1529
 
1530
  pipe.set_ip_adapter_scale(
1531
+ 0.95
1532
  )
1533
 
1534
 
 
1549
 
1550
 
1551
  # ============================================================
1552
+ # SEGMENTATION
1553
  # ============================================================
1554
 
1555
  print(
1556
+ "Loading ADE20K segmentation..."
1557
  )
1558
 
1559
  (
 
1567
 
1568
 
1569
  # ============================================================
1570
+ # DEPTH
1571
  # ============================================================
1572
 
1573
  print(
1574
+ "Loading Depth Anything..."
1575
  )
1576
 
1577
  (
 
1595
  )
1596
 
1597
  print(
1598
+ "=========================================="
 
1599
  )
1600
 
1601
+ print(
1602
+ "StableDesign V2 loaded successfully."
1603
  )
1604
 
1605
+ print(
1606
+ "Reference-room furniture transfer enabled."
1607
+ )
1608
+
1609
+ print(
1610
+ "=========================================="
1611
+ )
 
 
 
 
 
 
 
 
 
 
1612
 
1613
  with gr.Blocks(
1614
  css=css
1615
  ) as demo:
1616
 
1617
  gr.Markdown(
1618
+ "# StableDesign V2"
1619
  )
1620
 
1621
  gr.Markdown(
1622
+ """
1623
+ Upload an empty/target room and a fully furnished
1624
+ reference room. The system automatically analyzes
1625
+ the reference furniture and recreates the furnishing
1626
+ ensemble inside the target room.
1627
+ """
1628
  )
1629
 
1630
  create_demo(