图像修复的核心不是简单涂成某个颜色,而是根据遮罩周围的纹理和结构推断被移除区域。LemonArt 将图像修复作为独立板块,用户上传图片后在预览画布上拖动涂抹,点击“开始修复”才执行 OpenCV.js 算法,避免把普通预处理和修复操作混在一起。
一、遮罩是修复质量的第一控制点
OpenCV 的 inpaint 接收原图、单通道遮罩、修复半径和算法标识。遮罩中非零像素代表需要修复的区域,因此前端笔刷不能只画在瑕疵中心,通常还要覆盖边缘并留出少量安全范围。LemonArt 在 Canvas 上绘制用户笔迹,同时把相同坐标映射到离屏遮罩画布。
修复遮罩与显示画布尺寸可能不同,必须使用自然尺寸和 CSS 显示尺寸的比例换算。否则用户看到的绿色笔刷位置和真正交给 OpenCV 的区域会错位,尤其在手机端和高分辨率图片上更明显。
笔刷绘制本身也在维护状态:每次 pointermove 都把上一点与当前点连成线段,线段宽度等于修复半径的两倍,颜色固定为半透明绿色便于用户辨认;有笔迹后 repairHasMask 变为 true,这是“开始修复”按钮的启用条件,清除遮罩只清空离屏画布,不触碰原始图片。
- 单通道遮罩:黑色表示保留,白色或非零表示修复。
- 笔刷半径:太小会残留边缘,太大可能损失不需要修复的纹理。
- 坐标换算:以图片自然宽高为基准,不以屏幕像素直接写入 Mat。
- 独立清除:清除遮罩只清除修复区域,不修改原始图片。
ctx.save()
ctx.strokeStyle = 'rgba(128, 196, 78, .62)' // 遮罩层可见笔迹
ctx.lineWidth = Math.max(2, radius * 2) // 线段宽度 = 修复直径
ctx.lineCap = 'round'; ctx.lineJoin = 'round'
ctx.beginPath(); ctx.moveTo(from.x, from.y)
ctx.lineTo(to.x, to.y); ctx.stroke(); ctx.restore()
repairHasMask = truets二、Telea 与 Navier-Stokes 如何选择
Telea 是快速、实用的快速行进法,通常适合小面积瑕疵、文字和简单纹理,交互反馈更快。Navier-Stokes 会参考图像结构和等照度方向,在某些连续边缘上可以获得更自然的延展,但计算成本和结果差异也更明显。工具不应宣称某个算法对所有照片都最好,而应让用户在结果预览中选择。
int method = methodName == "telea" ? cv::INPAINT_TELEA : cv::INPAINT_NS;
cv::inpaint(source, mask, radius, result, method);cpp三、为什么采用显式的“开始修复”按钮
修复是计算密集型且可能不可逆的操作。若每次 pointermove 都调用 inpaint,用户拖动时会触发大量 WebAssembly 计算,页面会变得迟钝,也很难判断当前结果是否已经应用。LemonArt 先实时显示遮罩,只有用户点击开始修复后才提交算法;修复完成后才把结果作为新的预览基准。
这也让错误边界更清晰:OpenCV 尚未加载时显示加载状态,没有遮罩时禁用按钮,算法异常时保留原图和遮罩,用户可以调整半径后重新执行。
四、修复之后还要验证导出
修复结果回到 Canvas 后可以继续进行去噪、调色、缩放和裁剪,但导出前应确认当前画布尺寸、颜色通道和 Alpha 行为。透明 PNG 和 JPG 的背景语义不同,修复工具应沿用统一的导出设置,而不是另写一套下载逻辑。
在真实使用中,OpenCV 修复更适合小面积物体移除、划痕、传感器污点和局部文字清理。大面积背景重建、复杂人像重绘或语义级内容生成属于另一类模型能力,不应把传统 inpaint 的结果描述成生成式重建。
五、完整的修复管线与参数边界
把上面几节串起来,一次修复的完整管线是:pointermove 绘制遮罩 → 点击开始修复 → 校验遮罩非空 → 打开 OpenCV.js 运行时(缓存 Promise)→ 原图 RGBA 转 RGB → 遮罩 RGBA 转灰度并二值化 → cv.inpaint → 结果 RGB 转回 RGBA → 写回预览 Canvas → 释放所有 Mat。管线中的所有参数都在边界内收敛:修复半径被限制在 1-50 像素,阈值固定为 1,算法只接受 telea 与 ns 两个枚举值。
错误恢复遵循“保持可重试”原则:OpenCV 尚未加载完成时按钮显示加载状态并禁用;没有遮罩时按钮禁用并提示先涂抹;算法抛出异常时保留原图与遮罩,用户调整半径或换算法后可以重新执行。修复一旦成功,repairCommitted 置为 true,再次绘制遮罩会自动取消已提交状态,避免用户误以为旧结果仍然有效。
if (sourceChannels === 4) cv.cvtColor(current, rgb, cv.COLOR_RGBA2RGB)
cv.cvtColor(maskSource, maskGray, cv.COLOR_RGBA2GRAY)
cv.threshold(maskGray, maskBinary, 1, 255, cv.THRESH_BINARY)
const radius = Math.max(1, Math.min(50, repairRadius))
const method = repairMethod === 'ns' ? cv.INPAINT_NS : cv.INPAINT_TELEA
cv.inpaint(rgb, maskBinary, repaired, radius, method)
cv.cvtColor(repaired, restored, cv.COLOR_RGB2RGBA)
// finally 中统一 release 所有 Mat,避免 WASM 堆泄漏ts要点总结
- 遮罩坐标和笔刷边界比算法名称更直接影响修复结果。
- Telea 偏向快速实用,Navier-Stokes 更关注结构延展。
- 把算法触发放在明确按钮后,能同时改善性能、可控性和错误恢复。
- 通道转换、参数收敛与 Mat 释放是修复管线稳定性的三个关键点。