editify-backend / inpaint.py
Nisha56's picture
auth, LaMa inpainting, PostgreSQL, UI polish, back button, export fix
e84e3d2
Raw
History Blame Contribute Delete
5.04 kB
import os
import torch
import numpy as np
from PIL import Image
from dotenv import load_dotenv
load_dotenv()
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
MODEL_ID = "stable-diffusion-v1-5/stable-diffusion-inpainting"
BACKEND_DIR = os.path.dirname(os.path.abspath(__file__))
print(f"[INPAINT] Using device: {DEVICE}")
inpaint_pipeline = None
def load_inpaint_model():
global inpaint_pipeline
if inpaint_pipeline is not None:
print("[INPAINT] Model already loaded")
return inpaint_pipeline
print("[INPAINT] Loading SD inpainting pipeline...")
from diffusers import StableDiffusionInpaintPipeline
inpaint_pipeline = StableDiffusionInpaintPipeline.from_pretrained(
MODEL_ID,
torch_dtype=torch.float16,
safety_checker=None,
requires_safety_checker=False,
)
inpaint_pipeline = inpaint_pipeline.to(DEVICE)
inpaint_pipeline.enable_attention_slicing()
print("[INPAINT] Model loaded successfully")
return inpaint_pipeline
def unload_inpaint_model():
global inpaint_pipeline
if inpaint_pipeline is not None:
del inpaint_pipeline
inpaint_pipeline = None
torch.cuda.empty_cache()
print("[INPAINT] Model unloaded, VRAM freed")
def inpaint_region(
original_image_path: str,
mask: np.ndarray,
prompt: str = "background, clean, seamless, no text, no objects",
negative_prompt: str = "text, logo, object, watermark, distorted",
num_steps: int = 20,
) -> Image.Image:
"""Fill masked region with AI-generated background. Mask: 255=fill, 0=keep."""
pipeline = load_inpaint_model()
original = Image.open(original_image_path).convert("RGB")
orig_w, orig_h = original.size
print(f"[INPAINT] Original size: {orig_w}x{orig_h}")
# SD requires 512x512 — resize in, resize result back out
sd_size = (512, 512)
img_resized = original.resize(sd_size, Image.LANCZOS)
mask_pil = Image.fromarray(mask.astype(np.uint8))
mask_resized = mask_pil.resize(sd_size, Image.NEAREST)
print(f"[INPAINT] Running {num_steps} inference steps...")
# run pipeline but get raw latents — bypasses the fp16 image processor bug on GTX 1650
with torch.no_grad():
output = pipeline(
prompt = prompt,
negative_prompt = negative_prompt,
image = img_resized,
mask_image = mask_resized,
num_inference_steps = num_steps,
guidance_scale = 7.5,
output_type = "pil",
)
result = output.images[0]
# check if result is black — if so, use original
result_arr = np.array(result)
if result_arr.mean() < 10:
print("[INPAINT] WARNING: Black output detected, applying fallback...")
# fallback: blur the boundary of original image to simulate inpainting
import cv2
orig_arr = np.array(img_resized)
mask_arr = np.array(mask_resized)
# use opencv inpainting as CPU fallback
mask_cv = (mask_arr > 127).astype(np.uint8) * 255
result_arr = cv2.inpaint(orig_arr, mask_cv, inpaintRadius=15, flags=cv2.INPAINT_TELEA)
result = Image.fromarray(result_arr)
print("[INPAINT] Fallback inpainting applied")
result_fullsize = result.resize((orig_w, orig_h), Image.LANCZOS)
print("[INPAINT] Inpainting complete")
return result_fullsize
def create_mask_from_bbox(image_path: str, x: int, y: int, w: int, h: int) -> np.ndarray:
"""Create binary mask from bounding box. Used when user deletes a layer."""
original = Image.open(image_path)
img_w, img_h = original.size
mask = np.zeros((img_h, img_w), dtype=np.uint8)
mask[y:y+h, x:x+w] = 255
return mask
def save_inpaint_result(result_image: Image.Image, output_dir: str = None) -> str:
if output_dir is None:
output_dir = os.path.join(BACKEND_DIR, "outputs")
os.makedirs(output_dir, exist_ok=True)
out_path = os.path.join(output_dir, "inpaint_result.jpg")
result_image.save(out_path, quality=95)
print(f"[INPAINT] Saved → {out_path}")
return out_path
if __name__ == "__main__":
TEST_IMAGE = os.path.join(BACKEND_DIR, "test_images", "sale_img.jpg")
if not os.path.exists(TEST_IMAGE):
print(f"ERROR: No image at {TEST_IMAGE}")
exit(1)
print("=" * 50)
print("Testing Stable Diffusion Inpainting")
print("=" * 50)
original = Image.open(TEST_IMAGE)
img_w, img_h = original.size
test_mask = create_mask_from_bbox(TEST_IMAGE, x=630, y=46, w=410, h=573)
mask_path = os.path.join(BACKEND_DIR, "outputs", "test_mask.jpg")
Image.fromarray(test_mask).save(mask_path)
print(f"[INPAINT] Mask saved → {mask_path}")
result = inpaint_region(TEST_IMAGE, test_mask)
save_inpaint_result(result)
unload_inpaint_model()
print("=" * 50)
print("DONE. Open outputs/inpaint_result.jpg")
print("=" * 50)