Object-Remover / app.py
Avanish11's picture
Update app.py
0817d0f verified
Raw
History Blame Contribute Delete
37.3 kB
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>
"""
@app.get("/", response_class=HTMLResponse)
async def home():
return HTML
@app.post("/remove")
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)