Spaces:
Build error
Build error
Initial Commit
Browse files- Dockerfile +29 -0
- README.md +5 -7
- main.py +114 -0
- requirements.txt +12 -0
Dockerfile
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Dockerfile
|
2 |
+
|
3 |
+
# Start with a base image that has Python and NVIDIA's CUDA drivers
|
4 |
+
FROM nvidia/cuda:12.1.1-devel-ubuntu22.04
|
5 |
+
|
6 |
+
# Set up the environment
|
7 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
8 |
+
ENV PYTHONUNBUFFERED=1
|
9 |
+
|
10 |
+
# Install Python and pip
|
11 |
+
RUN apt-get update && apt-get install -y \
|
12 |
+
python3.10 python3-pip \
|
13 |
+
&& rm -rf /var/lib/apt/lists/*
|
14 |
+
|
15 |
+
# Set the working directory inside the container
|
16 |
+
WORKDIR /app
|
17 |
+
|
18 |
+
# Copy and install Python dependencies
|
19 |
+
COPY requirements.txt .
|
20 |
+
RUN pip3 install --no-cache-dir -r requirements.txt
|
21 |
+
|
22 |
+
# Copy your main application code
|
23 |
+
COPY main.py .
|
24 |
+
|
25 |
+
# Expose the port your API will run on
|
26 |
+
EXPOSE 8000
|
27 |
+
|
28 |
+
# The command to start the Uvicorn server when the container runs
|
29 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
@@ -1,11 +1,9 @@
|
|
1 |
---
|
2 |
-
title: Virtual Staging
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
colorTo: blue
|
6 |
sdk: docker
|
7 |
-
|
8 |
-
|
9 |
---
|
10 |
-
|
11 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: Virtual Staging API
|
3 |
+
emoji: 🛋️
|
4 |
+
colorFrom: pink
|
5 |
colorTo: blue
|
6 |
sdk: docker
|
7 |
+
app_port: 8000
|
8 |
+
hardware: nvidia-t4-small
|
9 |
---
|
|
|
|
main.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# main.py
|
2 |
+
import torch
|
3 |
+
import numpy as np
|
4 |
+
import cv2
|
5 |
+
from PIL import Image
|
6 |
+
import base64
|
7 |
+
import io
|
8 |
+
from fastapi import FastAPI, HTTPException
|
9 |
+
from pydantic import BaseModel
|
10 |
+
from contextlib import asynccontextmanager
|
11 |
+
from transformers import pipeline
|
12 |
+
from diffusers import StableDiffusionControlNetInpaintPipeline, ControlNetModel, UniPCMultepScheduler
|
13 |
+
|
14 |
+
# --- API Data Models ---
|
15 |
+
class StagingRequest(BaseModel):
|
16 |
+
image_base64: str
|
17 |
+
prompt: str
|
18 |
+
negative_prompt: str = "blurry, low quality, unrealistic, distorted, ugly, watermark, text, messy, deformed, extra windows, extra doors"
|
19 |
+
seed: int = 1234
|
20 |
+
|
21 |
+
# --- Global State & Model Loading ---
|
22 |
+
models = {}
|
23 |
+
|
24 |
+
@asynccontextmanager
|
25 |
+
async def lifespan(app: FastAPI):
|
26 |
+
# STARTUP: Load all models
|
27 |
+
print("🚀 Server starting up: Loading AI models...")
|
28 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
29 |
+
torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
|
30 |
+
|
31 |
+
models['segmentation_pipeline'] = pipeline("image-segmentation", model="Intel/dpt-large-ade", device=device)
|
32 |
+
models['depth_estimator'] = pipeline("depth-estimation", model="Intel/dpt-hybrid-midas", device=device)
|
33 |
+
|
34 |
+
controlnet = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-depth", torch_dtype=torch_dtype)
|
35 |
+
|
36 |
+
models['inpainting_pipe'] = StableDiffusionControlNetInpaintPipeline.from_pretrained(
|
37 |
+
"runwayml/stable-diffusion-v1-5",
|
38 |
+
controlnet=controlnet,
|
39 |
+
torch_dtype=torch_dtype,
|
40 |
+
safety_checker=None
|
41 |
+
).to(device)
|
42 |
+
models['inpainting_pipe'].scheduler = UniPCMultistepScheduler.from_config(models['inpainting_pipe'].scheduler.config)
|
43 |
+
|
44 |
+
print("✅ All models loaded and ready.")
|
45 |
+
yield
|
46 |
+
# SHUTDOWN: Clean up
|
47 |
+
print("⚡ Server shutting down.")
|
48 |
+
models.clear()
|
49 |
+
|
50 |
+
app = FastAPI(lifespan=lifespan)
|
51 |
+
|
52 |
+
# --- Helper Functions (Core Logic) ---
|
53 |
+
def create_precise_mask(image_pil: Image.Image) -> Image.Image:
|
54 |
+
segments = models['segmentation_pipeline'](image_pil)
|
55 |
+
W, H = image_pil.size
|
56 |
+
inclusion_mask_np = np.zeros((H, W), dtype=np.uint8)
|
57 |
+
exclusion_mask_np = np.zeros((H, W), dtype=np.uint8)
|
58 |
+
inclusion_labels = {"wall", "floor", "ceiling"}
|
59 |
+
base_exclusion_labels = {"door", "window", "windowpane", "window blind"}
|
60 |
+
insert_labels = {"painting", "picture", "shelf", "showcase", "cabinet", "mirror", "television", "radiator"}
|
61 |
+
walls, inserts = [], []
|
62 |
+
for segment in segments:
|
63 |
+
label, mask = segment['label'], np.array(segment['mask'])
|
64 |
+
if label in inclusion_labels:
|
65 |
+
inclusion_mask_np = np.maximum(inclusion_mask_np, mask)
|
66 |
+
if label == "wall": walls.append(mask)
|
67 |
+
if label in base_exclusion_labels:
|
68 |
+
exclusion_mask_np = np.maximum(exclusion_mask_np, mask)
|
69 |
+
if label in insert_labels:
|
70 |
+
inserts.append(mask)
|
71 |
+
for insert_mask in inserts:
|
72 |
+
for wall_mask in walls:
|
73 |
+
if np.all((wall_mask >= insert_mask)[insert_mask > 0]):
|
74 |
+
exclusion_mask_np = np.maximum(exclusion_mask_np, insert_mask)
|
75 |
+
break
|
76 |
+
raw_mask_np = np.copy(inclusion_mask_np); raw_mask_np[exclusion_mask_np > 0] = 0
|
77 |
+
mask_filled_np = cv2.morphologyEx(raw_mask_np, cv2.MORPH_CLOSE, np.ones((10,10),np.uint8))
|
78 |
+
return Image.fromarray(mask_filled_np)
|
79 |
+
|
80 |
+
def generate_depth_map(image_pil: Image.Image) -> Image.Image:
|
81 |
+
predicted_depth = models['depth_estimator'](image_pil)['predicted_depth']
|
82 |
+
depth_map_np = predicted_depth.cpu().numpy()
|
83 |
+
depth_map_np = (depth_map_np - depth_map_np.min()) / (depth_map_np.max() - depth_map_np.min()) * 255.0
|
84 |
+
depth_map_np = depth_map_np.astype(np.uint8)
|
85 |
+
return Image.fromarray(np.concatenate([depth_map_np[..., None]] * 3, axis=-1))
|
86 |
+
|
87 |
+
# --- API Endpoints ---
|
88 |
+
@app.get("/")
|
89 |
+
def read_root():
|
90 |
+
return {"status": "Virtual Staging API is running."}
|
91 |
+
|
92 |
+
@app.post("/furnish-room/")
|
93 |
+
async def furnish_room(request: StagingRequest):
|
94 |
+
try:
|
95 |
+
image_bytes = base64.b64decode(request.image_base64)
|
96 |
+
init_image_pil = Image.open(io.BytesIO(image_bytes)).convert("RGB").resize((512, 512))
|
97 |
+
|
98 |
+
mask_image_pil = create_precise_mask(init_image_pil)
|
99 |
+
control_image_pil = generate_depth_map(init_image_pil)
|
100 |
+
|
101 |
+
generator = torch.Generator(device="cuda").manual_seed(request.seed)
|
102 |
+
final_image = models['inpainting_pipe'](
|
103 |
+
prompt=request.prompt, negative_prompt=request.negative_prompt, image=init_image_pil,
|
104 |
+
mask_image=mask_image_pil, control_image=control_image_pil,
|
105 |
+
num_inference_steps=30, guidance_scale=8.0, generator=generator,
|
106 |
+
).images[0]
|
107 |
+
|
108 |
+
buffered = io.BytesIO()
|
109 |
+
final_image.save(buffered, format="PNG")
|
110 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
111 |
+
|
112 |
+
return {"result_image_base64": img_str}
|
113 |
+
except Exception as e:
|
114 |
+
raise HTTPException(status_code=500, detail=str(e))
|
requirements.txt
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# requirements.txt
|
2 |
+
diffusers
|
3 |
+
transformers
|
4 |
+
accelerate
|
5 |
+
torch
|
6 |
+
matplotlib
|
7 |
+
timm
|
8 |
+
scipy
|
9 |
+
opencv-python-headless
|
10 |
+
fastapi
|
11 |
+
uvicorn[standard]
|
12 |
+
python-multipart
|