midman79 commited on
Commit
55340d5
·
verified ·
1 Parent(s): 572b919
Files changed (1) hide show
  1. app.py +676 -266
app.py CHANGED
@@ -1,29 +1,37 @@
1
  import spaces
 
2
  from typing import Tuple, Union, List
3
  import os
4
  import gc
5
 
6
  import numpy as np
7
- from PIL import Image
8
 
9
  import torch
10
- from diffusers.pipelines.controlnet import StableDiffusionControlNetInpaintPipeline
 
 
 
 
11
  from diffusers import (
12
  ControlNetModel,
13
  UniPCMultistepScheduler,
14
  )
 
15
  from transformers import (
16
  AutoImageProcessor,
17
  UperNetForSemanticSegmentation,
18
  AutoModelForDepthEstimation,
19
  )
 
20
  from colors import ade_palette
21
  from utils import map_colors_rgb
 
22
  import gradio as gr
23
 
24
 
25
  # ============================================================
26
- # DEVICE
27
  # ============================================================
28
 
29
  device = "cuda"
@@ -31,7 +39,7 @@ dtype = torch.float16
31
 
32
 
33
  # ============================================================
34
- # CSS
35
  # ============================================================
36
 
37
  css = """
@@ -50,47 +58,182 @@ css = """
50
 
51
 
52
  # ============================================================
53
- # SEGMENTATION
54
  # ============================================================
55
 
56
- def filter_items(
57
- colors_list: Union[List, np.ndarray],
58
- items_list: Union[List, np.ndarray],
59
- items_to_remove: Union[List, np.ndarray]
60
- ) -> Tuple[Union[List, np.ndarray], Union[List, np.ndarray]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- filtered_colors = []
63
- filtered_items = []
64
 
65
- for color, item in zip(colors_list, items_list):
66
- if item not in items_to_remove:
67
- filtered_colors.append(color)
68
- filtered_items.append(item)
69
 
70
- return filtered_colors, filtered_items
 
 
71
 
 
72
 
73
- def get_segmentation_pipeline(
74
- ) -> Tuple[AutoImageProcessor, UperNetForSemanticSegmentation]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  image_processor = AutoImageProcessor.from_pretrained(
77
  "openmmlab/upernet-convnext-small"
78
  )
79
 
80
- image_segmentor = UperNetForSemanticSegmentation.from_pretrained(
81
- "openmmlab/upernet-convnext-small"
 
 
82
  )
83
 
84
- return image_processor, image_segmentor
 
 
 
85
 
86
 
87
  @torch.inference_mode()
88
  @spaces.GPU
89
  def segment_image(
90
- image: Image,
91
  image_processor: AutoImageProcessor,
92
  image_segmentor: UperNetForSemanticSegmentation
93
- ) -> Image:
 
 
 
94
 
95
  pixel_values = image_processor(
96
  image,
@@ -98,25 +241,50 @@ def segment_image(
98
  ).pixel_values.to(device)
99
 
100
  with torch.no_grad():
101
- outputs = image_segmentor(pixel_values)
102
 
103
- seg = image_processor.post_process_semantic_segmentation(
104
- outputs, target_sizes=[image.size[::-1]]
105
- )[0]
106
-
107
- # Move segmentation tensor from GPU to CPU before using it with NumPy
108
- seg = seg.detach().cpu().numpy()
109
-
110
- color_seg = np.zeros((seg.shape[0], seg.shape[1], 3), dtype=np.uint8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
- palette = np.array(ade_palette())
 
 
113
 
114
  for label, color in enumerate(palette):
115
- color_seg[seg == label, :] = color
116
 
117
- color_seg = color_seg.astype(np.uint8)
 
 
 
118
 
119
- return Image.fromarray(color_seg).convert("RGB")
 
 
120
 
121
 
122
  # ============================================================
@@ -135,16 +303,22 @@ def get_depth_pipeline():
135
  torch_dtype=dtype
136
  )
137
 
138
- return feature_extractor, depth_estimator
 
 
 
139
 
140
 
141
  @torch.inference_mode()
142
  @spaces.GPU
143
  def get_depth_image(
144
- image: Image,
145
  feature_extractor: AutoImageProcessor,
146
  depth_estimator: AutoModelForDepthEstimation
147
- ) -> Image:
 
 
 
148
 
149
  image_to_depth = feature_extractor(
150
  images=image,
@@ -152,9 +326,13 @@ def get_depth_image(
152
  ).to(device)
153
 
154
  with torch.no_grad():
155
- depth_map = depth_estimator(
156
- **image_to_depth
157
- ).predicted_depth
 
 
 
 
158
 
159
  width, height = image.size
160
 
@@ -188,57 +366,190 @@ def get_depth_image(
188
  dim=1
189
  )
190
 
191
- image = image.permute(
192
- 0, 2, 3, 1
193
- ).cpu().numpy()[0]
 
 
 
194
 
195
- image = Image.fromarray(
196
  (image * 255.0)
197
  .clip(0, 255)
198
  .astype(np.uint8)
199
  )
200
 
201
- return image
202
-
203
 
204
  # ============================================================
205
- # RESIZE
206
  # ============================================================
207
 
208
- def resize_dimensions(dimensions, target_size):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
- width, height = dimensions
211
 
212
- if width < target_size and height < target_size:
213
- return dimensions
 
 
 
 
214
 
215
- if width > height:
216
- aspect_ratio = height / width
217
 
218
- return (
219
- target_size,
220
- int(target_size * aspect_ratio)
221
- )
 
 
 
 
222
 
223
- else:
224
- aspect_ratio = width / height
 
 
 
225
 
226
- return (
227
- int(target_size * aspect_ratio),
228
- target_size
229
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
 
232
  # ============================================================
233
- # MEMORY CLEANUP
234
  # ============================================================
235
 
236
- def flush():
 
 
 
 
 
 
 
 
 
237
 
238
- gc.collect()
239
 
240
- if torch.cuda.is_available():
241
- torch.cuda.empty_cache()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
 
244
  # ============================================================
@@ -252,38 +563,43 @@ class ControlNetDepthDesignModelMulti:
252
  self.seed = 323 * 111
253
 
254
  self.neg_prompt = (
255
- "window, door, low resolution, banner, logo, watermark, "
256
- "text, deformed, blurry, out of focus, surreal, ugly, "
257
- "beginner, duplicated furniture, extra furniture"
 
 
 
 
 
 
 
 
258
  )
259
 
260
- self.control_items = [
261
- "windowpane;window",
262
- "door;double;door"
263
- ]
264
-
265
  self.additional_quality_suffix = (
266
- "interior design, 4K, high resolution, photorealistic"
 
 
 
 
267
  )
268
 
269
- # ========================================================
270
- # GENERATE
271
- # ========================================================
272
-
273
  @spaces.GPU
274
  def generate_design(
275
  self,
276
- empty_room_image: Image,
277
- furniture_image: Image,
278
  prompt: str,
279
- guidance_scale: int = 10,
280
- num_steps: int = 30,
281
- strength: float = 0.85,
282
  img_size: int = 640,
283
- ip_scale: float = 0.75
284
- ) -> Image:
285
 
286
- print("Starting furniture insertion...")
 
 
287
 
288
  flush()
289
 
@@ -293,19 +609,23 @@ class ControlNetDepthDesignModelMulti:
293
  int(self.seed)
294
  )
295
 
296
- # ----------------------------------------------------
297
- # PROMPT
298
- # ----------------------------------------------------
299
-
300
  if prompt is None:
301
  prompt = ""
302
 
303
  prompt = prompt.strip()
304
 
305
  if prompt == "":
 
306
  prompt = (
307
- "Place the furniture from the reference image "
308
- "naturally inside the room."
 
 
 
 
 
 
 
309
  )
310
 
311
  pos_prompt = (
@@ -314,178 +634,169 @@ class ControlNetDepthDesignModelMulti:
314
  + self.additional_quality_suffix
315
  )
316
 
317
- # ----------------------------------------------------
318
- # ORIGINAL ROOM SIZE
319
- # ----------------------------------------------------
320
 
321
- orig_w, orig_h = empty_room_image.size
322
-
323
- # ----------------------------------------------------
324
- # ROOM
325
- # ----------------------------------------------------
326
 
327
  new_width, new_height = resize_dimensions(
328
  empty_room_image.size,
329
  img_size
330
  )
331
 
332
- input_image = empty_room_image.convert(
333
- "RGB"
334
- ).resize(
335
- (new_width, new_height)
 
 
 
336
  )
337
 
338
- # ----------------------------------------------------
339
- # FURNITURE REFERENCE
340
- # ----------------------------------------------------
341
-
342
- furniture_image = furniture_image.convert("RGB")
343
 
344
- ip_width = max(
345
- 64,
346
- int(new_width / 8) * 8
347
  )
348
 
349
- ip_height = max(
350
- 64,
351
- int(new_height / 8) * 8
 
 
 
352
  )
353
 
354
- furniture_ip = furniture_image.resize(
355
- (ip_width, ip_height),
356
- Image.Resampling.LANCZOS
 
 
 
 
 
357
  )
358
 
359
- # ----------------------------------------------------
360
- # SEGMENTATION
361
- # ----------------------------------------------------
362
 
363
- real_seg = np.array(
364
- segment_image(
365
- input_image,
366
- seg_image_processor,
367
- image_segmentor
368
- )
369
  )
370
 
371
- unique_colors = np.unique(
372
- real_seg.reshape(
373
- -1,
374
- real_seg.shape[2]
375
- ),
376
- axis=0
377
  )
378
 
379
- unique_colors = [
380
- tuple(color)
381
- for color in unique_colors
382
- ]
383
-
384
- segment_items = [
385
- map_colors_rgb(i)
386
- for i in unique_colors
387
- ]
388
 
389
- chosen_colors, segment_items = filter_items(
390
- colors_list=unique_colors,
391
- items_list=segment_items,
392
- items_to_remove=self.control_items
393
  )
394
 
395
- mask = np.zeros_like(real_seg)
396
-
397
- for color in chosen_colors:
398
-
399
- color_matches = (
400
- real_seg == color
401
- ).all(axis=2)
402
-
403
- mask[color_matches] = 1
404
-
405
- image_np = np.array(input_image)
406
 
407
- image = Image.fromarray(
408
- image_np
409
- ).convert("RGB")
410
 
411
- mask_image = Image.fromarray(
412
- (mask * 255).astype(np.uint8)
413
- ).convert("RGB")
414
 
415
- segmentation_cond_image = Image.fromarray(
416
- real_seg
417
- ).convert("RGB")
418
 
419
- # ----------------------------------------------------
420
  # DEPTH
421
- # ----------------------------------------------------
422
 
423
  image_depth = get_depth_image(
424
- image,
425
  depth_feature_extractor,
426
  depth_estimator
427
  )
428
 
429
  flush()
430
 
431
- # ----------------------------------------------------
432
  # IP-ADAPTER
433
- #
434
- # IMPORTANT:
435
- # Unlike the original StableDesign code,
436
- # we use the ACTUAL furniture photo here.
437
- # ----------------------------------------------------
438
 
439
  pipe.set_ip_adapter_scale(
440
  float(ip_scale)
441
  )
442
 
443
- # ----------------------------------------------------
444
- # GENERATION
445
- # ----------------------------------------------------
446
 
447
  generated_image = pipe(
448
  prompt=pos_prompt,
 
449
  negative_prompt=self.neg_prompt,
450
 
451
- num_inference_steps=num_steps,
 
 
452
 
453
- strength=strength,
 
 
454
 
455
- guidance_scale=guidance_scale,
 
 
456
 
457
  generator=self.generator,
458
 
459
- image=image,
460
 
461
  mask_image=mask_image,
462
 
463
- ip_adapter_image=furniture_ip,
 
 
 
 
 
464
 
465
  control_image=[
466
  image_depth,
467
- segmentation_cond_image
468
  ],
469
 
470
  controlnet_conditioning_scale=[
471
- 0.5,
472
- 0.5
473
  ]
474
-
475
  ).images[0]
476
 
477
  flush()
478
 
479
- # ----------------------------------------------------
480
- # RESTORE ORIGINAL SIZE
481
- # ----------------------------------------------------
482
-
483
  design_image = generated_image.resize(
484
  (orig_w, orig_h),
485
  Image.Resampling.LANCZOS
486
  )
487
 
488
- print("Generation completed.")
 
 
489
 
490
  return design_image
491
 
@@ -497,30 +808,35 @@ class ControlNetDepthDesignModelMulti:
497
  def create_demo(model):
498
 
499
  gr.Markdown(
500
- "### StableDesign Furniture"
501
  )
502
 
503
  gr.Markdown(
504
- "Upload a room photo and a furniture/product photo. "
505
- "The AI will place the furniture into the room."
 
 
 
 
 
 
 
 
 
506
  )
507
 
508
  with gr.Row():
509
 
510
- # ----------------------------------------------------
511
- # LEFT
512
- # ----------------------------------------------------
513
-
514
  with gr.Column():
515
 
516
  room_image = gr.Image(
517
- label="1. Room photo",
518
  type="pil",
519
  elem_id="img-display-input"
520
  )
521
 
522
  furniture_image = gr.Image(
523
- label="2. Furniture / product photo",
524
  type="pil",
525
  elem_id="img-display-input"
526
  )
@@ -528,12 +844,16 @@ def create_demo(model):
528
  prompt = gr.Textbox(
529
  label="Instructions",
530
  value=(
531
- "Place the furniture from the second image "
532
- "naturally in the room. Preserve the furniture's "
533
- "shape, design, color and materials. Match the "
534
- "perspective, scale, lighting and shadows to the room."
 
 
 
 
535
  ),
536
- lines=4
537
  )
538
 
539
  with gr.Accordion(
@@ -543,9 +863,9 @@ def create_demo(model):
543
 
544
  num_steps = gr.Slider(
545
  label="Steps",
546
- minimum=10,
547
  maximum=50,
548
- value=30,
549
  step=1
550
  )
551
 
@@ -559,25 +879,25 @@ def create_demo(model):
559
 
560
  guidance_scale = gr.Slider(
561
  label="Guidance Scale",
562
- minimum=1.0,
563
- maximum=20.0,
564
  value=7.5,
565
  step=0.5
566
  )
567
 
568
  strength = gr.Slider(
569
  label="Room editing strength",
570
- minimum=0.3,
571
- maximum=1.0,
572
- value=0.85,
573
  step=0.05
574
  )
575
 
576
  ip_scale = gr.Slider(
577
- label="Furniture reference strength",
578
- minimum=0.2,
579
- maximum=1.2,
580
- value=0.75,
581
  step=0.05
582
  )
583
 
@@ -593,30 +913,37 @@ def create_demo(model):
593
  a_prompt = gr.Textbox(
594
  label="Added Prompt",
595
  value=(
596
- "interior design, 4K, high resolution, "
597
- "photorealistic"
 
 
 
598
  )
599
  )
600
 
601
  n_prompt = gr.Textbox(
602
  label="Negative Prompt",
603
  value=(
604
- "window, door, low resolution, banner, logo, "
605
- "watermark, text, deformed, blurry, out of focus, "
606
- "surreal, ugly, beginner, duplicated furniture, "
607
- "extra furniture, distorted furniture"
608
- )
 
 
 
 
 
 
 
 
609
  )
610
 
611
  submit = gr.Button(
612
- "Generate furnished room",
613
  variant="primary"
614
  )
615
 
616
- # ----------------------------------------------------
617
- # RIGHT
618
- # ----------------------------------------------------
619
-
620
  with gr.Column():
621
 
622
  design_image = gr.Image(
@@ -624,10 +951,6 @@ def create_demo(model):
624
  elem_id="img-display-output"
625
  )
626
 
627
- # ========================================================
628
- # SUBMIT
629
- # ========================================================
630
-
631
  def on_submit(
632
  room,
633
  furniture,
@@ -643,13 +966,15 @@ def create_demo(model):
643
  ):
644
 
645
  if room is None:
 
646
  raise gr.Error(
647
- "Please upload a photo of the room."
648
  )
649
 
650
  if furniture is None:
 
651
  raise gr.Error(
652
- "Please upload a photo of the furniture."
653
  )
654
 
655
  model.seed = int(seed)
@@ -664,11 +989,26 @@ def create_demo(model):
664
  room,
665
  furniture,
666
  text,
667
- guidance_scale=guidance_scale,
668
- num_steps=int(num_steps),
669
- strength=strength,
670
- img_size=int(img_size),
671
- ip_scale=ip_scale
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  )
673
 
674
  return out_img
@@ -698,12 +1038,21 @@ def create_demo(model):
698
  # LOAD MODELS
699
  # ============================================================
700
 
 
 
 
 
701
  controlnet_depth = ControlNetModel.from_pretrained(
702
  "controlnet_depth",
703
  torch_dtype=dtype,
704
  use_safetensors=True
705
  )
706
 
 
 
 
 
 
707
  controlnet_seg = ControlNetModel.from_pretrained(
708
  "own_controlnet",
709
  torch_dtype=dtype,
@@ -711,6 +1060,10 @@ controlnet_seg = ControlNetModel.from_pretrained(
711
  )
712
 
713
 
 
 
 
 
714
  pipe = StableDiffusionControlNetInpaintPipeline.from_pretrained(
715
  "SG161222/Realistic_Vision_V5.1_noVAE",
716
 
@@ -729,36 +1082,71 @@ pipe = StableDiffusionControlNetInpaintPipeline.from_pretrained(
729
  # IP-ADAPTER
730
  # ============================================================
731
 
 
 
 
 
732
  pipe.load_ip_adapter(
733
  "h94/IP-Adapter",
734
  subfolder="models",
735
  weight_name="ip-adapter_sd15.bin"
736
  )
737
 
738
- pipe.set_ip_adapter_scale(0.75)
 
 
 
739
 
740
- pipe.scheduler = UniPCMultistepScheduler.from_config(
741
- pipe.scheduler.config
 
 
 
 
 
 
742
  )
743
 
744
- pipe = pipe.to(device)
 
 
 
745
 
746
 
747
  # ============================================================
748
- # VISION MODELS
749
  # ============================================================
750
 
751
- seg_image_processor, image_segmentor = (
752
- get_segmentation_pipeline()
 
 
 
 
 
 
 
 
 
753
  )
754
 
755
- image_segmentor = image_segmentor.to(device)
756
 
757
- depth_feature_extractor, depth_estimator = (
758
- get_depth_pipeline()
 
 
 
 
759
  )
760
 
761
- depth_estimator = depth_estimator.to(device)
 
 
 
 
 
 
 
762
 
763
 
764
  # ============================================================
@@ -767,19 +1155,34 @@ depth_estimator = depth_estimator.to(device)
767
 
768
  def main():
769
 
770
- model = ControlNetDepthDesignModelMulti()
 
 
771
 
772
  print(
773
- "StableDesign Furniture models loaded successfully."
 
774
  )
775
 
776
- title = "# StableDesign Furniture"
 
 
777
 
778
  description = """
779
  <p>
780
- Upload a photo of an empty room and a photo of a furniture item.
781
- The furniture image is used as a visual reference and the room
782
- geometry is preserved using depth and semantic segmentation.
 
 
 
 
 
 
 
 
 
 
783
  </p>
784
  """
785
 
@@ -787,11 +1190,17 @@ def main():
787
  css=css
788
  ) as demo:
789
 
790
- gr.Markdown(title)
 
 
791
 
792
- gr.Markdown(description)
 
 
793
 
794
- create_demo(model)
 
 
795
 
796
  demo.queue().launch(
797
  share=False
@@ -799,4 +1208,5 @@ def main():
799
 
800
 
801
  if __name__ == "__main__":
 
802
  main()
 
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
+
12
+ from diffusers.pipelines.controlnet import (
13
+ StableDiffusionControlNetInpaintPipeline
14
+ )
15
+
16
  from diffusers import (
17
  ControlNetModel,
18
  UniPCMultistepScheduler,
19
  )
20
+
21
  from transformers import (
22
  AutoImageProcessor,
23
  UperNetForSemanticSegmentation,
24
  AutoModelForDepthEstimation,
25
  )
26
+
27
  from colors import ade_palette
28
  from utils import map_colors_rgb
29
+
30
  import gradio as gr
31
 
32
 
33
  # ============================================================
34
+ # CONFIG
35
  # ============================================================
36
 
37
  device = "cuda"
 
39
 
40
 
41
  # ============================================================
42
+ # UI CSS
43
  # ============================================================
44
 
45
  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
84
+
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")
119
+
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),
132
+ Image.Resampling.LANCZOS
133
+ )
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,
146
+ (x, y)
147
+ )
148
+
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,
 
241
  ).pixel_values.to(device)
242
 
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,
253
+ target_sizes=[image.size[::-1]]
254
+ )[0]
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
272
+ )
273
 
274
+ palette = np.array(
275
+ ade_palette()
276
+ )
277
 
278
  for label, color in enumerate(palette):
 
279
 
280
+ color_seg[
281
+ seg == label,
282
+ :
283
+ ] = color
284
 
285
+ return Image.fromarray(
286
+ color_seg
287
+ ).convert("RGB")
288
 
289
 
290
  # ============================================================
 
303
  torch_dtype=dtype
304
  )
305
 
306
+ return (
307
+ feature_extractor,
308
+ depth_estimator
309
+ )
310
 
311
 
312
  @torch.inference_mode()
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,
 
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
 
 
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",
392
+ "bed",
393
+ "cabinet",
394
+ "shelf",
395
+ "desk",
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",
423
+ "counter",
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
  # ============================================================
 
563
  self.seed = 323 * 111
564
 
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
 
 
609
  int(self.seed)
610
  )
611
 
 
 
 
 
612
  if prompt is None:
613
  prompt = ""
614
 
615
  prompt = prompt.strip()
616
 
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
  + 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,
753
 
754
+ num_inference_steps=int(
755
+ num_steps
756
+ ),
757
 
758
+ strength=float(
759
+ strength
760
+ ),
761
 
762
+ guidance_scale=float(
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
 
 
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
  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
  )
858
 
859
  with gr.Accordion(
 
863
 
864
  num_steps = gr.Slider(
865
  label="Steps",
866
+ minimum=20,
867
  maximum=50,
868
+ value=35,
869
  step=1
870
  )
871
 
 
879
 
880
  guidance_scale = gr.Slider(
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
  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(
925
  label="Negative Prompt",
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(
 
951
  elem_id="img-display-output"
952
  )
953
 
 
 
 
 
954
  def on_submit(
955
  room,
956
  furniture,
 
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)
 
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
 
1038
  # LOAD MODELS
1039
  # ============================================================
1040
 
1041
+ print(
1042
+ "Loading ControlNet depth..."
1043
+ )
1044
+
1045
  controlnet_depth = ControlNetModel.from_pretrained(
1046
  "controlnet_depth",
1047
  torch_dtype=dtype,
1048
  use_safetensors=True
1049
  )
1050
 
1051
+
1052
+ print(
1053
+ "Loading ControlNet segmentation..."
1054
+ )
1055
+
1056
  controlnet_seg = ControlNetModel.from_pretrained(
1057
  "own_controlnet",
1058
  torch_dtype=dtype,
 
1060
  )
1061
 
1062
 
1063
+ print(
1064
+ "Loading Stable Diffusion..."
1065
+ )
1066
+
1067
  pipe = StableDiffusionControlNetInpaintPipeline.from_pretrained(
1068
  "SG161222/Realistic_Vision_V5.1_noVAE",
1069
 
 
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
 
1100
+ # ============================================================
1101
+ # SCHEDULER
1102
+ # ============================================================
1103
+
1104
+ pipe.scheduler = (
1105
+ UniPCMultistepScheduler.from_config(
1106
+ pipe.scheduler.config
1107
+ )
1108
  )
1109
 
1110
+
1111
+ pipe = pipe.to(
1112
+ device
1113
+ )
1114
 
1115
 
1116
  # ============================================================
1117
+ # SEGMENTATION MODEL
1118
  # ============================================================
1119
 
1120
+ print(
1121
+ "Loading segmentation model..."
1122
+ )
1123
+
1124
+ (
1125
+ seg_image_processor,
1126
+ image_segmentor
1127
+ ) = get_segmentation_pipeline()
1128
+
1129
+ image_segmentor = (
1130
+ image_segmentor.to(device)
1131
  )
1132
 
 
1133
 
1134
+ # ============================================================
1135
+ # DEPTH MODEL
1136
+ # ============================================================
1137
+
1138
+ print(
1139
+ "Loading depth model..."
1140
  )
1141
 
1142
+ (
1143
+ depth_feature_extractor,
1144
+ depth_estimator
1145
+ ) = get_depth_pipeline()
1146
+
1147
+ depth_estimator = (
1148
+ depth_estimator.to(device)
1149
+ )
1150
 
1151
 
1152
  # ============================================================
 
1155
 
1156
  def main():
1157
 
1158
+ model = (
1159
+ ControlNetDepthDesignModelMulti()
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
 
 
1190
  css=css
1191
  ) as demo:
1192
 
1193
+ gr.Markdown(
1194
+ title
1195
+ )
1196
 
1197
+ gr.Markdown(
1198
+ description
1199
+ )
1200
 
1201
+ create_demo(
1202
+ model
1203
+ )
1204
 
1205
  demo.queue().launch(
1206
  share=False
 
1208
 
1209
 
1210
  if __name__ == "__main__":
1211
+
1212
  main()