Spaces:
Paused
Paused
sachin
commited on
Commit
·
a14b836
1
Parent(s):
57cd301
add- fit to image
Browse files
runway.py
CHANGED
@@ -24,7 +24,7 @@ async def root():
|
|
24 |
"""
|
25 |
Root endpoint for basic health check.
|
26 |
"""
|
27 |
-
return {"message": "InstructPix2Pix API is running. Use POST /inpaint
|
28 |
|
29 |
def prepare_guided_image(original_image: Image, reference_image: Image, mask_image: Image) -> Image:
|
30 |
"""
|
@@ -40,30 +40,16 @@ def prepare_guided_image(original_image: Image, reference_image: Image, mask_ima
|
|
40 |
Returns:
|
41 |
Image: The blended image to guide inpainting.
|
42 |
"""
|
43 |
-
# Convert images to numpy arrays
|
44 |
original_array = np.array(original_image)
|
45 |
reference_array = np.array(reference_image)
|
46 |
-
mask_array = np.array(mask_image) / 255.0
|
47 |
-
|
48 |
-
# Expand mask to RGB channels
|
49 |
mask_array = mask_array[:, :, np.newaxis]
|
50 |
-
|
51 |
-
# Blend: use original where mask=0 (black), reference where mask=1 (white)
|
52 |
blended_array = original_array * (1 - mask_array) + reference_array * mask_array
|
53 |
-
|
54 |
-
|
55 |
-
return Image.fromarray(blended_array)
|
56 |
|
57 |
def soften_mask(mask_image: Image, softness: int = 5) -> Image:
|
58 |
"""
|
59 |
Soften the edges of the mask for smoother transitions.
|
60 |
-
|
61 |
-
Args:
|
62 |
-
mask_image (Image): The original mask (grayscale, L mode).
|
63 |
-
softness (int): Size of the Gaussian blur kernel for softening edges.
|
64 |
-
|
65 |
-
Returns:
|
66 |
-
Image: The softened mask.
|
67 |
"""
|
68 |
from PIL import ImageFilter
|
69 |
return mask_image.filter(ImageFilter.GaussianBlur(radius=softness))
|
@@ -72,24 +58,59 @@ def generate_rectangular_mask(image_size: tuple, x1: int = 100, y1: int = 100, x
|
|
72 |
"""
|
73 |
Generate a rectangular mask matching the image dimensions.
|
74 |
- Black (0) for areas to keep, white (255) for areas to inpaint.
|
75 |
-
|
76 |
-
Args:
|
77 |
-
image_size (tuple): Tuple of (width, height) of the original image.
|
78 |
-
x1, y1 (int): Top-left corner coordinates of the rectangle.
|
79 |
-
x2, y2 (int): Bottom-right corner coordinates of the rectangle.
|
80 |
-
|
81 |
-
Returns:
|
82 |
-
Image: The generated mask in grayscale (L mode).
|
83 |
"""
|
84 |
-
# Create a blank black mask (0 = keep)
|
85 |
mask = Image.new("L", image_size, 0)
|
86 |
draw = ImageDraw.Draw(mask)
|
87 |
-
|
88 |
-
# Draw a white rectangle (255 = inpaint)
|
89 |
draw.rectangle([x1, y1, x2, y2], fill=255)
|
90 |
-
|
91 |
return mask
|
92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
@app.post("/inpaint/")
|
94 |
async def inpaint_image(
|
95 |
image: UploadFile = File(...),
|
@@ -101,36 +122,20 @@ async def inpaint_image(
|
|
101 |
):
|
102 |
"""
|
103 |
Endpoint for image inpainting using a text prompt and autogenerated mask.
|
104 |
-
- `image`: Original image file (PNG/JPG).
|
105 |
-
- `prompt`: Text prompt describing the desired output.
|
106 |
-
- `mask_x1, mask_y1, mask_x2, mask_y2`: Coordinates for the rectangular mask (default: 100,100 to 200,200).
|
107 |
-
|
108 |
-
Returns:
|
109 |
-
- The inpainted image as a PNG file.
|
110 |
"""
|
111 |
try:
|
112 |
-
# Load the uploaded image
|
113 |
image_bytes = await image.read()
|
114 |
original_image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
115 |
-
|
116 |
-
# Generate the mask based on image dimensions and provided coordinates
|
117 |
mask_image = generate_rectangular_mask(original_image.size, mask_x1, mask_y1, mask_x2, mask_y2)
|
118 |
-
|
119 |
-
# Perform inpainting using the pipeline
|
120 |
result = pipe(prompt=prompt, image=original_image, mask_image=mask_image).images[0]
|
121 |
-
|
122 |
-
# Convert result to bytes for response
|
123 |
result_bytes = io.BytesIO()
|
124 |
result.save(result_bytes, format="PNG")
|
125 |
result_bytes.seek(0)
|
126 |
-
|
127 |
-
# Return the image as a streaming response
|
128 |
return StreamingResponse(
|
129 |
result_bytes,
|
130 |
media_type="image/png",
|
131 |
headers={"Content-Disposition": "attachment; filename=inpainted_image.png"}
|
132 |
)
|
133 |
-
|
134 |
except Exception as e:
|
135 |
raise HTTPException(status_code=500, detail=f"Error during inpainting: {e}")
|
136 |
|
@@ -145,9 +150,53 @@ async def inpaint_with_reference(
|
|
145 |
mask_y2: int = 200
|
146 |
):
|
147 |
"""
|
148 |
-
Endpoint for replacing masked areas with reference image content, refined to look natural
|
149 |
-
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
- `prompt`: Text prompt for inpainting refinement.
|
152 |
- `mask_x1, mask_y1, mask_x2, mask_y2`: Coordinates for the rectangular mask (default: 100,100 to 200,200).
|
153 |
|
@@ -155,49 +204,38 @@ async def inpaint_with_reference(
|
|
155 |
- The resulting image as a PNG file.
|
156 |
"""
|
157 |
try:
|
158 |
-
# Load the uploaded
|
159 |
image_bytes = await image.read()
|
160 |
reference_bytes = await reference_image.read()
|
161 |
-
|
162 |
original_image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
163 |
reference_image = Image.open(io.BytesIO(reference_bytes)).convert("RGB")
|
164 |
|
165 |
-
#
|
166 |
-
|
167 |
-
reference_image = reference_image.resize(original_image.size, Image.Resampling.LANCZOS)
|
168 |
-
|
169 |
-
# Generate the mask based on image dimensions and provided coordinates
|
170 |
-
mask_image = generate_rectangular_mask(original_image.size, mask_x1, mask_y1, mask_x2, mask_y2)
|
171 |
|
172 |
# Soften the mask for smoother transitions
|
173 |
softened_mask = soften_mask(mask_image, softness=5)
|
174 |
|
175 |
-
#
|
176 |
-
guided_image = prepare_guided_image(original_image, reference_image, softened_mask)
|
177 |
-
|
178 |
-
# Perform inpainting to refine the result and make it look natural
|
179 |
result = pipe(
|
180 |
prompt=prompt,
|
181 |
image=guided_image,
|
182 |
-
mask_image=softened_mask,
|
183 |
-
strength=0.75,
|
184 |
-
guidance_scale=7.5
|
185 |
).images[0]
|
186 |
|
187 |
# Convert result to bytes for response
|
188 |
result_bytes = io.BytesIO()
|
189 |
result.save(result_bytes, format="PNG")
|
190 |
result_bytes.seek(0)
|
191 |
-
|
192 |
-
# Return the image as a streaming response
|
193 |
return StreamingResponse(
|
194 |
result_bytes,
|
195 |
media_type="image/png",
|
196 |
-
headers={"Content-Disposition": "attachment; filename=
|
197 |
)
|
198 |
-
|
199 |
except Exception as e:
|
200 |
-
raise HTTPException(status_code=500, detail=f"Error during
|
201 |
|
202 |
if __name__ == "__main__":
|
203 |
import uvicorn
|
|
|
24 |
"""
|
25 |
Root endpoint for basic health check.
|
26 |
"""
|
27 |
+
return {"message": "InstructPix2Pix API is running. Use POST /inpaint/, /inpaint-with-reference/, or /fit-image-to-mask/ to edit images."}
|
28 |
|
29 |
def prepare_guided_image(original_image: Image, reference_image: Image, mask_image: Image) -> Image:
|
30 |
"""
|
|
|
40 |
Returns:
|
41 |
Image: The blended image to guide inpainting.
|
42 |
"""
|
|
|
43 |
original_array = np.array(original_image)
|
44 |
reference_array = np.array(reference_image)
|
45 |
+
mask_array = np.array(mask_image) / 255.0
|
|
|
|
|
46 |
mask_array = mask_array[:, :, np.newaxis]
|
|
|
|
|
47 |
blended_array = original_array * (1 - mask_array) + reference_array * mask_array
|
48 |
+
return Image.fromarray(blended_array.astype(np.uint8))
|
|
|
|
|
49 |
|
50 |
def soften_mask(mask_image: Image, softness: int = 5) -> Image:
|
51 |
"""
|
52 |
Soften the edges of the mask for smoother transitions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
"""
|
54 |
from PIL import ImageFilter
|
55 |
return mask_image.filter(ImageFilter.GaussianBlur(radius=softness))
|
|
|
58 |
"""
|
59 |
Generate a rectangular mask matching the image dimensions.
|
60 |
- Black (0) for areas to keep, white (255) for areas to inpaint.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
"""
|
|
|
62 |
mask = Image.new("L", image_size, 0)
|
63 |
draw = ImageDraw.Draw(mask)
|
|
|
|
|
64 |
draw.rectangle([x1, y1, x2, y2], fill=255)
|
|
|
65 |
return mask
|
66 |
|
67 |
+
def fit_image_to_mask(original_image: Image, reference_image: Image, mask_x1: int, mask_y1: int, mask_x2: int, mask_y2: int) -> tuple:
|
68 |
+
"""
|
69 |
+
Fit the reference image into the masked region of the original image.
|
70 |
+
|
71 |
+
Args:
|
72 |
+
original_image (Image): The original image (RGB).
|
73 |
+
reference_image (Image): The image to fit into the masked region (RGB).
|
74 |
+
mask_x1, mask_y1, mask_x2, mask_y2 (int): Coordinates of the masked region.
|
75 |
+
|
76 |
+
Returns:
|
77 |
+
tuple: (guided_image, mask_image) - The image with the fitted reference and the corresponding mask.
|
78 |
+
"""
|
79 |
+
# Calculate mask dimensions
|
80 |
+
mask_width = mask_x2 - mask_x1
|
81 |
+
mask_height = mask_y2 - mask_y1
|
82 |
+
|
83 |
+
# Resize reference image to fit the mask while preserving aspect ratio
|
84 |
+
ref_width, ref_height = reference_image.size
|
85 |
+
aspect_ratio = ref_width / ref_height
|
86 |
+
|
87 |
+
if mask_width / mask_height > aspect_ratio:
|
88 |
+
# Fit to height
|
89 |
+
new_height = mask_height
|
90 |
+
new_width = int(new_height * aspect_ratio)
|
91 |
+
else:
|
92 |
+
# Fit to width
|
93 |
+
new_width = mask_width
|
94 |
+
new_height = int(new_width / aspect_ratio)
|
95 |
+
|
96 |
+
# Resize reference image
|
97 |
+
reference_image_resized = reference_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
98 |
+
|
99 |
+
# Create a copy of the original image to paste the reference image onto
|
100 |
+
guided_image = original_image.copy()
|
101 |
+
|
102 |
+
# Calculate position to center the resized image in the mask
|
103 |
+
paste_x = mask_x1 + (mask_width - new_width) // 2
|
104 |
+
paste_y = mask_y1 + (mask_height - new_height) // 2
|
105 |
+
|
106 |
+
# Paste the resized reference image onto the original image
|
107 |
+
guided_image.paste(reference_image_resized, (paste_x, paste_y))
|
108 |
+
|
109 |
+
# Generate the mask for inpainting (white in the pasted region)
|
110 |
+
mask_image = generate_rectangular_mask(original_image.size, mask_x1, mask_y1, mask_x2, mask_y2)
|
111 |
+
|
112 |
+
return guided_image, mask_image
|
113 |
+
|
114 |
@app.post("/inpaint/")
|
115 |
async def inpaint_image(
|
116 |
image: UploadFile = File(...),
|
|
|
122 |
):
|
123 |
"""
|
124 |
Endpoint for image inpainting using a text prompt and autogenerated mask.
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
"""
|
126 |
try:
|
|
|
127 |
image_bytes = await image.read()
|
128 |
original_image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
|
|
|
|
129 |
mask_image = generate_rectangular_mask(original_image.size, mask_x1, mask_y1, mask_x2, mask_y2)
|
|
|
|
|
130 |
result = pipe(prompt=prompt, image=original_image, mask_image=mask_image).images[0]
|
|
|
|
|
131 |
result_bytes = io.BytesIO()
|
132 |
result.save(result_bytes, format="PNG")
|
133 |
result_bytes.seek(0)
|
|
|
|
|
134 |
return StreamingResponse(
|
135 |
result_bytes,
|
136 |
media_type="image/png",
|
137 |
headers={"Content-Disposition": "attachment; filename=inpainted_image.png"}
|
138 |
)
|
|
|
139 |
except Exception as e:
|
140 |
raise HTTPException(status_code=500, detail=f"Error during inpainting: {e}")
|
141 |
|
|
|
150 |
mask_y2: int = 200
|
151 |
):
|
152 |
"""
|
153 |
+
Endpoint for replacing masked areas with reference image content, refined to look natural.
|
154 |
+
"""
|
155 |
+
try:
|
156 |
+
image_bytes = await image.read()
|
157 |
+
reference_bytes = await reference_image.read()
|
158 |
+
original_image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
159 |
+
reference_image = Image.open(io.BytesIO(reference_bytes)).convert("RGB")
|
160 |
+
|
161 |
+
if original_image.size != reference_image.size:
|
162 |
+
reference_image = reference_image.resize(original_image.size, Image.Resampling.LANCZOS)
|
163 |
+
|
164 |
+
mask_image = generate_rectangular_mask(original_image.size, mask_x1, mask_y1, mask_x2, mask_y2)
|
165 |
+
softened_mask = soften_mask(mask_image, softness=5)
|
166 |
+
guided_image = prepare_guided_image(original_image, reference_image, softened_mask)
|
167 |
+
result = pipe(
|
168 |
+
prompt=prompt,
|
169 |
+
image=guided_image,
|
170 |
+
mask_image=softened_mask,
|
171 |
+
strength=0.75,
|
172 |
+
guidance_scale=7.5
|
173 |
+
).images[0]
|
174 |
+
|
175 |
+
result_bytes = io.BytesIO()
|
176 |
+
result.save(result_bytes, format="PNG")
|
177 |
+
result_bytes.seek(0)
|
178 |
+
return StreamingResponse(
|
179 |
+
result_bytes,
|
180 |
+
media_type="image/png",
|
181 |
+
headers={"Content-Disposition": "attachment; filename=natural_inpaint_image.png"}
|
182 |
+
)
|
183 |
+
except Exception as e:
|
184 |
+
raise HTTPException(status_code=500, detail=f"Error during natural inpainting: {e}")
|
185 |
+
|
186 |
+
@app.post("/fit-image-to-mask/")
|
187 |
+
async def fit_image_to_mask(
|
188 |
+
image: UploadFile = File(...),
|
189 |
+
reference_image: UploadFile = File(...),
|
190 |
+
prompt: str = "Blend the fitted image naturally into the scene, matching style and lighting.",
|
191 |
+
mask_x1: int = 100,
|
192 |
+
mask_y1: int = 100,
|
193 |
+
mask_x2: int = 200,
|
194 |
+
mask_y2: int = 200
|
195 |
+
):
|
196 |
+
"""
|
197 |
+
Endpoint for fitting a reference image into a masked region of the original image, refined to look natural.
|
198 |
+
- `image`: Original image file (PNG/JPG), e.g., a table.
|
199 |
+
- `reference_image`: Image to fit into the masked region (PNG/JPG), e.g., a cat.
|
200 |
- `prompt`: Text prompt for inpainting refinement.
|
201 |
- `mask_x1, mask_y1, mask_x2, mask_y2`: Coordinates for the rectangular mask (default: 100,100 to 200,200).
|
202 |
|
|
|
204 |
- The resulting image as a PNG file.
|
205 |
"""
|
206 |
try:
|
207 |
+
# Load the uploaded images
|
208 |
image_bytes = await image.read()
|
209 |
reference_bytes = await reference_image.read()
|
|
|
210 |
original_image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
211 |
reference_image = Image.open(io.BytesIO(reference_bytes)).convert("RGB")
|
212 |
|
213 |
+
# Fit the reference image into the masked region
|
214 |
+
guided_image, mask_image = fit_image_to_mask(original_image, reference_image, mask_x1, mask_y1, mask_x2, mask_y2)
|
|
|
|
|
|
|
|
|
215 |
|
216 |
# Soften the mask for smoother transitions
|
217 |
softened_mask = soften_mask(mask_image, softness=5)
|
218 |
|
219 |
+
# Perform inpainting to blend the fitted image naturally
|
|
|
|
|
|
|
220 |
result = pipe(
|
221 |
prompt=prompt,
|
222 |
image=guided_image,
|
223 |
+
mask_image=softened_mask,
|
224 |
+
strength=0.75,
|
225 |
+
guidance_scale=7.5
|
226 |
).images[0]
|
227 |
|
228 |
# Convert result to bytes for response
|
229 |
result_bytes = io.BytesIO()
|
230 |
result.save(result_bytes, format="PNG")
|
231 |
result_bytes.seek(0)
|
|
|
|
|
232 |
return StreamingResponse(
|
233 |
result_bytes,
|
234 |
media_type="image/png",
|
235 |
+
headers={"Content-Disposition": "attachment; filename=fitted_image.png"}
|
236 |
)
|
|
|
237 |
except Exception as e:
|
238 |
+
raise HTTPException(status_code=500, detail=f"Error during fitting and inpainting: {e}")
|
239 |
|
240 |
if __name__ == "__main__":
|
241 |
import uvicorn
|