midman79 commited on
Commit
2ab3ef3
·
verified ·
1 Parent(s): 002f607

add second image

Browse files
Files changed (1) hide show
  1. app.py +630 -204
app.py CHANGED
@@ -1,377 +1,803 @@
1
  import spaces
2
  from typing import Tuple, Union, List
3
  import os
 
4
 
5
  import numpy as np
6
  from PIL import Image
7
 
8
  import torch
9
  from diffusers.pipelines.controlnet import StableDiffusionControlNetInpaintPipeline
10
- from diffusers import ControlNetModel, UniPCMultistepScheduler, AutoPipelineForText2Image
11
- from transformers import AutoImageProcessor, UperNetForSemanticSegmentation, AutoModelForDepthEstimation
 
 
 
 
 
 
 
12
  from colors import ade_palette
13
  from utils import map_colors_rgb
14
- from diffusers import StableDiffusionXLPipeline
15
  import gradio as gr
16
- import gc
 
 
 
 
17
 
18
  device = "cuda"
19
  dtype = torch.float16
20
 
21
-
 
 
 
 
22
  css = """
23
  #img-display-container {
24
  max-height: 50vh;
25
- }
 
26
  #img-display-input {
27
  max-height: 40vh;
28
- }
 
29
  #img-display-output {
30
  max-height: 40vh;
31
- }
32
-
33
  """
34
 
35
 
 
 
 
 
36
  def filter_items(
37
  colors_list: Union[List, np.ndarray],
38
  items_list: Union[List, np.ndarray],
39
  items_to_remove: Union[List, np.ndarray]
40
  ) -> Tuple[Union[List, np.ndarray], Union[List, np.ndarray]]:
41
- """
42
- Filters items and their corresponding colors from given lists, excluding
43
- specified items.
44
-
45
- Args:
46
- colors_list: A list or numpy array of colors corresponding to items.
47
- items_list: A list or numpy array of items.
48
- items_to_remove: A list or numpy array of items to be removed.
49
 
50
- Returns:
51
- A tuple of two lists or numpy arrays: filtered colors and filtered
52
- items.
53
- """
54
  filtered_colors = []
55
  filtered_items = []
 
56
  for color, item in zip(colors_list, items_list):
57
  if item not in items_to_remove:
58
  filtered_colors.append(color)
59
  filtered_items.append(item)
 
60
  return filtered_colors, filtered_items
61
 
 
62
  def get_segmentation_pipeline(
63
  ) -> Tuple[AutoImageProcessor, UperNetForSemanticSegmentation]:
64
- """Method to load the segmentation pipeline
65
- Returns:
66
- Tuple[AutoImageProcessor, UperNetForSemanticSegmentation]: segmentation pipeline
67
- """
68
  image_processor = AutoImageProcessor.from_pretrained(
69
  "openmmlab/upernet-convnext-small"
70
  )
 
71
  image_segmentor = UperNetForSemanticSegmentation.from_pretrained(
72
  "openmmlab/upernet-convnext-small"
73
  )
 
74
  return image_processor, image_segmentor
75
 
76
 
77
  @torch.inference_mode()
78
  @spaces.GPU
79
  def segment_image(
80
- image: Image,
81
- image_processor: AutoImageProcessor,
82
- image_segmentor: UperNetForSemanticSegmentation
83
  ) -> Image:
84
- """
85
- Segments an image using a semantic segmentation model.
86
-
87
- Args:
88
- image (Image): The input image to be segmented.
89
- image_processor (AutoImageProcessor): The processor to prepare the
90
- image for segmentation.
91
- image_segmentor (UperNetForSemanticSegmentation): The semantic
92
- segmentation model used to identify different segments in the image.
93
-
94
- Returns:
95
- Image: The segmented image with each segment colored differently based
96
- on its identified class.
97
- """
98
- # image_processor, image_segmentor = get_segmentation_pipeline()
99
- pixel_values = image_processor(image, return_tensors="pt").pixel_values
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]])[0]
105
- color_seg = np.zeros((seg.shape[0], seg.shape[1], 3), dtype=np.uint8)
 
 
 
 
 
 
 
106
  palette = np.array(ade_palette())
 
107
  for label, color in enumerate(palette):
108
  color_seg[seg == label, :] = color
 
109
  color_seg = color_seg.astype(np.uint8)
110
- seg_image = Image.fromarray(color_seg).convert('RGB')
111
- return seg_image
112
 
 
 
 
 
 
 
113
 
114
  def get_depth_pipeline():
115
- feature_extractor = AutoImageProcessor.from_pretrained("LiheYoung/depth-anything-large-hf",
116
- torch_dtype=dtype)
117
- depth_estimator = AutoModelForDepthEstimation.from_pretrained("LiheYoung/depth-anything-large-hf",
118
- torch_dtype=dtype)
 
 
 
 
 
 
 
119
  return feature_extractor, depth_estimator
120
 
121
 
122
  @torch.inference_mode()
123
  @spaces.GPU
124
  def get_depth_image(
125
- image: Image,
126
- feature_extractor: AutoImageProcessor,
127
- depth_estimator: AutoModelForDepthEstimation
128
  ) -> Image:
129
- image_to_depth = feature_extractor(images=image, return_tensors="pt").to(device)
 
 
 
 
 
130
  with torch.no_grad():
131
- depth_map = depth_estimator(**image_to_depth).predicted_depth
 
 
132
 
133
  width, height = image.size
 
134
  depth_map = torch.nn.functional.interpolate(
135
  depth_map.unsqueeze(1).float(),
136
  size=(height, width),
137
  mode="bicubic",
138
  align_corners=False,
139
  )
140
- depth_min = torch.amin(depth_map, dim=[1, 2, 3], keepdim=True)
141
- depth_max = torch.amax(depth_map, dim=[1, 2, 3], keepdim=True)
142
- depth_map = (depth_map - depth_min) / (depth_max - depth_min)
143
- image = torch.cat([depth_map] * 3, dim=1)
144
 
145
- image = image.permute(0, 2, 3, 1).cpu().numpy()[0]
146
- image = Image.fromarray((image * 255.0).clip(0, 255).astype(np.uint8))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  return image
148
 
149
 
 
 
 
 
150
  def resize_dimensions(dimensions, target_size):
151
- """
152
- Resize PIL to target size while maintaining aspect ratio
153
- If smaller than target size leave it as is
154
- """
155
  width, height = dimensions
156
 
157
- # Check if both dimensions are smaller than the target size
158
  if width < target_size and height < target_size:
159
  return dimensions
160
 
161
- # Determine the larger side
162
  if width > height:
163
- # Calculate the aspect ratio
164
  aspect_ratio = height / width
165
- # Resize dimensions
166
- return (target_size, int(target_size * aspect_ratio))
 
 
 
 
167
  else:
168
- # Calculate the aspect ratio
169
  aspect_ratio = width / height
170
- # Resize dimensions
171
- return (int(target_size * aspect_ratio), target_size)
172
 
 
 
 
 
 
 
 
 
 
173
 
174
  def flush():
 
175
  gc.collect()
176
- torch.cuda.empty_cache()
177
-
178
-
 
 
 
 
 
 
179
  class ControlNetDepthDesignModelMulti:
180
- """ Produces random noise images """
181
-
182
  def __init__(self):
183
- """ Initialize your model(s) here """
184
- #os.environ['HF_HUB_OFFLINE'] = "True"
185
-
186
- self.seed = 323*111
187
- self.neg_prompt = "window, door, low resolution, banner, logo, watermark, text, deformed, blurry, out of focus, surreal, ugly, beginner"
188
- self.control_items = ["windowpane;window", "door;double;door"]
189
- self.additional_quality_suffix = "interior design, 4K, high resolution, photorealistic"
190
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  @spaces.GPU
192
- def generate_design(self, empty_room_image: Image, prompt: str, guidance_scale: int = 10, num_steps: int = 50, strength: float =0.9, img_size: int = 640) -> Image:
193
- """
194
- Given an image of an empty room and a prompt
195
- generate the designed room according to the prompt
196
- Inputs -
197
- empty_room_image - An RGB PIL Image of the empty room
198
- prompt - Text describing the target design elements of the room
199
- Returns -
200
- design_image - PIL Image of the same size as the empty room image
201
- If the size is not the same the submission will fail.
202
- """
203
- print(prompt)
 
 
204
  flush()
205
- self.generator = torch.Generator(device=device).manual_seed(self.seed)
206
 
207
- pos_prompt = prompt + f', {self.additional_quality_suffix}'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
  orig_w, orig_h = empty_room_image.size
210
- new_width, new_height = resize_dimensions(empty_room_image.size, img_size)
211
- input_image = empty_room_image.resize((new_width, new_height))
212
- real_seg = np.array(segment_image(input_image,
213
- seg_image_processor,
214
- image_segmentor))
215
- unique_colors = np.unique(real_seg.reshape(-1, real_seg.shape[2]), axis=0)
216
- unique_colors = [tuple(color) for color in unique_colors]
217
- segment_items = [map_colors_rgb(i) for i in unique_colors]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  chosen_colors, segment_items = filter_items(
219
  colors_list=unique_colors,
220
  items_list=segment_items,
221
  items_to_remove=self.control_items
222
  )
 
223
  mask = np.zeros_like(real_seg)
 
224
  for color in chosen_colors:
225
- color_matches = (real_seg == color).all(axis=2)
 
 
 
 
226
  mask[color_matches] = 1
227
 
228
  image_np = np.array(input_image)
229
- image = Image.fromarray(image_np).convert("RGB")
230
- mask_image = Image.fromarray((mask * 255).astype(np.uint8)).convert("RGB")
231
- segmentation_cond_image = Image.fromarray(real_seg).convert("RGB")
232
 
233
- image_depth = get_depth_image(image, depth_feature_extractor, depth_estimator)
 
 
234
 
235
- # generate image that would be used as IP-adapter
236
- flush()
237
- new_width_ip = int(new_width / 8) * 8
238
- new_height_ip = int(new_height / 8) * 8
239
- ip_image = guide_pipe(pos_prompt,
240
- num_inference_steps=num_steps,
241
- negative_prompt=self.neg_prompt,
242
- height=new_height_ip,
243
- width=new_width_ip,
244
- generator=[self.generator]).images[0]
 
 
 
 
 
 
 
245
 
246
  flush()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  generated_image = pipe(
248
  prompt=pos_prompt,
249
  negative_prompt=self.neg_prompt,
 
250
  num_inference_steps=num_steps,
 
251
  strength=strength,
 
252
  guidance_scale=guidance_scale,
253
- generator=[self.generator],
 
 
254
  image=image,
 
255
  mask_image=mask_image,
256
- ip_adapter_image=ip_image,
257
- control_image=[image_depth, segmentation_cond_image],
258
- controlnet_conditioning_scale=[0.5, 0.5]
 
 
 
 
 
 
 
 
 
 
259
  ).images[0]
260
-
261
  flush()
 
 
 
 
 
262
  design_image = generated_image.resize(
263
- (orig_w, orig_h), Image.Resampling.LANCZOS
 
264
  )
265
-
 
 
266
  return design_image
267
 
268
 
 
 
 
 
269
  def create_demo(model):
270
- gr.Markdown("### Stable Design demo")
 
 
 
 
 
 
 
 
 
271
  with gr.Row():
 
 
 
 
 
272
  with gr.Column():
273
- input_image = gr.Image(label="Input Image", type='pil', elem_id='img-display-input')
274
- input_text = gr.Textbox(label='Prompt', placeholder='Please upload your image first', lines=2)
275
- with gr.Accordion('Advanced options', open=False):
276
- num_steps = gr.Slider(label='Steps',
277
- minimum=1,
278
- maximum=50,
279
- value=50,
280
- step=1)
281
- img_size = gr.Slider(label='Image size',
282
- minimum=256,
283
- maximum=768,
284
- value=768,
285
- step=64)
286
- guidance_scale = gr.Slider(label='Guidance Scale',
287
- minimum=0.1,
288
- maximum=30.0,
289
- value=10.0,
290
- step=0.1)
291
- seed = gr.Slider(label='Seed',
292
- minimum=-1,
293
- maximum=2147483647,
294
- value=323*111,
295
- step=1,
296
- randomize=True)
297
- strength = gr.Slider(label='Strength',
298
- minimum=0.1,
299
- maximum=1.0,
300
- value=0.9,
301
- step=0.1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  a_prompt = gr.Textbox(
303
- label='Added Prompt',
304
- value="interior design, 4K, high resolution, photorealistic")
 
 
 
 
 
305
  n_prompt = gr.Textbox(
306
- label='Negative Prompt',
307
- value="window, door, low resolution, banner, logo, watermark, text, deformed, blurry, out of focus, surreal, ugly, beginner")
308
- submit = gr.Button("Submit")
309
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  with gr.Column():
311
- design_image = gr.Image(label="Output Mask", elem_id='img-display-output')
312
-
313
-
314
- def on_submit(image, text, num_steps, guidance_scale, seed, strength, a_prompt, n_prompt, img_size):
315
- model.seed = seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  model.neg_prompt = n_prompt
 
317
  model.additional_quality_suffix = a_prompt
318
-
319
  with torch.no_grad():
320
- out_img = model.generate_design(image, text, guidance_scale=guidance_scale, num_steps=num_steps, strength=strength, img_size=img_size)
 
 
 
 
 
 
 
 
 
 
321
 
322
  return out_img
323
 
324
- submit.click(on_submit, inputs=[input_image, input_text, num_steps, guidance_scale, seed, strength, a_prompt, n_prompt, img_size], outputs=design_image)
325
- examples = gr.Examples(examples=[["imgs/bedroom_1.jpg", "An elegantly appointed bedroom in the Art Deco style, featuring a grand king-size bed with geometric bedding, a luxurious velvet armchair, and a mirrored nightstand that reflects the room's opulence. Art Deco-inspired artwork adds a touch of glamour"], ["imgs/bedroom_2.jpg", "A bedroom that exudes French country charm with a soft upholstered bed, walls adorned with floral wallpaper, and a vintage wooden wardrobe. A crystal chandelier casts a warm, inviting glow over the space"], ["imgs/dinning_room_1.jpg", "A cozy dining room that captures the essence of rustic charm with a solid wooden farmhouse table at its core, surrounded by an eclectic mix of mismatched chairs. An antique sideboard serves as a statement piece, and the ambiance is warmly lit by a series of quaint Edison bulbs dangling from the ceiling"], ["imgs/dinning_room_3.jpg", "A dining room that epitomizes contemporary elegance, anchored by a sleek, minimalist dining table paired with stylish modern chairs. Artistic lighting fixtures create a focal point above, while the surrounding minimalist decor ensures the space feels open, airy, and utterly modern"], ["imgs/image_1.jpg", "A glamorous master bedroom in Hollywood Regency style, boasting a plush tufted headboard, mirrored furniture reflecting elegance, luxurious fabrics in rich textures, and opulent gold accents for a touch of luxury."], ["imgs/image_2.jpg", "A vibrant living room with a tropical theme, complete with comfortable rattan furniture, large leafy plants bringing the outdoors in, bright cushions adding pops of color, and bamboo blinds for natural light control."], ["imgs/living_room_1.jpg", "A stylish living room embracing mid-century modern aesthetics, featuring a vintage teak coffee table at its center, complemented by a classic sunburst clock on the wall and a cozy shag rug underfoot, creating a warm and inviting atmosphere"]],
326
- inputs=[input_image, input_text], cache_examples=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
 
329
- controlnet_depth= ControlNetModel.from_pretrained(
330
- "controlnet_depth", torch_dtype=dtype, use_safetensors=True)
 
 
 
 
 
 
 
 
331
  controlnet_seg = ControlNetModel.from_pretrained(
332
- "own_controlnet", torch_dtype=dtype, use_safetensors=True)
 
 
 
 
333
 
334
  pipe = StableDiffusionControlNetInpaintPipeline.from_pretrained(
335
  "SG161222/Realistic_Vision_V5.1_noVAE",
336
- #"models/runwayml--stable-diffusion-inpainting",
337
- controlnet=[controlnet_depth, controlnet_seg],
 
 
 
 
338
  safety_checker=None,
 
339
  torch_dtype=dtype
340
  )
341
 
342
- pipe.load_ip_adapter("h94/IP-Adapter", subfolder="models",
343
- weight_name="ip-adapter_sd15.bin")
344
- pipe.set_ip_adapter_scale(0.4)
345
- pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config)
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  pipe = pipe.to(device)
347
- guide_pipe = StableDiffusionXLPipeline.from_pretrained("segmind/SSD-1B",
348
- torch_dtype=dtype, use_safetensors=True, variant="fp16")
349
- guide_pipe = guide_pipe.to(device)
350
-
351
- seg_image_processor, image_segmentor = get_segmentation_pipeline()
352
- depth_feature_extractor, depth_estimator = get_depth_pipeline()
 
 
 
 
 
 
 
 
 
 
353
  depth_estimator = depth_estimator.to(device)
354
 
355
 
 
 
 
 
356
  def main():
 
357
  model = ControlNetDepthDesignModelMulti()
358
- print('Models uploaded successfully')
359
-
360
- title = "# StableDesign"
 
 
 
 
361
  description = """
362
- <p style='font-size: 14px; margin-bottom: 10px;'><a href='https://www.linkedin.com/in/mykola-lavreniuk/'>Mykola Lavreniuk</a>, <a href='https://www.linkedin.com/in/bartosz-ludwiczuk-a677a760/'>Bartosz Ludwiczuk</a></p>
363
- <p style='font-size: 16px; margin-bottom: 0px; margin-top=0px;'>Official demo for <strong>StableDesign:</strong> 2nd place solution for the Generative Interior Design 2024 <a href='https://www.aicrowd.com/challenges/generative-interior-design-challenge-2024/leaderboards?challenge_round_id=1314'>competition</a>. StableDesign is a deep learning model designed to harness the power of AI, providing innovative and creative tools for designers. Using our algorithms, images of empty rooms can be transformed into fully furnished spaces based on text descriptions. Please refer to our <a href='https://github.com/Lavreniuk/generative-interior-design'>GitHub</a> for more details.</p>
 
 
 
364
  """
365
- with gr.Blocks() as demo:
 
 
 
 
366
  gr.Markdown(title)
 
367
  gr.Markdown(description)
368
-
369
  create_demo(model)
370
- gr.HTML('''<br><br><br><center>You can duplicate this Space to skip the queue:<a href="https://huggingface.co/spaces/MykolaL/StableDesign?duplicate=true"><img src="https://bit.ly/3gLdBN6" alt="Duplicate Space"></a><br>
371
- <p><img src="https://visitor-badge.glitch.me/badge?page_id=MykolaL/StableDesign" alt="visitors"></p></center>''')
372
 
373
- demo.queue().launch(share=False)
 
 
374
 
375
 
376
- if __name__ == '__main__':
377
  main()
 
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"
30
  dtype = torch.float16
31
 
32
+
33
+ # ============================================================
34
+ # CSS
35
+ # ============================================================
36
+
37
  css = """
38
  #img-display-container {
39
  max-height: 50vh;
40
+ }
41
+
42
  #img-display-input {
43
  max-height: 40vh;
44
+ }
45
+
46
  #img-display-output {
47
  max-height: 40vh;
48
+ }
 
49
  """
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,
97
+ return_tensors="pt"
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,
105
+ target_sizes=[image.size[::-1]]
106
+ )[0]
107
+
108
+ color_seg = np.zeros(
109
+ (seg.shape[0], seg.shape[1], 3),
110
+ dtype=np.uint8
111
+ )
112
+
113
  palette = np.array(ade_palette())
114
+
115
  for label, color in enumerate(palette):
116
  color_seg[seg == label, :] = color
117
+
118
  color_seg = color_seg.astype(np.uint8)
 
 
119
 
120
+ return Image.fromarray(color_seg).convert("RGB")
121
+
122
+
123
+ # ============================================================
124
+ # DEPTH
125
+ # ============================================================
126
 
127
  def get_depth_pipeline():
128
+
129
+ feature_extractor = AutoImageProcessor.from_pretrained(
130
+ "LiheYoung/depth-anything-large-hf",
131
+ torch_dtype=dtype
132
+ )
133
+
134
+ depth_estimator = AutoModelForDepthEstimation.from_pretrained(
135
+ "LiheYoung/depth-anything-large-hf",
136
+ torch_dtype=dtype
137
+ )
138
+
139
  return feature_extractor, depth_estimator
140
 
141
 
142
  @torch.inference_mode()
143
  @spaces.GPU
144
  def get_depth_image(
145
+ image: Image,
146
+ feature_extractor: AutoImageProcessor,
147
+ depth_estimator: AutoModelForDepthEstimation
148
  ) -> Image:
149
+
150
+ image_to_depth = feature_extractor(
151
+ images=image,
152
+ return_tensors="pt"
153
+ ).to(device)
154
+
155
  with torch.no_grad():
156
+ depth_map = depth_estimator(
157
+ **image_to_depth
158
+ ).predicted_depth
159
 
160
  width, height = image.size
161
+
162
  depth_map = torch.nn.functional.interpolate(
163
  depth_map.unsqueeze(1).float(),
164
  size=(height, width),
165
  mode="bicubic",
166
  align_corners=False,
167
  )
 
 
 
 
168
 
169
+ depth_min = torch.amin(
170
+ depth_map,
171
+ dim=[1, 2, 3],
172
+ keepdim=True
173
+ )
174
+
175
+ depth_max = torch.amax(
176
+ depth_map,
177
+ dim=[1, 2, 3],
178
+ keepdim=True
179
+ )
180
+
181
+ depth_map = (
182
+ depth_map - depth_min
183
+ ) / (
184
+ depth_max - depth_min + 1e-8
185
+ )
186
+
187
+ image = torch.cat(
188
+ [depth_map] * 3,
189
+ dim=1
190
+ )
191
+
192
+ image = image.permute(
193
+ 0, 2, 3, 1
194
+ ).cpu().numpy()[0]
195
+
196
+ image = Image.fromarray(
197
+ (image * 255.0)
198
+ .clip(0, 255)
199
+ .astype(np.uint8)
200
+ )
201
+
202
  return image
203
 
204
 
205
+ # ============================================================
206
+ # RESIZE
207
+ # ============================================================
208
+
209
  def resize_dimensions(dimensions, target_size):
210
+
 
 
 
211
  width, height = dimensions
212
 
 
213
  if width < target_size and height < target_size:
214
  return dimensions
215
 
 
216
  if width > height:
 
217
  aspect_ratio = height / width
218
+
219
+ return (
220
+ target_size,
221
+ int(target_size * aspect_ratio)
222
+ )
223
+
224
  else:
 
225
  aspect_ratio = width / height
 
 
226
 
227
+ return (
228
+ int(target_size * aspect_ratio),
229
+ target_size
230
+ )
231
+
232
+
233
+ # ============================================================
234
+ # MEMORY CLEANUP
235
+ # ============================================================
236
 
237
  def flush():
238
+
239
  gc.collect()
240
+
241
+ if torch.cuda.is_available():
242
+ torch.cuda.empty_cache()
243
+
244
+
245
+ # ============================================================
246
+ # MAIN MODEL
247
+ # ============================================================
248
+
249
  class ControlNetDepthDesignModelMulti:
250
+
 
251
  def __init__(self):
252
+
253
+ self.seed = 323 * 111
254
+
255
+ self.neg_prompt = (
256
+ "window, door, low resolution, banner, logo, watermark, "
257
+ "text, deformed, blurry, out of focus, surreal, ugly, "
258
+ "beginner, duplicated furniture, extra furniture"
259
+ )
260
+
261
+ self.control_items = [
262
+ "windowpane;window",
263
+ "door;double;door"
264
+ ]
265
+
266
+ self.additional_quality_suffix = (
267
+ "interior design, 4K, high resolution, photorealistic"
268
+ )
269
+
270
+ # ========================================================
271
+ # GENERATE
272
+ # ========================================================
273
+
274
  @spaces.GPU
275
+ def generate_design(
276
+ self,
277
+ empty_room_image: Image,
278
+ furniture_image: Image,
279
+ prompt: str,
280
+ guidance_scale: int = 10,
281
+ num_steps: int = 30,
282
+ strength: float = 0.85,
283
+ img_size: int = 640,
284
+ ip_scale: float = 0.75
285
+ ) -> Image:
286
+
287
+ print("Starting furniture insertion...")
288
+
289
  flush()
 
290
 
291
+ self.generator = torch.Generator(
292
+ device=device
293
+ ).manual_seed(
294
+ int(self.seed)
295
+ )
296
+
297
+ # ----------------------------------------------------
298
+ # PROMPT
299
+ # ----------------------------------------------------
300
+
301
+ if prompt is None:
302
+ prompt = ""
303
+
304
+ prompt = prompt.strip()
305
+
306
+ if prompt == "":
307
+ prompt = (
308
+ "Place the furniture from the reference image "
309
+ "naturally inside the room."
310
+ )
311
+
312
+ pos_prompt = (
313
+ prompt
314
+ + ", "
315
+ + self.additional_quality_suffix
316
+ )
317
+
318
+ # ----------------------------------------------------
319
+ # ORIGINAL ROOM SIZE
320
+ # ----------------------------------------------------
321
 
322
  orig_w, orig_h = empty_room_image.size
323
+
324
+ # ----------------------------------------------------
325
+ # ROOM
326
+ # ----------------------------------------------------
327
+
328
+ new_width, new_height = resize_dimensions(
329
+ empty_room_image.size,
330
+ img_size
331
+ )
332
+
333
+ input_image = empty_room_image.convert(
334
+ "RGB"
335
+ ).resize(
336
+ (new_width, new_height)
337
+ )
338
+
339
+ # ----------------------------------------------------
340
+ # FURNITURE REFERENCE
341
+ # ----------------------------------------------------
342
+
343
+ furniture_image = furniture_image.convert("RGB")
344
+
345
+ ip_width = max(
346
+ 64,
347
+ int(new_width / 8) * 8
348
+ )
349
+
350
+ ip_height = max(
351
+ 64,
352
+ int(new_height / 8) * 8
353
+ )
354
+
355
+ furniture_ip = furniture_image.resize(
356
+ (ip_width, ip_height),
357
+ Image.Resampling.LANCZOS
358
+ )
359
+
360
+ # ----------------------------------------------------
361
+ # SEGMENTATION
362
+ # ----------------------------------------------------
363
+
364
+ real_seg = np.array(
365
+ segment_image(
366
+ input_image,
367
+ seg_image_processor,
368
+ image_segmentor
369
+ )
370
+ )
371
+
372
+ unique_colors = np.unique(
373
+ real_seg.reshape(
374
+ -1,
375
+ real_seg.shape[2]
376
+ ),
377
+ axis=0
378
+ )
379
+
380
+ unique_colors = [
381
+ tuple(color)
382
+ for color in unique_colors
383
+ ]
384
+
385
+ segment_items = [
386
+ map_colors_rgb(i)
387
+ for i in unique_colors
388
+ ]
389
+
390
  chosen_colors, segment_items = filter_items(
391
  colors_list=unique_colors,
392
  items_list=segment_items,
393
  items_to_remove=self.control_items
394
  )
395
+
396
  mask = np.zeros_like(real_seg)
397
+
398
  for color in chosen_colors:
399
+
400
+ color_matches = (
401
+ real_seg == color
402
+ ).all(axis=2)
403
+
404
  mask[color_matches] = 1
405
 
406
  image_np = np.array(input_image)
 
 
 
407
 
408
+ image = Image.fromarray(
409
+ image_np
410
+ ).convert("RGB")
411
 
412
+ mask_image = Image.fromarray(
413
+ (mask * 255).astype(np.uint8)
414
+ ).convert("RGB")
415
+
416
+ segmentation_cond_image = Image.fromarray(
417
+ real_seg
418
+ ).convert("RGB")
419
+
420
+ # ----------------------------------------------------
421
+ # DEPTH
422
+ # ----------------------------------------------------
423
+
424
+ image_depth = get_depth_image(
425
+ image,
426
+ depth_feature_extractor,
427
+ depth_estimator
428
+ )
429
 
430
  flush()
431
+
432
+ # ----------------------------------------------------
433
+ # IP-ADAPTER
434
+ #
435
+ # IMPORTANT:
436
+ # Unlike the original StableDesign code,
437
+ # we use the ACTUAL furniture photo here.
438
+ # ----------------------------------------------------
439
+
440
+ pipe.set_ip_adapter_scale(
441
+ float(ip_scale)
442
+ )
443
+
444
+ # ----------------------------------------------------
445
+ # GENERATION
446
+ # ----------------------------------------------------
447
+
448
  generated_image = pipe(
449
  prompt=pos_prompt,
450
  negative_prompt=self.neg_prompt,
451
+
452
  num_inference_steps=num_steps,
453
+
454
  strength=strength,
455
+
456
  guidance_scale=guidance_scale,
457
+
458
+ generator=self.generator,
459
+
460
  image=image,
461
+
462
  mask_image=mask_image,
463
+
464
+ ip_adapter_image=furniture_ip,
465
+
466
+ control_image=[
467
+ image_depth,
468
+ segmentation_cond_image
469
+ ],
470
+
471
+ controlnet_conditioning_scale=[
472
+ 0.5,
473
+ 0.5
474
+ ]
475
+
476
  ).images[0]
477
+
478
  flush()
479
+
480
+ # ----------------------------------------------------
481
+ # RESTORE ORIGINAL SIZE
482
+ # ----------------------------------------------------
483
+
484
  design_image = generated_image.resize(
485
+ (orig_w, orig_h),
486
+ Image.Resampling.LANCZOS
487
  )
488
+
489
+ print("Generation completed.")
490
+
491
  return design_image
492
 
493
 
494
+ # ============================================================
495
+ # GRADIO UI
496
+ # ============================================================
497
+
498
  def create_demo(model):
499
+
500
+ gr.Markdown(
501
+ "### StableDesign Furniture"
502
+ )
503
+
504
+ gr.Markdown(
505
+ "Upload a room photo and a furniture/product photo. "
506
+ "The AI will place the furniture into the room."
507
+ )
508
+
509
  with gr.Row():
510
+
511
+ # ----------------------------------------------------
512
+ # LEFT
513
+ # ----------------------------------------------------
514
+
515
  with gr.Column():
516
+
517
+ room_image = gr.Image(
518
+ label="1. Room photo",
519
+ type="pil",
520
+ elem_id="img-display-input"
521
+ )
522
+
523
+ furniture_image = gr.Image(
524
+ label="2. Furniture / product photo",
525
+ type="pil",
526
+ elem_id="img-display-input"
527
+ )
528
+
529
+ prompt = gr.Textbox(
530
+ label="Instructions",
531
+ value=(
532
+ "Place the furniture from the second image "
533
+ "naturally in the room. Preserve the furniture's "
534
+ "shape, design, color and materials. Match the "
535
+ "perspective, scale, lighting and shadows to the room."
536
+ ),
537
+ lines=4
538
+ )
539
+
540
+ with gr.Accordion(
541
+ "Advanced options",
542
+ open=False
543
+ ):
544
+
545
+ num_steps = gr.Slider(
546
+ label="Steps",
547
+ minimum=10,
548
+ maximum=50,
549
+ value=30,
550
+ step=1
551
+ )
552
+
553
+ img_size = gr.Slider(
554
+ label="Image size",
555
+ minimum=384,
556
+ maximum=768,
557
+ value=640,
558
+ step=64
559
+ )
560
+
561
+ guidance_scale = gr.Slider(
562
+ label="Guidance Scale",
563
+ minimum=1.0,
564
+ maximum=20.0,
565
+ value=7.5,
566
+ step=0.5
567
+ )
568
+
569
+ strength = gr.Slider(
570
+ label="Room editing strength",
571
+ minimum=0.3,
572
+ maximum=1.0,
573
+ value=0.85,
574
+ step=0.05
575
+ )
576
+
577
+ ip_scale = gr.Slider(
578
+ label="Furniture reference strength",
579
+ minimum=0.2,
580
+ maximum=1.2,
581
+ value=0.75,
582
+ step=0.05
583
+ )
584
+
585
+ seed = gr.Slider(
586
+ label="Seed",
587
+ minimum=-1,
588
+ maximum=2147483647,
589
+ value=323 * 111,
590
+ step=1,
591
+ randomize=True
592
+ )
593
+
594
  a_prompt = gr.Textbox(
595
+ label="Added Prompt",
596
+ value=(
597
+ "interior design, 4K, high resolution, "
598
+ "photorealistic"
599
+ )
600
+ )
601
+
602
  n_prompt = gr.Textbox(
603
+ label="Negative Prompt",
604
+ value=(
605
+ "window, door, low resolution, banner, logo, "
606
+ "watermark, text, deformed, blurry, out of focus, "
607
+ "surreal, ugly, beginner, duplicated furniture, "
608
+ "extra furniture, distorted furniture"
609
+ )
610
+ )
611
+
612
+ submit = gr.Button(
613
+ "Generate furnished room",
614
+ variant="primary"
615
+ )
616
+
617
+ # ----------------------------------------------------
618
+ # RIGHT
619
+ # ----------------------------------------------------
620
+
621
  with gr.Column():
622
+
623
+ design_image = gr.Image(
624
+ label="Result",
625
+ elem_id="img-display-output"
626
+ )
627
+
628
+ # ========================================================
629
+ # SUBMIT
630
+ # ========================================================
631
+
632
+ def on_submit(
633
+ room,
634
+ furniture,
635
+ text,
636
+ num_steps,
637
+ guidance_scale,
638
+ seed,
639
+ strength,
640
+ ip_scale,
641
+ a_prompt,
642
+ n_prompt,
643
+ img_size
644
+ ):
645
+
646
+ if room is None:
647
+ raise gr.Error(
648
+ "Please upload a photo of the room."
649
+ )
650
+
651
+ if furniture is None:
652
+ raise gr.Error(
653
+ "Please upload a photo of the furniture."
654
+ )
655
+
656
+ model.seed = int(seed)
657
+
658
  model.neg_prompt = n_prompt
659
+
660
  model.additional_quality_suffix = a_prompt
661
+
662
  with torch.no_grad():
663
+
664
+ out_img = model.generate_design(
665
+ room,
666
+ furniture,
667
+ text,
668
+ guidance_scale=guidance_scale,
669
+ num_steps=int(num_steps),
670
+ strength=strength,
671
+ img_size=int(img_size),
672
+ ip_scale=ip_scale
673
+ )
674
 
675
  return out_img
676
 
677
+ submit.click(
678
+ on_submit,
679
+
680
+ inputs=[
681
+ room_image,
682
+ furniture_image,
683
+ prompt,
684
+ num_steps,
685
+ guidance_scale,
686
+ seed,
687
+ strength,
688
+ ip_scale,
689
+ a_prompt,
690
+ n_prompt,
691
+ img_size
692
+ ],
693
+
694
+ outputs=design_image
695
+ )
696
 
697
 
698
+ # ============================================================
699
+ # LOAD MODELS
700
+ # ============================================================
701
+
702
+ controlnet_depth = ControlNetModel.from_pretrained(
703
+ "controlnet_depth",
704
+ torch_dtype=dtype,
705
+ use_safetensors=True
706
+ )
707
+
708
  controlnet_seg = ControlNetModel.from_pretrained(
709
+ "own_controlnet",
710
+ torch_dtype=dtype,
711
+ use_safetensors=True
712
+ )
713
+
714
 
715
  pipe = StableDiffusionControlNetInpaintPipeline.from_pretrained(
716
  "SG161222/Realistic_Vision_V5.1_noVAE",
717
+
718
+ controlnet=[
719
+ controlnet_depth,
720
+ controlnet_seg
721
+ ],
722
+
723
  safety_checker=None,
724
+
725
  torch_dtype=dtype
726
  )
727
 
728
+
729
+ # ============================================================
730
+ # IP-ADAPTER
731
+ # ============================================================
732
+
733
+ pipe.load_ip_adapter(
734
+ "h94/IP-Adapter",
735
+ subfolder="models",
736
+ weight_name="ip-adapter_sd15.bin"
737
+ )
738
+
739
+ pipe.set_ip_adapter_scale(0.75)
740
+
741
+ pipe.scheduler = UniPCMultistepScheduler.from_config(
742
+ pipe.scheduler.config
743
+ )
744
+
745
  pipe = pipe.to(device)
746
+
747
+
748
+ # ============================================================
749
+ # VISION MODELS
750
+ # ============================================================
751
+
752
+ seg_image_processor, image_segmentor = (
753
+ get_segmentation_pipeline()
754
+ )
755
+
756
+ image_segmentor = image_segmentor.to(device)
757
+
758
+ depth_feature_extractor, depth_estimator = (
759
+ get_depth_pipeline()
760
+ )
761
+
762
  depth_estimator = depth_estimator.to(device)
763
 
764
 
765
+ # ============================================================
766
+ # MAIN
767
+ # ============================================================
768
+
769
  def main():
770
+
771
  model = ControlNetDepthDesignModelMulti()
772
+
773
+ print(
774
+ "StableDesign Furniture models loaded successfully."
775
+ )
776
+
777
+ title = "# StableDesign Furniture"
778
+
779
  description = """
780
+ <p>
781
+ Upload a photo of an empty room and a photo of a furniture item.
782
+ The furniture image is used as a visual reference and the room
783
+ geometry is preserved using depth and semantic segmentation.
784
+ </p>
785
  """
786
+
787
+ with gr.Blocks(
788
+ css=css
789
+ ) as demo:
790
+
791
  gr.Markdown(title)
792
+
793
  gr.Markdown(description)
794
+
795
  create_demo(model)
 
 
796
 
797
+ demo.queue().launch(
798
+ share=False
799
+ )
800
 
801
 
802
+ if __name__ == "__main__":
803
  main()