inpainting / index.html
Reality123b's picture
Update index.html
fe2c81d verified
<!DOCTYPE html>
<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>