HTML5与JavaScript图像修复中Canvas绘制蒙版捕获不准确问题
图像修复应用蒙版匹配问题排查与修复
我正在开发一款图像修复(Image Inpainting)应用,允许用户在Canvas上绘制需要移除的区域,绘制的蒙版(Mask)会发送至服务器处理。但目前遇到问题:服务器生成的蒙版与用户绘制区域不匹配,存在缺失或错位,导致修复效果不佳。



现有代码实现
HTML结构
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Inpaint Image</title> <style> /* Styles omitted for brevity */ </style> </head> <body> <div class="container"> <h1>Paint on the object you want to remove</h1> <canvas id="canvas" width="300" height="200"></canvas> <button id="undoButton" disabled>Undo</button> <form id="maskForm"> <input type="hidden" name="maskData" id="maskData"> <input type="hidden" name="filename" id="filename" value="{{ filename }}"> <input type="button" value="Remove" onclick="submitMask();" id="removeButton"> </form> <label for="brushSize">Brush Size:</label> <input type="range" id="brushSize" name="brushSize" min="1" max="50" value="5"> <button id="downloadButton" style="display:none;">Download Image</button> </div> <script> const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.src = `/static/uploads/{{ filename }}`; const undoButton = document.getElementById('undoButton'); let undoStack = []; let mask = []; let brushSize = 5; // Default brush size img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); mask = new Uint8Array(canvas.width * canvas.height).fill(0); saveState(); }; let drawing = false; function saveState() { undoStack.push(canvas.toDataURL()); undoButton.disabled = false; } function restoreState() { if (undoStack.length > 0) { const state = undoStack.pop(); const img = new Image(); img.src = state; img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let i = 0; i < imgData.data.length; i += 4) { mask[i / 4] = imgData.data[i] === 255 ? 255 : 0; } }; } if (undoStack.length === 0) { undoButton.disabled = true; } } canvas.addEventListener('mousedown', () => { drawing = true; saveState(); }); canvas.addEventListener('mouseup', () => { drawing = false; }); canvas.addEventListener('mouseout', () => { if (drawing) { drawing = false; } }); canvas.addEventListener('mousemove', (e) => { if (!drawing) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; ctx.fillStyle = 'rgba(255, 0, 0, 1.0)'; ctx.beginPath(); ctx.arc(x, y, brushSize, 0, 2 * Math.PI); ctx.fill(); updateMask(x, y, brushSize); }); document.getElementById('brushSize').addEventListener('input', (e) => { brushSize = parseInt(e.target.value); }); function updateMask(x, y, brushSize) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const scaledX = Math.floor(x * scaleX); const scaledY = Math.floor(y * scaleY); const brushRadius = Math.floor(brushSize * scaleX / 2); for (let i = -brushRadius; i <= brushRadius; i++) { for (let j = -brushRadius; j <= brushRadius; j++) { const dist = Math.sqrt(i * i + j * j); if (dist <= brushRadius) { const maskX = scaledX + i; const maskY = scaledY + j; if (maskX >= 0 && maskX < canvas.width && maskY >= 0 && maskY < canvas.height) { mask[maskY * canvas.width + maskX] = 255; } } } } } function submitMask() { const maskData = Array.from(mask).join(','); const filename = document.getElementById('filename').value; canvas.classList.add('blinking'); fetch('/inpaint_ajax', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ maskData, filename }), }) .then(response => response.blob()) .then(blob => { const url = URL.createObjectURL(blob); img.src = url; img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); mask = new Uint8Array(canvas.width * canvas.height).fill(0); canvas.classList.remove('blinking'); document.getElementById('downloadButton').style.display = 'block'; document.getElementById('downloadButton').onclick = () => downloadImage(url); }; }) .catch(error => console.error('Error:', error)); } function downloadImage(url) { const a = document.createElement('a'); a.href = url; a.download = 'processed_image.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } undoButton.addEventListener('click', restoreState); </script> </body> </html>
后端处理(Flask)
@app.route('/inpaint_ajax', methods=['POST']) def inpaint_ajax(): data = request.get_json() filename = data['filename'] mask_data = data['maskData'] image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) img = Image.open(image_path).convert('RGB') original_size = img.size fixed_size = app.config['FIXED_SIZE'] image_tensor = preprocess_image(image_path, fixed_size) mask_tensor = preprocess_mask(mask_data, original_size[::-1], fixed_size) lama_model = load_lama_model(app.config['MODEL_PATH']) with torch.no_grad(): inpainted_tensor = lama_model(image_tensor, mask_tensor) output_path = os.path.join(app.config['OUTPUT_FOLDER'], filename) postprocess_and_save(inpainted_tensor, output_path, original_size) shutil.copy(output_path, image_path) mask_array = np.array(mask_data.split(','), dtype=np.uint8).reshape(original_size[::-1]) mask_image_path = os.path.join(app.config['UPLOAD_FOLDER'], f'mask_{filename}') cv2.imwrite(mask_image_path, mask_array) return send_from_directory(app.config['OUTPUT_FOLDER'], filename)
问题根源与修复方案
前端问题修复
1. 修正蒙版坐标计算逻辑
updateMask函数中重复对坐标进行缩放是错误的——mousemove中已经通过e.clientX - rect.left得到了Canvas内部的正确坐标,无需再次缩放。修改后的代码:
function updateMask(x, y, brushSize) { const scaledX = Math.floor(x); const scaledY = Math.floor(y); const brushRadius = Math.floor(brushSize / 2); for (let i = -brushRadius; i <= brushRadius; i++) { for (let j = -brushRadius; j <= brushRadius; j++) { const dist = Math.sqrt(i * i + j * j); if (dist <= brushRadius) { const maskX = scaledX + i; const maskY = scaledY + j; if (maskX >= 0 && maskX < canvas.width && maskY >= 0 && maskY < canvas.height) { mask[maskY * canvas.width + maskX] = 255; } } } } }
2. 优化Undo时的蒙版同步
之前从Canvas图像中提取蒙版容易出现误差,改为直接保存蒙版状态:
// 修改saveState function saveState() { undoStack.push({ canvasState: canvas.toDataURL(), maskState: [...mask] }); undoButton.disabled = false; } // 修改restoreState function restoreState() { if (undoStack.length > 0) { const state = undoStack.pop(); const img = new Image(); img.src = state.canvasState; img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); mask = state.maskState; }; } if (undoStack.length === 0) { undoButton.disabled = true; } }
3. 保证Canvas显示与内部尺寸一致
添加CSS或JS逻辑,避免Canvas显示缩放导致坐标错位:
canvas { max-width: 100%; height: auto; image-rendering: pixelated; }
或者在图片加载时设置Canvas样式尺寸:
img.onload = () => { canvas.width = img.width; canvas.height = img.height; canvas.style.width = `${img.width}px`; canvas.style.height = `${img.height}px`; ctx.drawImage(img, 0, 0); mask = new Uint8Array(canvas.width * canvas.height).fill(0); saveState(); };
后端问题修复
1. 统一蒙版与图像的预处理逻辑
确保图像和蒙版使用相同的缩放、padding规则,且蒙版缩放使用最近邻插值避免边缘模糊:
def preprocess_mask(mask_data, original_size, fixed_size): mask_array = np.array(mask_data.split(','), dtype=np.uint8).reshape(original_size) h, w = original_size scale = min(fixed_size[0]/w, fixed_size[1]/h) new_w, new_h = int(w*scale), int(h*scale) # 用最近邻插值保持蒙版边缘锐利 mask_resized = cv2.resize(mask_array, (new_w, new_h), interpolation=cv2.INTER_NEAREST) # 和图像预处理一致的padding逻辑 padded_mask = np.zeros((fixed_size[1], fixed_size[0]), dtype=np.uint8) x_offset = (fixed_size[0] - new_w) // 2 y_offset = (fixed_size[1] - new_h) // 2 padded_mask[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = mask_resized # 后续tensor转换逻辑 ... return padded_mask_tensor
2. 调试蒙版生成过程
在后端保存原始尺寸的蒙版图像,对比用户绘制区域,定位问题环节:
mask_array = np.array(mask_data.split(','), dtype=np.uint8).reshape(original_size) cv2.imwrite(os.path.join(app.config['UPLOAD_FOLDER'], f'original_mask_{filename}'), mask_array)
内容的提问来源于stack exchange,提问作者Ashmit Singh
相关产品推荐
相关产品推荐

