Spaces:
Paused
Paused
| from fastapi import FastAPI, UploadFile, File | |
| from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from PIL import Image | |
| from simple_lama_inpainting import SimpleLama | |
| import io | |
| import numpy as np | |
| import cv2 | |
| import uvicorn | |
| app = FastAPI(title="LaMa Object Remover - Clean Removal") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Initialize LaMa with optimal settings | |
| lama = SimpleLama() | |
| def preprocess_mask(mask_pil, target_size=None): | |
| """Preprocess mask for better inpainting results""" | |
| # Convert to numpy array | |
| mask_np = np.array(mask_pil) | |
| # Ensure binary mask | |
| mask_np = (mask_np > 128).astype(np.uint8) * 255 | |
| # Apply morphological operations to clean mask | |
| kernel = np.ones((3,3), np.uint8) | |
| mask_np = cv2.morphologyEx(mask_np, cv2.MORPH_CLOSE, kernel) | |
| mask_np = cv2.morphologyEx(mask_np, cv2.MORPH_OPEN, kernel) | |
| # Dilate mask slightly for better edge detection | |
| kernel = np.ones((5,5), np.uint8) | |
| mask_np = cv2.dilate(mask_np, kernel, iterations=1) | |
| # Apply Gaussian blur to mask edges for smoother transitions | |
| mask_np = cv2.GaussianBlur(mask_np, (5,5), 0) | |
| mask_np = (mask_np > 128).astype(np.uint8) * 255 | |
| # Ensure same dimensions as target if specified | |
| if target_size: | |
| mask_pil_resized = Image.fromarray(mask_np) | |
| mask_pil_resized = mask_pil_resized.resize(target_size, Image.Resampling.LANCZOS) | |
| mask_np = np.array(mask_pil_resized) | |
| return Image.fromarray(mask_np) | |
| def resize_to_match(img1, img2): | |
| """Resize images to match dimensions""" | |
| # Get dimensions | |
| w1, h1 = img1.size | |
| w2, h2 = img2.size | |
| # If dimensions don't match, resize to smaller dimensions | |
| if w1 != w2 or h1 != h2: | |
| target_width = min(w1, w2) | |
| target_height = min(h1, h2) | |
| # Resize both images to same dimensions | |
| img1 = img1.resize((target_width, target_height), Image.Resampling.LANCZOS) | |
| img2 = img2.resize((target_width, target_height), Image.Resampling.LANCZOS) | |
| return img1, img2 | |
| def postprocess_result(result_pil, original_pil, mask_pil): | |
| """Post-process result for cleaner output""" | |
| # Ensure dimensions match | |
| result_pil, original_pil = resize_to_match(result_pil, original_pil) | |
| result_np = np.array(result_pil) | |
| original_np = np.array(original_pil) | |
| mask_np = np.array(mask_pil.convert('L')) | |
| # Ensure mask dimensions match | |
| if mask_np.shape[:2] != result_np.shape[:2]: | |
| mask_pil_resized = Image.fromarray(mask_np) | |
| mask_pil_resized = mask_pil_resized.resize((result_np.shape[1], result_np.shape[0]), Image.Resampling.LANCZOS) | |
| mask_np = np.array(mask_pil_resized) | |
| # Create a blending mask for edges | |
| mask_blur = cv2.GaussianBlur(mask_np, (15,15), 0) | |
| mask_blur = mask_blur / 255.0 | |
| # Extend mask to 3 channels | |
| if len(mask_blur.shape) == 2: | |
| mask_3ch = np.stack([mask_blur, mask_blur, mask_blur], axis=2) | |
| else: | |
| mask_3ch = mask_blur | |
| # Blend result with original at edges for smoother transitions | |
| blended = (result_np * mask_3ch + original_np * (1 - mask_3ch)).astype(np.uint8) | |
| return Image.fromarray(blended) | |
| HTML = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | |
| <title>LaMa Object Remover - Clean Removal</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .header p { | |
| font-size: 1.1em; | |
| opacity: 0.95; | |
| } | |
| .main-content { | |
| padding: 30px; | |
| } | |
| .image-section { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| .canvas-container { | |
| background: #f5f5f5; | |
| border-radius: 10px; | |
| padding: 20px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| canvas { | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 8px; | |
| cursor: crosshair; | |
| display: block; | |
| margin: 0 auto; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
| } | |
| .tools { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| margin-bottom: 20px; | |
| } | |
| .tool-group { | |
| margin-bottom: 20px; | |
| } | |
| .tool-group label { | |
| display: block; | |
| margin-bottom: 10px; | |
| font-weight: 600; | |
| color: #555; | |
| } | |
| .brush-controls { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| input[type="range"] { | |
| flex: 1; | |
| min-width: 200px; | |
| } | |
| .size-value { | |
| background: #667eea; | |
| color: white; | |
| padding: 5px 15px; | |
| border-radius: 5px; | |
| font-weight: bold; | |
| } | |
| .mode-buttons { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .mode-btn { | |
| padding: 10px 20px; | |
| border: 2px solid #ddd; | |
| background: white; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s; | |
| } | |
| .mode-btn.active { | |
| background: #667eea; | |
| color: white; | |
| border-color: #667eea; | |
| } | |
| .mode-btn.eraser.active { | |
| background: #f56565; | |
| border-color: #f56565; | |
| } | |
| button { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin: 5px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .btn-danger { | |
| background: #f56565; | |
| color: white; | |
| } | |
| .btn-danger:hover { | |
| background: #e53e3e; | |
| transform: translateY(-2px); | |
| } | |
| .btn-success { | |
| background: #48bb78; | |
| color: white; | |
| } | |
| .btn-success:hover { | |
| background: #38a169; | |
| transform: translateY(-2px); | |
| } | |
| .btn-warning { | |
| background: #ed8936; | |
| color: white; | |
| } | |
| .btn-warning:hover { | |
| background: #dd6b20; | |
| transform: translateY(-2px); | |
| } | |
| .action-buttons { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| margin-top: 20px; | |
| justify-content: center; | |
| } | |
| .status { | |
| margin-top: 20px; | |
| padding: 15px; | |
| border-radius: 10px; | |
| text-align: center; | |
| animation: fadeIn 0.3s; | |
| } | |
| .status.success { | |
| background: #c6f6d5; | |
| color: #22543d; | |
| border-left: 4px solid #48bb78; | |
| } | |
| .status.error { | |
| background: #fed7d7; | |
| color: #c53030; | |
| border-left: 4px solid #f56565; | |
| } | |
| .status.info { | |
| background: #bee3f8; | |
| color: #2c5282; | |
| border-left: 4px solid #4299e1; | |
| } | |
| .result-section { | |
| margin-top: 30px; | |
| padding: 20px; | |
| background: #f8f9fa; | |
| border-radius: 10px; | |
| display: none; | |
| } | |
| .result-section.show { | |
| display: block; | |
| animation: slideUp 0.5s; | |
| } | |
| .result-image { | |
| text-align: center; | |
| margin-top: 15px; | |
| } | |
| .result-image img { | |
| max-width: 100%; | |
| border-radius: 10px; | |
| box-shadow: 0 5px 20px rgba(0,0,0,0.2); | |
| } | |
| .loading { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.9); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| flex-direction: column; | |
| } | |
| .loading.show { | |
| display: flex; | |
| } | |
| .spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 5px solid #f3f3f3; | |
| border-top: 5px solid #667eea; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| .loading p { | |
| color: white; | |
| margin-top: 20px; | |
| font-size: 1.1em; | |
| } | |
| .shortcuts { | |
| display: flex; | |
| gap: 15px; | |
| justify-content: center; | |
| flex-wrap: wrap; | |
| margin-top: 15px; | |
| padding-top: 15px; | |
| border-top: 1px solid #e0e0e0; | |
| font-size: 12px; | |
| color: #666; | |
| } | |
| .shortcut { | |
| background: #e2e8f0; | |
| padding: 4px 8px; | |
| border-radius: 5px; | |
| font-family: monospace; | |
| font-weight: bold; | |
| } | |
| .info-box { | |
| background: #e2e8f0; | |
| padding: 10px; | |
| border-radius: 8px; | |
| margin-top: 10px; | |
| font-size: 14px; | |
| text-align: center; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(-10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes slideUp { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @media (max-width: 768px) { | |
| .header h1 { | |
| font-size: 1.8em; | |
| } | |
| .brush-controls { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🎨 LaMa Object Remover Pro</h1> | |
| <p>Clean removal - No blur, No mess - Professional results</p> | |
| </div> | |
| <div class="main-content"> | |
| <div class="image-section"> | |
| <div class="canvas-container"> | |
| <canvas id="imageCanvas" width="800" height="600"></canvas> | |
| </div> | |
| </div> | |
| <div class="tools"> | |
| <div class="tool-group"> | |
| <label>🖌️ Brush Settings</label> | |
| <div class="brush-controls"> | |
| <span>Brush Size:</span> | |
| <input type="range" id="brushSize" min="5" max="50" value="20"> | |
| <span id="brushSizeDisplay" class="size-value">20px</span> | |
| </div> | |
| </div> | |
| <div class="tool-group"> | |
| <label>🎨 Tools</label> | |
| <div class="mode-buttons"> | |
| <button id="brushMode" class="mode-btn active">✏️ Brush (Mark to Remove)</button> | |
| <button id="eraserMode" class="mode-btn eraser">🗑️ Eraser (Remove Marks)</button> | |
| <button id="clearMarksBtn" class="mode-btn">🧹 Clear All Marks</button> | |
| </div> | |
| </div> | |
| <div class="action-buttons"> | |
| <button id="uploadBtn" class="btn-success">📁 Upload Image</button> | |
| <button id="removeBtn" class="btn-primary">✨ Remove Marked Objects</button> | |
| <button id="resetBtn" class="btn-warning">🔄 Reset Image</button> | |
| </div> | |
| <div class="info-box"> | |
| 💡 <strong>For best results:</strong> Mark the ENTIRE object you want to remove. Include a small border around the object for cleaner edges. The AI will intelligently fill the marked area. | |
| </div> | |
| <div class="shortcuts"> | |
| <span>⌨️ Shortcuts:</span> | |
| <span><span class="shortcut">B</span> Brush</span> | |
| <span><span class="shortcut">E</span> Eraser</span> | |
| <span><span class="shortcut">C</span> Clear All</span> | |
| <span><span class="shortcut">R</span> Remove</span> | |
| <span><span class="shortcut">[</span> <span class="shortcut">]</span> Brush Size</span> | |
| <span><span class="shortcut">Ctrl+Z</span> Undo</span> | |
| </div> | |
| </div> | |
| <div class="result-section" id="resultSection"> | |
| <h3 style="margin-bottom: 15px;">✨ Clean Result - Objects Removed</h3> | |
| <div class="result-image"> | |
| <img id="resultImage" alt="Result"> | |
| <div style="margin-top: 15px;"> | |
| <button id="downloadBtn" class="btn-success">💾 Download Result</button> | |
| <button id="newEditBtn" class="btn-primary">✏️ Edit Another Image</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="status" class="status info"> | |
| 💡 Ready - Draw RED marks on objects you want to remove completely | |
| </div> | |
| </div> | |
| </div> | |
| <div id="loading" class="loading"> | |
| <div class="spinner"></div> | |
| <p>🤖 AI is removing objects with LaMa inpainting...</p> | |
| <p style="font-size: 14px; margin-top: 10px;">This may take 5-10 seconds for best quality</p> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('imageCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let isDrawing = false; | |
| let currentMode = 'brush'; | |
| let brushSize = 20; | |
| let lastX = 0, lastY = 0; | |
| let originalImageData = null; | |
| let currentImageData = null; | |
| let maskLayer = null; | |
| let history = []; | |
| let canvasWidth = 800; | |
| let canvasHeight = 600; | |
| function initCanvas(width, height) { | |
| canvasWidth = width; | |
| canvasHeight = height; | |
| canvas.width = width; | |
| canvas.height = height; | |
| maskLayer = document.createElement('canvas'); | |
| maskLayer.width = width; | |
| maskLayer.height = height; | |
| const maskCtx = maskLayer.getContext('2d'); | |
| maskCtx.clearRect(0, 0, width, height); | |
| ctx.lineCap = 'round'; | |
| ctx.lineJoin = 'round'; | |
| } | |
| function saveState() { | |
| const maskCtx = maskLayer.getContext('2d'); | |
| const imageData = maskCtx.getImageData(0, 0, canvasWidth, canvasHeight); | |
| history.push(imageData); | |
| if (history.length > 20) history.shift(); | |
| } | |
| function undo() { | |
| if (history.length > 0) { | |
| const lastState = history.pop(); | |
| const maskCtx = maskLayer.getContext('2d'); | |
| maskCtx.putImageData(lastState, 0, 0); | |
| redrawImageWithMask(); | |
| updateStatus('Undo successful', 'success'); | |
| } else { | |
| updateStatus('Nothing to undo', 'info'); | |
| } | |
| } | |
| function redrawImageWithMask() { | |
| if (currentImageData) { | |
| const img = new Image(); | |
| img.onload = () => { | |
| ctx.clearRect(0, 0, canvasWidth, canvasHeight); | |
| ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); | |
| const maskCtx = maskLayer.getContext('2d'); | |
| const maskData = maskCtx.getImageData(0, 0, canvasWidth, canvasHeight); | |
| for (let i = 0; i < maskData.data.length; i += 4) { | |
| if (maskData.data[i] > 200 && maskData.data[i+3] > 0) { | |
| ctx.save(); | |
| ctx.globalAlpha = 0.6; | |
| ctx.fillStyle = 'rgba(255, 0, 0, 0.6)'; | |
| ctx.fillRect((i/4) % canvasWidth, Math.floor((i/4) / canvasWidth), 1, 1); | |
| ctx.restore(); | |
| } | |
| } | |
| }; | |
| img.src = currentImageData; | |
| } | |
| } | |
| function setupDrawing() { | |
| canvas.addEventListener('mousedown', startDrawing); | |
| canvas.addEventListener('mousemove', draw); | |
| canvas.addEventListener('mouseup', stopDrawing); | |
| canvas.addEventListener('mouseleave', stopDrawing); | |
| canvas.addEventListener('touchstart', handleTouchStart); | |
| canvas.addEventListener('touchmove', handleTouchMove); | |
| canvas.addEventListener('touchend', stopDrawing); | |
| canvas.addEventListener('touchcancel', stopDrawing); | |
| } | |
| function handleTouchStart(e) { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| const mouseEvent = new MouseEvent('mousedown', { | |
| clientX: touch.clientX, | |
| clientY: touch.clientY | |
| }); | |
| canvas.dispatchEvent(mouseEvent); | |
| } | |
| function handleTouchMove(e) { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| const mouseEvent = new MouseEvent('mousemove', { | |
| clientX: touch.clientX, | |
| clientY: touch.clientY | |
| }); | |
| canvas.dispatchEvent(mouseEvent); | |
| } | |
| function getCanvasCoordinates(e) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = canvas.width / rect.width; | |
| const scaleY = canvas.height / rect.height; | |
| let clientX, clientY; | |
| if (e.touches) { | |
| clientX = e.touches[0].clientX; | |
| clientY = e.touches[0].clientY; | |
| } else { | |
| clientX = e.clientX; | |
| clientY = e.clientY; | |
| } | |
| let x = (clientX - rect.left) * scaleX; | |
| let y = (clientY - rect.top) * scaleY; | |
| x = Math.max(0, Math.min(canvas.width, x)); | |
| y = Math.max(0, Math.min(canvas.height, y)); | |
| return { x, y }; | |
| } | |
| function startDrawing(e) { | |
| e.preventDefault(); | |
| isDrawing = true; | |
| saveState(); | |
| const pos = getCanvasCoordinates(e); | |
| lastX = pos.x; | |
| lastY = pos.y; | |
| const maskCtx = maskLayer.getContext('2d'); | |
| maskCtx.save(); | |
| if (currentMode === 'eraser') { | |
| maskCtx.globalCompositeOperation = 'destination-out'; | |
| maskCtx.strokeStyle = 'rgba(0,0,0,1)'; | |
| } else { | |
| maskCtx.globalCompositeOperation = 'source-over'; | |
| maskCtx.strokeStyle = 'rgba(255,0,0,1)'; | |
| } | |
| maskCtx.lineWidth = brushSize; | |
| maskCtx.beginPath(); | |
| maskCtx.moveTo(lastX, lastY); | |
| maskCtx.lineTo(lastX + 0.1, lastY + 0.1); | |
| maskCtx.stroke(); | |
| maskCtx.restore(); | |
| redrawImageWithMask(); | |
| } | |
| function draw(e) { | |
| if (!isDrawing) return; | |
| e.preventDefault(); | |
| const pos = getCanvasCoordinates(e); | |
| const maskCtx = maskLayer.getContext('2d'); | |
| maskCtx.save(); | |
| if (currentMode === 'eraser') { | |
| maskCtx.globalCompositeOperation = 'destination-out'; | |
| maskCtx.strokeStyle = 'rgba(0,0,0,1)'; | |
| } else { | |
| maskCtx.globalCompositeOperation = 'source-over'; | |
| maskCtx.strokeStyle = 'rgba(255,0,0,1)'; | |
| } | |
| maskCtx.lineWidth = brushSize; | |
| maskCtx.lineCap = 'round'; | |
| maskCtx.lineJoin = 'round'; | |
| maskCtx.beginPath(); | |
| maskCtx.moveTo(lastX, lastY); | |
| maskCtx.lineTo(pos.x, pos.y); | |
| maskCtx.stroke(); | |
| lastX = pos.x; | |
| lastY = pos.y; | |
| maskCtx.restore(); | |
| redrawImageWithMask(); | |
| } | |
| function stopDrawing() { | |
| isDrawing = false; | |
| } | |
| const brushSizeSlider = document.getElementById('brushSize'); | |
| const brushSizeDisplay = document.getElementById('brushSizeDisplay'); | |
| brushSizeSlider.addEventListener('input', (e) => { | |
| brushSize = parseInt(e.target.value); | |
| brushSizeDisplay.textContent = brushSize + 'px'; | |
| }); | |
| const brushModeBtn = document.getElementById('brushMode'); | |
| const eraserModeBtn = document.getElementById('eraserMode'); | |
| brushModeBtn.addEventListener('click', () => { | |
| currentMode = 'brush'; | |
| brushModeBtn.classList.add('active'); | |
| eraserModeBtn.classList.remove('active'); | |
| updateStatus('Brush mode - Draw RED to mark objects for removal', 'info'); | |
| }); | |
| eraserModeBtn.addEventListener('click', () => { | |
| currentMode = 'eraser'; | |
| eraserModeBtn.classList.add('active'); | |
| brushModeBtn.classList.remove('active'); | |
| updateStatus('Eraser mode - Remove marks', 'info'); | |
| }); | |
| document.getElementById('clearMarksBtn').addEventListener('click', () => { | |
| const maskCtx = maskLayer.getContext('2d'); | |
| maskCtx.clearRect(0, 0, canvasWidth, canvasHeight); | |
| redrawImageWithMask(); | |
| updateStatus('All marks cleared!', 'success'); | |
| }); | |
| document.getElementById('resetBtn').addEventListener('click', () => { | |
| if (originalImageData) { | |
| currentImageData = originalImageData; | |
| const maskCtx = maskLayer.getContext('2d'); | |
| maskCtx.clearRect(0, 0, canvasWidth, canvasHeight); | |
| const img = new Image(); | |
| img.onload = () => { | |
| ctx.clearRect(0, 0, canvasWidth, canvasHeight); | |
| ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); | |
| document.getElementById('resultSection').classList.remove('show'); | |
| updateStatus('Image reset!', 'success'); | |
| }; | |
| img.src = originalImageData; | |
| } | |
| }); | |
| document.getElementById('uploadBtn').addEventListener('click', () => { | |
| const input = document.createElement('input'); | |
| input.type = 'file'; | |
| input.accept = 'image/*'; | |
| input.onchange = (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| let width = img.width; | |
| let height = img.height; | |
| const maxWidth = 800; | |
| const maxHeight = 600; | |
| if (width > maxWidth) { | |
| height = (height * maxWidth) / width; | |
| width = maxWidth; | |
| } | |
| if (height > maxHeight) { | |
| width = (width * maxHeight) / height; | |
| height = maxHeight; | |
| } | |
| width = Math.floor(width); | |
| height = Math.floor(height); | |
| initCanvas(width, height); | |
| ctx.drawImage(img, 0, 0, width, height); | |
| originalImageData = event.target.result; | |
| currentImageData = event.target.result; | |
| const maskCtx = maskLayer.getContext('2d'); | |
| maskCtx.clearRect(0, 0, width, height); | |
| document.getElementById('resultSection').classList.remove('show'); | |
| updateStatus('Image loaded! Draw RED marks on objects you want to remove', 'success'); | |
| }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }; | |
| input.click(); | |
| }); | |
| document.getElementById('removeBtn').addEventListener('click', async () => { | |
| if (!currentImageData) { | |
| updateStatus('Please upload an image first!', 'error'); | |
| return; | |
| } | |
| const maskCtx = maskLayer.getContext('2d'); | |
| const maskData = maskCtx.getImageData(0, 0, canvasWidth, canvasHeight); | |
| let hasMarks = false; | |
| for (let i = 0; i < maskData.data.length; i += 4) { | |
| if (maskData.data[i] > 200 && maskData.data[i+3] > 0) { | |
| hasMarks = true; | |
| break; | |
| } | |
| } | |
| if (!hasMarks) { | |
| updateStatus('Please draw RED marks on objects you want to remove first!', 'error'); | |
| return; | |
| } | |
| const binaryMaskCanvas = document.createElement('canvas'); | |
| binaryMaskCanvas.width = canvasWidth; | |
| binaryMaskCanvas.height = canvasHeight; | |
| const binaryCtx = binaryMaskCanvas.getContext('2d'); | |
| binaryCtx.drawImage(maskLayer, 0, 0); | |
| const imageData = binaryCtx.getImageData(0, 0, canvasWidth, canvasHeight); | |
| const data = imageData.data; | |
| for (let i = 0; i < data.length; i += 4) { | |
| if (data[i] > 200 && data[i+3] > 0) { | |
| data[i] = 255; | |
| data[i+1] = 255; | |
| data[i+2] = 255; | |
| data[i+3] = 255; | |
| } else { | |
| data[i] = 0; | |
| data[i+1] = 0; | |
| data[i+2] = 0; | |
| data[i+3] = 255; | |
| } | |
| } | |
| binaryCtx.putImageData(imageData, 0, 0); | |
| const originalBlob = await new Promise(resolve => { | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = canvasWidth; | |
| tempCanvas.height = canvasHeight; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| const img = new Image(); | |
| img.onload = () => { | |
| tempCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight); | |
| tempCanvas.toBlob(resolve, 'image/png'); | |
| }; | |
| img.src = currentImageData; | |
| }); | |
| const maskBlob = await new Promise(resolve => binaryMaskCanvas.toBlob(resolve, 'image/png')); | |
| const formData = new FormData(); | |
| formData.append('image', originalBlob, 'image.png'); | |
| formData.append('mask', maskBlob, 'mask.png'); | |
| document.getElementById('loading').classList.add('show'); | |
| updateStatus('🤖 AI is performing clean removal... This may take a few seconds', 'info'); | |
| try { | |
| const response = await fetch('/remove', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const url = URL.createObjectURL(blob); | |
| const resultImg = document.getElementById('resultImage'); | |
| resultImg.src = url; | |
| document.getElementById('resultSection').classList.add('show'); | |
| updateStatus('✅ Objects removed cleanly! Download to save', 'success'); | |
| document.getElementById('downloadBtn').onclick = () => { | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'removed_object_clean.png'; | |
| a.click(); | |
| updateStatus('Download started!', 'success'); | |
| }; | |
| document.getElementById('newEditBtn').onclick = () => { | |
| document.getElementById('resultSection').classList.remove('show'); | |
| document.getElementById('uploadBtn').click(); | |
| }; | |
| } else { | |
| const error = await response.json(); | |
| updateStatus('Error: ' + (error.error || 'Unknown error'), 'error'); | |
| } | |
| } catch (error) { | |
| updateStatus('Network error: ' + error.message, 'error'); | |
| } finally { | |
| document.getElementById('loading').classList.remove('show'); | |
| } | |
| }); | |
| function updateStatus(message, type) { | |
| const statusDiv = document.getElementById('status'); | |
| statusDiv.textContent = message; | |
| statusDiv.className = 'status ' + type; | |
| if (type !== 'error') { | |
| setTimeout(() => { | |
| if (statusDiv.className === 'status ' + type) { | |
| statusDiv.className = 'status info'; | |
| statusDiv.textContent = '💡 Ready - Draw RED marks on objects you want to remove completely'; | |
| } | |
| }, 3000); | |
| } | |
| } | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'b' || e.key === 'B') { | |
| brushModeBtn.click(); | |
| e.preventDefault(); | |
| } else if (e.key === 'e' || e.key === 'E') { | |
| eraserModeBtn.click(); | |
| e.preventDefault(); | |
| } else if (e.key === 'c' || e.key === 'C') { | |
| document.getElementById('clearMarksBtn').click(); | |
| e.preventDefault(); | |
| } else if (e.key === 'r' || e.key === 'R') { | |
| document.getElementById('removeBtn').click(); | |
| e.preventDefault(); | |
| } else if (e.key === ']') { | |
| brushSize = Math.min(50, brushSize + 2); | |
| brushSizeSlider.value = brushSize; | |
| brushSizeDisplay.textContent = brushSize + 'px'; | |
| e.preventDefault(); | |
| } else if (e.key === '[') { | |
| brushSize = Math.max(5, brushSize - 2); | |
| brushSizeSlider.value = brushSize; | |
| brushSizeDisplay.textContent = brushSize + 'px'; | |
| e.preventDefault(); | |
| } else if ((e.ctrlKey || e.metaKey) && e.key === 'z') { | |
| undo(); | |
| e.preventDefault(); | |
| } | |
| }); | |
| function loadDefaultImage() { | |
| const width = 800; | |
| const height = 600; | |
| initCanvas(width, height); | |
| const gradient = ctx.createLinearGradient(0, 0, width, height); | |
| gradient.addColorStop(0, '#667eea'); | |
| gradient.addColorStop(1, '#764ba2'); | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(0, 0, width, height); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 32px Segoe UI'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('📸 Click "Upload Image" to Start', width/2, height/2 - 50); | |
| ctx.font = '18px Segoe UI'; | |
| ctx.fillText('Then draw RED marks on objects you want to remove', width/2, height/2 + 20); | |
| ctx.fillText('The AI will remove them cleanly without blur', width/2, height/2 + 60); | |
| ctx.fillStyle = 'rgba(255,255,255,0.3)'; | |
| for(let i = 0; i < 5; i++) { | |
| ctx.beginPath(); | |
| ctx.arc(120 + i*140, height - 80, 35, 0, Math.PI*2); | |
| ctx.fill(); | |
| } | |
| originalImageData = canvas.toDataURL(); | |
| currentImageData = originalImageData; | |
| } | |
| setupDrawing(); | |
| loadDefaultImage(); | |
| updateStatus('🎨 Ready! Draw RED marks on objects you want to remove cleanly', 'success'); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| async def home(): | |
| return HTML | |
| async def remove_objects(image: UploadFile = File(...), mask: UploadFile = File(...)): | |
| """Remove objects with clean, non-blurry results""" | |
| try: | |
| # Read images | |
| image_pil = Image.open(image.file).convert("RGB") | |
| mask_pil = Image.open(mask.file).convert("L") | |
| # Get original dimensions | |
| original_size = image_pil.size | |
| # Preprocess mask to match original dimensions | |
| mask_pil = preprocess_mask(mask_pil, original_size) | |
| # Apply LaMa inpainting | |
| result = lama(image_pil, mask_pil) | |
| # Ensure result has same dimensions as original | |
| if result.size != original_size: | |
| result = result.resize(original_size, Image.Resampling.LANCZOS) | |
| # Post-process for cleaner edges (skip if dimensions don't match) | |
| try: | |
| result = postprocess_result(result, image_pil, mask_pil) | |
| except Exception as e: | |
| print(f"Post-processing skipped: {e}") | |
| # If post-processing fails, just return the raw result | |
| # Save to buffer with high quality | |
| buffer = io.BytesIO() | |
| result.save(buffer, format="PNG", optimize=True) | |
| buffer.seek(0) | |
| return StreamingResponse( | |
| buffer, | |
| media_type="image/png", | |
| headers={"Content-Disposition": "attachment; filename=cleaned_image.png"} | |
| ) | |
| except Exception as e: | |
| print(f"Error in removal: {str(e)}") | |
| return JSONResponse( | |
| status_code=500, | |
| content={"error": f"Failed to remove objects: {str(e)}"} | |
| ) | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) |