Madiy commited on
Commit
f3edaa2
Β·
verified Β·
1 Parent(s): ec9ef77

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +207 -125
app.py CHANGED
@@ -9,8 +9,8 @@ import sys
9
  from PIL import Image, ImageFilter, ImageDraw
10
  from diffusers import StableDiffusionInpaintPipeline
11
  from transformers import BlipProcessor, BlipForConditionalGeneration
12
- from diffusers import StableDiffusionImg2ImgPipeline
13
-
14
  import torchvision.transforms.functional as F
15
  sys.modules["torchvision.transforms.functional_tensor"] = F
16
 
@@ -53,18 +53,21 @@ print("Real-ESRGAN ready.")
53
  print("Loading SD Inpainting...")
54
 
55
  inpaint = StableDiffusionInpaintPipeline.from_pretrained(
56
- "runwayml/stable-diffusion-inpainting",
57
  torch_dtype=torch.float16,
 
58
  )
59
  print("SD Inpainting ready.")
60
 
61
- # COLORING- IMG2IMG Pipeline
62
- colour= StableDiffusionImg2ImgPipeline.from_pretrained(
63
- "runwayml/stable-diffusion-v1-5",
64
- safety_checker=None,
65
- requires_safety_checker=False,
 
66
  )
67
- print("colour model ready!")
 
68
 
69
 
70
  # BLIP
@@ -103,7 +106,7 @@ def get_caption(image):
103
  caption = blip_processor.decode(output[0], skip_special_tokens=True)
104
  return caption
105
 
106
- def build_prompt(caption, image):
107
  bw = is_greyscale(image)
108
  style_hint = (
109
  "black and white photography, monochrome, greyscale, "
@@ -117,9 +120,105 @@ def build_prompt(caption, image):
117
  f"same atmosphere, high quality"
118
  )
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  def extend_one_side(image, direction, pixels, prompt, negative_prompt):
121
- orig_w = image.width
122
- orig_h = image.height
 
 
 
 
 
 
123
 
124
  new_w = orig_w
125
  new_h = orig_h
@@ -129,19 +228,24 @@ def extend_one_side(image, direction, pixels, prompt, negative_prompt):
129
  if direction == "left":
130
  new_w = orig_w + pixels
131
  paste_x = pixels
 
132
  elif direction == "right":
133
  new_w = orig_w + pixels
134
  paste_x = 0
 
135
  elif direction == "top":
136
  new_h = orig_h + pixels
137
  paste_y = pixels
 
138
  elif direction == "bottom":
139
  new_h = orig_h + pixels
140
  paste_y = 0
141
 
 
142
  new_w = (new_w // 8) * 8
143
  new_h = (new_h // 8) * 8
144
 
 
145
  if direction in ["left", "right"]:
146
  pixels = new_w - orig_w
147
  else:
@@ -152,27 +256,54 @@ def extend_one_side(image, direction, pixels, prompt, negative_prompt):
152
  if direction == "top":
153
  paste_y = pixels
154
 
 
155
  canvas = Image.new("RGB", (new_w, new_h), (0, 0, 0))
156
  canvas.paste(image, (paste_x, paste_y))
157
 
 
158
  mask = Image.new("L", (new_w, new_h), 255)
 
159
  draw = ImageDraw.Draw(mask)
 
160
  feather = 30
 
 
 
161
 
162
  if direction == "left":
163
- draw.rectangle([paste_x + feather, feather, new_w - feather, new_h - feather], fill=0)
 
 
 
 
164
  elif direction == "right":
165
- draw.rectangle([feather, feather, orig_w - feather, new_h - feather], fill=0)
166
- elif direction == "top":
167
- draw.rectangle([feather, paste_y + feather, new_w - feather, new_h - feather], fill=0)
168
- elif direction == "bottom":
169
- draw.rectangle([feather, feather, new_w - feather, orig_h - feather], fill=0)
170
 
171
- mask = mask.filter(ImageFilter.GaussianBlur(radius=30))
 
 
 
 
172
 
173
- sd_size = 512
 
 
 
 
 
 
 
 
 
 
 
 
174
  canvas_sd = canvas.resize((sd_size, sd_size), Image.LANCZOS)
175
  mask_sd = mask.resize((sd_size, sd_size), Image.LANCZOS)
 
176
 
177
  result = inpaint(
178
  prompt = prompt,
@@ -185,132 +316,83 @@ def extend_one_side(image, direction, pixels, prompt, negative_prompt):
185
  negative_prompt = negative_prompt,
186
  )
187
 
188
- generated_512 = result.images[0]
 
189
  generated_full = generated_512.resize((new_w, new_h), Image.LANCZOS)
190
- # No hard paste β€” GaussianBlur mask handles the boundary softly
191
  return generated_full
192
 
193
- @spaces.GPU
194
- def enhance_image(image, scale_factor):
195
- if image is None:
196
- raise gr.Error("Please upload an image first.")
197
-
198
- # Move models to GPU β€” happens inside the decorated function
199
- # because GPU is only available here
200
- enhancer.device = torch.device("cuda")
201
- enhancer.half = True
202
-
203
- image_array = np.array(image)
204
- image_array = image_array[:, :, :3]
205
- outscale = 4 if scale_factor == "4x" else 2
206
-
207
- try:
208
- output_array, _ = enhancer.enhance(image_array, outscale=outscale)
209
- except RuntimeError as e:
210
- raise gr.Error(f"Enhancement failed: {e}. Try a smaller image.")
211
-
212
- output_rgb = output_array[:, :, ::-1]
213
- output_image = Image.fromarray(output_rgb)
214
-
215
- original_size = f"{image.width}Γ—{image.height}"
216
- new_size = f"{output_image.width}Γ—{output_image.height}"
217
-
218
- return output_image, f"Original: {original_size} β†’ Enhanced: {new_size}"
219
-
220
- @spaces.GPU
221
- def colour_image(image, prompt, strength):
222
- colour.to("cuda", torch.float16)
223
- if Image is None:
224
- raise gr.Error("Please Upload the Image")
225
- # Convert to RGB
226
- image= image.convert("RGB")
227
- # Resize to 512 for SD
228
- target= 512
229
- ratio= min(target/ image.width, target/image.height)
230
- image= image.resize(
231
- (int(image.width * ratio), int(image.height * ratio)),
232
- Image.LANCZOS
233
- )
234
-
235
- #Build Prompt
236
- if not prompt.strip():
237
- # auto generate with BLIP
238
- caption= get_caption(image)
239
- full_prompt= (
240
- f"colorized photograph, {caption}, "
241
- f"natural realistic colors, vivid, sharp, "
242
- f"professional color grading, high quality"
243
- )
244
- else:
245
- full_prompt= (
246
- f"colorized photograph, {prompt}, "
247
- f"natural realistic colors, high quality"
248
- )
249
-
250
- negative_prompt= (
251
- "black and white, greyscale, monochrome, "
252
- "blurry, bad quality, oversaturated, unnatural colors"
253
- )
254
-
255
- output= colour(
256
- prompt= full_prompt,
257
- image= image,
258
- strength= float(strength),
259
- negative_prompt= negative_prompt,
260
- num_inference_steps= 30,
261
- guidance_scale= 7.5,
262
- )
263
-
264
- result= output.images[0]
265
- return result, f"Prompt used: \n {full_prompt}"
266
-
267
-
268
  @spaces.GPU
269
  def outpaint_image(image, direction, extend_percent, custom_prompt, progress=gr.Progress()):
 
270
  if image is None:
271
  raise gr.Error("Please upload an image first.")
272
-
273
- # Move models to GPU inside the decorated function
274
  inpaint.to("cuda")
275
  blip_model.to("cuda")
276
 
277
- target_side = 512
278
- ratio = min(target_side / image.width, target_side / image.height)
279
- new_size = (int(image.width * ratio), int(image.height * ratio))
280
- image = image.resize(new_size, Image.LANCZOS)
 
 
281
 
282
  progress(0.05, desc="Analyzing image with BLIP...")
283
 
284
- blip_caption = custom_prompt.strip() if custom_prompt.strip() else get_caption(image)
285
- prompt = build_prompt(blip_caption, image)
 
 
 
 
286
 
287
  negative_prompt = (
288
  "blurry, bad quality, watermark, text, "
289
- "new person, new face, extra people, "
290
  "colorful, vibrant colors, color photography, "
291
- "duplicate, border, frame, seam, visible edge, "
292
- "distorted, inconsistent style"
 
293
  )
294
 
 
295
  STEP_PX = 64
296
- extend = extend_percent / 100.0
297
- h_total = int(image.width * extend)
298
- v_total = int(image.height * extend)
 
 
 
 
 
 
 
 
299
 
300
  def make_passes(side, total_px):
301
- passes = []
 
 
302
  remaining = total_px
303
  while remaining > 0:
304
  step = min(STEP_PX, remaining)
 
305
  passes.append((side, step))
306
  remaining -= step
307
  return passes
 
 
308
 
 
309
  if direction == "Horizontal":
310
  passes = make_passes("right", h_total) + make_passes("left", h_total)
 
 
311
  elif direction == "Vertical":
312
  passes = make_passes("bottom", v_total) + make_passes("top", v_total)
 
 
313
  else:
 
314
  passes = (
315
  make_passes("bottom", v_total) +
316
  make_passes("top", v_total) +
@@ -322,20 +404,24 @@ def outpaint_image(image, direction, extend_percent, custom_prompt, progress=gr.
322
  current_image = image
323
 
324
  for i, (side, px) in enumerate(passes):
325
- progress(
326
- 0.1 + 0.85 * (i / total_passes),
327
- desc=f"Pass {i+1}/{total_passes} β€” extending {side} by {px}px"
328
- )
329
  current_image = extend_one_side(
330
- current_image, side, px, prompt, negative_prompt
 
 
 
 
331
  )
 
 
332
 
333
  progress(1.0, desc="Done!")
334
 
335
- bw_note = " [B&W detected]" if is_greyscale(image) else ""
336
  return (
337
  current_image,
338
- f"Caption{bw_note}:\n{blip_caption}\n\nPrompt:\n{prompt}"
339
  )
340
 
341
  # GRADIO UI
@@ -379,9 +465,6 @@ with gr.Blocks(title="CanvasAI β€” Enhance & Outpaint") as demo:
379
  label="Upload Image",
380
  type="pil",
381
  )
382
- col_prompt= gr.Textbox(label= "Custom Prompt(optional)",
383
- placeholder="Leave empty for auto-detection, or type: portrait of a young woman in 1960s clothing",
384
- lines=2)
385
  col_strength= gr.Slider(
386
  minimum= 0.3,
387
  maximum= 0.7,
@@ -396,11 +479,10 @@ with gr.Blocks(title="CanvasAI β€” Enhance & Outpaint") as demo:
396
  )
397
  with gr.Column():
398
  col_output= gr.Image(label="Colorized Result", type="pil", interactive=False)
399
- col_caption = gr.Textbox(label="Prompt Used", interactive=False, lines=3)
400
  col_btn.click(
401
  fn= colour_image,
402
- inputs= [col_input, col_prompt, col_strength],
403
- outputs= [col_output, col_caption]
404
  )
405
 
406
  with gr.Tab(" Outpaint"):
 
9
  from PIL import Image, ImageFilter, ImageDraw
10
  from diffusers import StableDiffusionInpaintPipeline
11
  from transformers import BlipProcessor, BlipForConditionalGeneration
12
+ from modelscope.pipelines import pipeline
13
+ from modelscope.utils.constant import Tasks
14
  import torchvision.transforms.functional as F
15
  sys.modules["torchvision.transforms.functional_tensor"] = F
16
 
 
53
  print("Loading SD Inpainting...")
54
 
55
  inpaint = StableDiffusionInpaintPipeline.from_pretrained(
56
+ "stabilityai/stable-diffusion-2-inpainting",
57
  torch_dtype=torch.float16,
58
+ variant= "fp16"
59
  )
60
  print("SD Inpainting ready.")
61
 
62
+ # COLORING
63
+ colorizer = pipeline(
64
+ Tasks.image_colorization,
65
+ model="damo/cv_ddcolor_image-colorization"
66
+ # DDColor = dedicated colorization model
67
+ # Trained specifically to add color without changing structure
68
  )
69
+
70
+ print("Colorizer ready.")
71
 
72
 
73
  # BLIP
 
106
  caption = blip_processor.decode(output[0], skip_special_tokens=True)
107
  return caption
108
 
109
+ def build_prompt(caption, image): #for outpaint
110
  bw = is_greyscale(image)
111
  style_hint = (
112
  "black and white photography, monochrome, greyscale, "
 
120
  f"same atmosphere, high quality"
121
  )
122
 
123
+
124
+
125
+ @spaces.GPU
126
+ def enhance_image(image, scale_factor):
127
+ if image is None:
128
+ raise gr.Error("Please upload an image first.")
129
+
130
+ # Move models to GPU β€” happens inside the decorated function
131
+ # because GPU is only available here
132
+ enhancer.device = torch.device("cuda")
133
+ enhancer.half = True
134
+
135
+ image_array = np.array(image)
136
+ image_array = image_array[:, :, :3]
137
+ outscale = 4 if scale_factor == "4x" else 2
138
+
139
+ try:
140
+ output_array, _ = enhancer.enhance(image_array, outscale=outscale)
141
+ except RuntimeError as e:
142
+ raise gr.Error(f"Enhancement failed: {e}. Try a smaller image.")
143
+
144
+ output_rgb = output_array[:, :, ::-1]
145
+ output_image = Image.fromarray(output_rgb)
146
+
147
+ original_size = f"{image.width}Γ—{image.height}"
148
+ new_size = f"{output_image.width}Γ—{output_image.height}"
149
+
150
+ return output_image, f"Original: {original_size} β†’ Enhanced: {new_size}"
151
+
152
+ @spaces.GPU
153
+ def colour_image(image, strength):
154
+
155
+ if Image is None:
156
+ raise gr.Error("Please Upload the Image")
157
+
158
+ # Convert PIL to numpy RGB
159
+ img_array = image.convert("RGB")
160
+ # Resize so longest side = 512
161
+ # DDColor works best at this resolution
162
+ target = 512
163
+ ratio = min(target / image.width, target / image.height)
164
+ w = int(image.width * ratio)
165
+ h = int(image.height * ratio)
166
+ image = image.resize((w, h), Image.LANCZOS)
167
+ # LANCZOS = high quality resampling, preserves sharp edges
168
+ # DDColor expects BGR (OpenCV format)
169
+ # Convert PIL β†’ numpy BGR for DDColor
170
+ img_rgb = np.array(image)
171
+ # np.array(PIL) = numpy array shape (h, w, 3) in RGB
172
+ img_bgr = img_rgb[:, :, ::-1]
173
+
174
+ #run colorizer
175
+ result= colorizer(img_bgr)
176
+ output_bgr= result["output_img"]
177
+
178
+ # Convert BGR β†’ RGB for PIL
179
+ output_rgb = output_bgr[:, :, ::-1]
180
+
181
+ # Apply strength blending
182
+ if strength < 1.0:
183
+ grey = np.array(image.convert("L"))
184
+ # convert("L") = convert to greyscale (single channel)
185
+ grey_3ch = np.stack([grey, grey, grey], axis=-1)
186
+ # stack same channel 3 times β†’ (h, w, 3) array
187
+ # needed to match output_rgb shape for blending
188
+ output_rgb = (
189
+ strength * output_rgb.astype(float) +
190
+ (1 - strength) * grey_3ch.astype(float)
191
+ ).astype(np.uint8)
192
+ # Linear blend: weighted average of colored and grey
193
+ # .astype(np.uint8) = convert back to 0-255 integers
194
+ return Image.fromarray(output_rgb)
195
+
196
+
197
+ def get_caption(image):
198
+ inputs = blip_processor(image.convert("RGB"), return_tensors="pt").to("cuda", torch.float16)
199
+ output = blip_model.generate(**inputs, max_new_tokens=60)
200
+ caption = blip_processor.decode(output[0], skip_special_tokens=True)
201
+ return caption
202
+
203
+
204
+ def build_prompt(caption):
205
+ return (
206
+ f"seamless continuation of a scene with {caption}, "
207
+ f"same exact lighting, same exact style, "
208
+ f"same background, extending the existing scene naturally, "
209
+ f"no new objects, no new subjects, only environment continuation"
210
+ )
211
+
212
+
213
  def extend_one_side(image, direction, pixels, prompt, negative_prompt):
214
+ # CORE LOGIC β€” extends one side by a SMALL number of pixels
215
+ # Called multiple times in small steps instead of one big jump
216
+ # Small steps = SD always has lots of context = coherent output
217
+
218
+ from PIL import ImageDraw
219
+
220
+ orig_w = image.width
221
+ orig_h = image.height
222
 
223
  new_w = orig_w
224
  new_h = orig_h
 
228
  if direction == "left":
229
  new_w = orig_w + pixels
230
  paste_x = pixels
231
+
232
  elif direction == "right":
233
  new_w = orig_w + pixels
234
  paste_x = 0
235
+
236
  elif direction == "top":
237
  new_h = orig_h + pixels
238
  paste_y = pixels
239
+
240
  elif direction == "bottom":
241
  new_h = orig_h + pixels
242
  paste_y = 0
243
 
244
+ # Round to multiple of 8 β€” SD requirement
245
  new_w = (new_w // 8) * 8
246
  new_h = (new_h // 8) * 8
247
 
248
+ # Recalculate after rounding
249
  if direction in ["left", "right"]:
250
  pixels = new_w - orig_w
251
  else:
 
256
  if direction == "top":
257
  paste_y = pixels
258
 
259
+ # Build black canvas and paste original into it
260
  canvas = Image.new("RGB", (new_w, new_h), (0, 0, 0))
261
  canvas.paste(image, (paste_x, paste_y))
262
 
263
+ # ── Build mask ────────────────────────────────────────────
264
  mask = Image.new("L", (new_w, new_h), 255)
265
+ # All white = generate everywhere
266
  draw = ImageDraw.Draw(mask)
267
+
268
  feather = 30
269
+ # This ensures the black region doesn't touch the very edge of original
270
+ # leaving a thin white strip that GaussianBlur will turn into a gradient
271
+
272
 
273
  if direction == "left":
274
+ draw.rectangle([
275
+ paste_x + feather, feather,
276
+ new_w - feather, new_h - feather
277
+ ], fill=0)
278
+
279
  elif direction == "right":
280
+ draw.rectangle([
281
+ feather, feather,
282
+ orig_w - feather, new_h - feather
283
+ ], fill=0)
 
284
 
285
+ elif direction == "top":
286
+ draw.rectangle([
287
+ feather, paste_y + feather,
288
+ new_w - feather, new_h - feather
289
+ ], fill=0)
290
 
291
+ elif direction == "bottom":
292
+ draw.rectangle([
293
+ feather, feather,
294
+ new_w - feather, orig_h - feather
295
+ ], fill=0)
296
+
297
+
298
+ #Apply GaussianBlur to the mask AFTER drawing
299
+ # This is what creates REAL feathering β€” a soft gradient at the boundary
300
+ mask= mask.filter(ImageFilter.GaussianBlur(radius=30))
301
+
302
+
303
+ sd_size = 768
304
  canvas_sd = canvas.resize((sd_size, sd_size), Image.LANCZOS)
305
  mask_sd = mask.resize((sd_size, sd_size), Image.LANCZOS)
306
+ # LANCZOS = high quality downsampling, preserves sharp details
307
 
308
  result = inpaint(
309
  prompt = prompt,
 
316
  negative_prompt = negative_prompt,
317
  )
318
 
319
+ generated_512 = result.images[0]
320
+
321
  generated_full = generated_512.resize((new_w, new_h), Image.LANCZOS)
 
322
  return generated_full
323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  @spaces.GPU
325
  def outpaint_image(image, direction, extend_percent, custom_prompt, progress=gr.Progress()):
326
+
327
  if image is None:
328
  raise gr.Error("Please upload an image first.")
 
 
329
  inpaint.to("cuda")
330
  blip_model.to("cuda")
331
 
332
+ # Resize input to max 512 on longest side
333
+ max_side = 512
334
+ ratio = min(max_side / image.width, max_side / image.height)
335
+
336
+ new_size = (int(image.width * ratio), int(image.height * ratio))
337
+ image = image.resize(new_size, Image.LANCZOS)
338
 
339
  progress(0.05, desc="Analyzing image with BLIP...")
340
 
341
+ if custom_prompt.strip():
342
+ blip_caption = custom_prompt.strip()
343
+ else:
344
+ blip_caption = get_caption(image)
345
+
346
+ prompt = build_prompt(blip_caption)
347
 
348
  negative_prompt = (
349
  "blurry, bad quality, watermark, text, "
350
+ "new person, new face, new subject, extra people, "
351
  "colorful, vibrant colors, color photography, "
352
+ "duplicate, tiled, repeated pattern, border, frame, "
353
+ "seam, visible edge, abrupt change, inconsistent, "
354
+ "distorted, unnatural, different style, different era"
355
  )
356
 
357
+ # ── KEY CHANGE: small fixed step size, multiple passes ────
358
  STEP_PX = 64
359
+ # Add only 64 pixels per pass instead of the full extension at once
360
+ # 64px out of a 512px image = 12.5% of canvas = SD has 87.5% context
361
+ # Compare to before: 35% extension = SD only had 65% context
362
+ # More context = SD understands the scene = coherent generation
363
+
364
+ # Calculate total pixels needed per direction
365
+ extend = extend_percent / 100.0
366
+ h_total = int(image.width * extend)
367
+ # Total horizontal pixels to add on each side
368
+ v_total = int(image.height * extend)
369
+ # Total vertical pixels to add on each side
370
 
371
  def make_passes(side, total_px):
372
+ # Break total_px into multiple STEP_PX passes
373
+ # Example: total=180px, STEP_PX=64 β†’ passes of [64, 64, 52]
374
+ passes = []
375
  remaining = total_px
376
  while remaining > 0:
377
  step = min(STEP_PX, remaining)
378
+ # min() = don't overshoot β€” last step may be smaller
379
  passes.append((side, step))
380
  remaining -= step
381
  return passes
382
+ # Returns list of (direction, pixels) tuples
383
+ # Each tuple = one call to extend_one_side
384
 
385
+ # Build full pass list based on chosen direction
386
  if direction == "Horizontal":
387
  passes = make_passes("right", h_total) + make_passes("left", h_total)
388
+ # Extend right in small steps, then left in small steps
389
+
390
  elif direction == "Vertical":
391
  passes = make_passes("bottom", v_total) + make_passes("top", v_total)
392
+ # Bottom first (more grounded content), then top
393
+
394
  else:
395
+ # Both β€” all four sides in small steps
396
  passes = (
397
  make_passes("bottom", v_total) +
398
  make_passes("top", v_total) +
 
404
  current_image = image
405
 
406
  for i, (side, px) in enumerate(passes):
407
+ progress_val = 0.1 + 0.85 * (i / total_passes)
408
+ progress(progress_val, desc=f"Pass {i+1}/{total_passes} β€” extending {side} by {px}px")
409
+
 
410
  current_image = extend_one_side(
411
+ current_image,
412
+ side,
413
+ px,
414
+ prompt,
415
+ negative_prompt
416
  )
417
+ # Each pass returns a slightly larger image
418
+ # Next pass uses that as input β€” builds on previous result
419
 
420
  progress(1.0, desc="Done!")
421
 
 
422
  return (
423
  current_image,
424
+ f"BLIP caption:\n{blip_caption}\n\nFull prompt:\n{prompt}"
425
  )
426
 
427
  # GRADIO UI
 
465
  label="Upload Image",
466
  type="pil",
467
  )
 
 
 
468
  col_strength= gr.Slider(
469
  minimum= 0.3,
470
  maximum= 0.7,
 
479
  )
480
  with gr.Column():
481
  col_output= gr.Image(label="Colorized Result", type="pil", interactive=False)
 
482
  col_btn.click(
483
  fn= colour_image,
484
+ inputs= [col_input, col_strength],
485
+ outputs= [col_output]
486
  )
487
 
488
  with gr.Tab(" Outpaint"):