You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 10:12:01