sachin commited on
Commit
a14b836
·
1 Parent(s): 57cd301

add- fit to image

Browse files
Files changed (1) hide show
  1. runway.py +106 -68
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/ or /inpaint-with-reference/ to edit images."}
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 # Normalize to [0, 1]
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
- blended_array = blended_array.astype(np.uint8)
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, using an autogenerated mask.
149
- - `image`: Original image file (PNG/JPG).
150
- - `reference_image`: Reference image to guide the replacement (PNG/JPG).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 image and reference image
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
- # Ensure reference image matches original image dimensions
166
- if original_image.size != reference_image.size:
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
- # Prepare the initial guided image by blending reference content into the masked area
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, # Use softened mask for inpainting
183
- strength=0.75, # Control how much inpainting modifies the image (0.0 to 1.0)
184
- guidance_scale=7.5 # Control how closely the prompt is followed
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=natural_inpaint_image.png"}
197
  )
198
-
199
  except Exception as e:
200
- raise HTTPException(status_code=500, detail=f"Error during natural inpainting: {e}")
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