Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Advanced Image Inpainting Tool</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 20px; | |
| max-width: 100%; | |
| overflow-x: hidden; | |
| } | |
| #app-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| width: 100%; | |
| max-width: 100%; | |
| } | |
| #canvas-container { | |
| position: relative; | |
| max-width: 100%; | |
| width: 100%; | |
| display: flex; | |
| justify-content: center; | |
| margin: 20px 0; | |
| } | |
| canvas { | |
| border: 2px solid #333; | |
| max-width: 100%; | |
| max-height: 70vh; | |
| object-fit: contain; | |
| } | |
| #originalCanvas, #maskCanvas { | |
| position: absolute; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| #brushSize { | |
| width: 100px; | |
| } | |
| #bottom-controls { | |
| position: fixed; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| background-color: white; | |
| padding: 10px; | |
| box-shadow: 0 -2px 5px rgba(0,0,0,0.1); | |
| display: flex; | |
| justify-content: center; | |
| gap: 10px; | |
| z-index: 1000; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app-container"> | |
| <h1>Advanced Image Inpainting Tool</h1> | |
| <div class="controls"> | |
| <input type="file" id="imageUpload" accept="image/*"> | |
| <label>Brush Size: <input type="range" id="brushSize" min="5" max="50" value="20"></label> | |
| </div> | |
| <div id="canvas-container"> | |
| <canvas id="originalCanvas"></canvas> | |
| <canvas id="maskCanvas"></canvas> | |
| </div> | |
| </div> | |
| <div id="bottom-controls"> | |
| <button id="eraseBtn">Inpaint Selected Area</button> | |
| <button id="clearBtn">Clear Selection</button> | |
| </div> | |
| <script> | |
| class OptimizedImageInpainter { | |
| constructor() { | |
| this.imageUpload = document.getElementById('imageUpload'); | |
| this.originalCanvas = document.getElementById('originalCanvas'); | |
| this.maskCanvas = document.getElementById('maskCanvas'); | |
| this.eraseBtn = document.getElementById('eraseBtn'); | |
| this.clearBtn = document.getElementById('clearBtn'); | |
| this.brushSizeInput = document.getElementById('brushSize'); | |
| this.originalCtx = this.originalCanvas.getContext('2d'); | |
| this.maskCtx = this.maskCanvas.getContext('2d'); | |
| this.isDrawing = false; | |
| this.brushSize = 20; | |
| this.init(); | |
| } | |
| init() { | |
| this.imageUpload.addEventListener('change', this.handleImageUpload.bind(this)); | |
| this.maskCanvas.addEventListener('touchstart', this.startDrawing.bind(this)); | |
| this.maskCanvas.addEventListener('touchmove', this.draw.bind(this)); | |
| this.maskCanvas.addEventListener('touchend', this.stopDrawing.bind(this)); | |
| this.maskCanvas.addEventListener('touchcancel', this.stopDrawing.bind(this)); | |
| this.maskCanvas.addEventListener('mousedown', this.startDrawing.bind(this)); | |
| this.maskCanvas.addEventListener('mousemove', this.draw.bind(this)); | |
| this.maskCanvas.addEventListener('mouseup', this.stopDrawing.bind(this)); | |
| this.maskCanvas.addEventListener('mouseout', this.stopDrawing.bind(this)); | |
| this.eraseBtn.addEventListener('click', this.inpaintImage.bind(this)); | |
| this.clearBtn.addEventListener('click', this.clearSelection.bind(this)); | |
| this.brushSizeInput.addEventListener('input', this.updateBrushSize.bind(this)); | |
| } | |
| handleImageUpload(e) { | |
| const file = e.target.files[0]; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| const maxWidth = window.innerWidth * 0.9; | |
| const maxHeight = window.innerHeight * 0.7; | |
| let width = img.width; | |
| let height = img.height; | |
| if (width > maxWidth) { | |
| height *= maxWidth / width; | |
| width = maxWidth; | |
| } | |
| if (height > maxHeight) { | |
| width *= maxHeight / height; | |
| height = maxHeight; | |
| } | |
| this.originalCanvas.width = width; | |
| this.originalCanvas.height = height; | |
| this.maskCanvas.width = width; | |
| this.maskCanvas.height = height; | |
| this.originalCtx.drawImage(img, 0, 0, width, height); | |
| this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); | |
| } | |
| img.src = event.target.result; | |
| } | |
| reader.readAsDataURL(file); | |
| } | |
| startDrawing(e) { | |
| if (e.type === 'touchstart') { | |
| e.preventDefault(); | |
| this.isDrawing = true; | |
| const rect = this.maskCanvas.getBoundingClientRect(); | |
| const touch = e.touches[0]; | |
| const x = touch.clientX - rect.left; | |
| const y = touch.clientY - rect.top; | |
| this.maskCtx.beginPath(); | |
| this.maskCtx.moveTo(x, y); | |
| this.maskCtx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; | |
| this.maskCtx.lineWidth = this.brushSize; | |
| this.maskCtx.lineCap = 'round'; | |
| this.maskCtx.lineJoin = 'round'; | |
| } else { | |
| // Handle mouse events as before | |
| this.isDrawing = true; | |
| const rect = this.maskCanvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| this.maskCtx.beginPath(); | |
| this.maskCtx.moveTo(x, y); | |
| this.maskCtx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; | |
| this.maskCtx.lineWidth = this.brushSize; | |
| this.maskCtx.lineCap = 'round'; | |
| this.maskCtx.lineJoin = 'round'; | |
| } | |
| } | |
| draw(e) { | |
| if (e.type === 'touchmove') { | |
| e.preventDefault(); | |
| if (this.isDrawing) { | |
| const rect = this.maskCanvas.getBoundingClientRect(); | |
| const touch = e.touches[0]; | |
| const x = touch.clientX - rect.left; | |
| const y = touch.clientY - rect.top; | |
| this.maskCtx.lineTo(x, y); | |
| this.maskCtx.stroke(); | |
| } | |
| } else { | |
| // Handle mouse events as before | |
| if (this.isDrawing) { | |
| const rect = this.maskCanvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| this.maskCtx.lineTo(x, y); | |
| this.maskCtx.stroke(); | |
| } | |
| } | |
| } | |
| stopDrawing(e) { | |
| if (e.type === 'touchend' || e.type === 'touchcancel') { | |
| this.isDrawing = false; | |
| this.maskCtx.closePath(); | |
| } else { | |
| // Handle mouse events as before | |
| if (this.isDrawing) { | |
| this.isDrawing = false; | |
| this.maskCtx.closePath(); | |
| } | |
| } | |
| } | |
| updateBrushSize(e) { | |
| this.brushSize = parseInt(e.target.value); | |
| } | |
| clearSelection() { | |
| this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); | |
| } | |
| inpaintImage() { | |
| // Use Web Workers for parallel processing | |
| if (window.Worker) { | |
| const worker = new Worker(this.createWorkerBlob()); | |
| const imageData = this.originalCtx.getImageData(0, 0, this.originalCanvas.width, this.originalCanvas.height); | |
| const maskImageData = this.maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height); | |
| worker.postMessage({ | |
| imageData: imageData, | |
| maskImageData: maskImageData, | |
| width: this.originalCanvas.width, | |
| height: this.originalCanvas.height | |
| }); | |
| worker.onmessage = (e) => { | |
| this.originalCtx.putImageData(e.data, 0, 0); | |
| this.clearSelection(); | |
| worker.terminate(); | |
| }; | |
| } else { | |
| // Fallback for browsers without Web Worker support | |
| this.inpaintImageFallback(); | |
| } | |
| } | |
| createWorkerBlob() { | |
| const workerCode = ` | |
| self.onmessage = function(e) { | |
| const { imageData, maskImageData, width, height } = e.data; | |
| const data = imageData.data; | |
| const maskData = maskImageData.data; | |
| // More efficient inpainting algorithm | |
| function findBestPixel(x, y) { | |
| const searchRadius = 30; | |
| let bestMatch = null; | |
| let minDifference = Infinity; | |
| for (let r = 1; r <= searchRadius; r++) { | |
| const angleStep = Math.max(45, 360 / (r * 2)); | |
| for (let angle = 0; angle < 360; angle += angleStep) { | |
| const searchX = Math.round(x + r * Math.cos(angle * Math.PI / 180)); | |
| const searchY = Math.round(y + r * Math.sin(angle * Math.PI / 180)); | |
| if (searchX >= 0 && searchX < width && searchY >= 0 && searchY < height) { | |
| const searchIdx = (searchY * width + searchX) * 4; | |
| if (maskData[searchIdx + 3] > 0) continue; | |
| const diff = calculateColorDifference(x, y, searchX, searchY, data, maskData); | |
| if (diff < minDifference) { | |
| minDifference = diff; | |
| bestMatch = { | |
| r: data[searchIdx], | |
| g: data[searchIdx + 1], | |
| b: data[searchIdx + 2] | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| return bestMatch; | |
| } | |
| function calculateColorDifference(x1, y1, x2, y2, imageData, maskData) { | |
| const neighborhoodSize = 3; | |
| let totalDifference = 0; | |
| let validPixels = 0; | |
| for (let dy = -neighborhoodSize; dy <= neighborhoodSize; dy++) { | |
| for (let dx = -neighborhoodSize; dx <= neighborhoodSize; dx++) { | |
| const nx1 = x1 + dx, ny1 = y1 + dy; | |
| const nx2 = x2 + dx, ny2 = y2 + dy; | |
| if (nx1 >= 0 && nx1 < width && ny1 >= 0 && ny1 < height && | |
| nx2 >= 0 && nx2 < width && ny2 >= 0 && ny2 < height) { | |
| const idx1 = (ny1 * width + nx1) * 4; | |
| const idx2 = (ny2 * width + nx2) * 4; | |
| // Weighted color difference with emphasis on luminance | |
| const rDiff = Math.abs(imageData[idx1] - imageData[idx2]) * 0.299; | |
| const gDiff = Math.abs(imageData[idx1 + 1] - imageData[idx2 + 1]) * 0.587; | |
| const bDiff = Math.abs(imageData[idx1 + 2] - imageData[idx2 + 2]) * 0.114; | |
| totalDifference += rDiff + gDiff + bDiff; | |
| validPixels++; | |
| } | |
| } | |
| } | |
| return validPixels > 0 ? totalDifference / validPixels : Infinity; | |
| } | |
| // Multi-pass inpainting with progressively larger search areas | |
| for (let pass = 0; pass < 2; pass++) { | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const idx = (y * width + x) * 4; | |
| if (maskData[idx + 3] > 0) { | |
| const replacementPixel = findBestPixel(x, y); | |
| if (replacementPixel) { | |
| data[idx] = replacementPixel.r; | |
| data[idx + 1] = replacementPixel.g; | |
| data[idx + 2] = replacementPixel.b; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| self.postMessage(imageData); | |
| }`; | |
| const blob = new Blob([workerCode], { type: 'application/javascript' }); | |
| return URL.createObjectURL(blob); | |
| } | |
| inpaintImageFallback() { | |
| const imageData = this.originalCtx.getImageData(0, 0, this.originalCanvas.width, this.originalCanvas.height); | |
| const data = imageData.data; | |
| const maskImageData = this.maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height); | |
| const maskData = maskImageData.data; | |
| const width = this.originalCanvas.width; | |
| const height = this.originalCanvas.height; | |
| // Simplified, less intensive inpainting for browsers without Web Worker support | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| const idx = (y * width + x) * 4; | |
| if (maskData[idx + 3] > 0) { | |
| const nearestPixel = this.findNearestValidPixel(x, y, data, maskData, width, height); | |
| if (nearestPixel) { | |
| data[idx] = nearestPixel.r; | |
| data[idx + 1] = nearestPixel.g; | |
| data[idx + 2] = nearestPixel.b; | |
| } | |
| } | |
| } | |
| } | |
| this.originalCtx.putImageData(imageData, 0, 0); | |
| this.clearSelection(); | |
| } | |
| findNearestValidPixel(x, y, imageData, maskData, width, height) { | |
| const maxSearchRadius = 20; | |
| for (let r = 1; r <= maxSearchRadius; r++) { | |
| for (let angle = 0; angle < 360; angle += 45) { | |
| const searchX = Math.round(x + r * Math.cos(angle * Math.PI / 180)); | |
| const searchY = Math.round(y + r * Math.sin(angle * Math.PI / 180)); | |
| if (searchX >= 0 && searchX < width && searchY >= 0 && searchY < height) { | |
| const searchIdx = (searchY * width + searchX) * 4; | |
| if (maskData[searchIdx + 3] === 0) { | |
| return { | |
| r: imageData[searchIdx], | |
| g: imageData[searchIdx + 1], | |
| b: imageData[searchIdx + 2] | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| } | |
| // Initialize the inpainter | |
| new OptimizedImageInpainter(); | |
| </script> | |
| </body> | |
| </html> |