Mask 背景抠除
Mask 做的是最朴素的抠图:猜出背景色是什么,
然后把所有接近这个颜色的像素变成透明。
node.cache();
node.filters([Konva.Filters.Mask]);
node.threshold(10); // RGB 距离容差,默认 0.5
它怎么猜背景色:采样四个角
源码里的逻辑很直接——取图片四个角的像素:
const no = pixelAt(idata, 0, 0); // 左上
const ne = pixelAt(idata, idata.width - 1, 0); // 右上
const so = pixelAt(idata, 0, idata.height - 1); // 左下
const se = pixelAt(idata, idata.width - 1, idata.height - 1); // 右下
然后检查相邻的四对角是否两两接近(RGB 距离小于 threshold)。
只有全部通过,才取四者的平均值作为背景色,
再把所有与之距离小于 threshold 的像素的 alpha 设为 0。
这个前置检查决定了这个滤镜的全部脾气。
四角颜色不一致时,它什么也不做
如果四对角的距离检查有任何一对没通过,函数直接返回 undefined,
滤镜不做任何修改——不抛错、不警告、没有任何提示。
表现就是"加了 Mask 但图片纹丝不动"。
本站用三张示例图实测过(threshold 设为 10):
| 图片 | 四角颜色 | Mask 后透明像素变化 |
|---|---|---|
yoda.jpg | 164,221,104 / 194,162,113 / 4,0,0 / 177,148,106 | 0 → 0(毫无变化) |
darth-vader.jpg | 165,172,182 / 189,203,212 / 183,187,196 / 155,135,124 | 0 → 0(毫无变化) |
两张照片的四个角各不相同,检查全部失败。这就是为什么
Mask 对真实照片基本无效——照片的四个角极少恰好是同一种颜色。
作为对照,把同一张图合成到纯绿背景上(四角完全相同)之后:
threshold | 透明像素占比 |
|---|---|
| 未加滤镜 | 0.0% |
| 5 | 55.8% |
| 10 | 56.3% |
| 40 | 57.7% |
| 80 | 59.0% |
背景被干净地抠掉了。上面的演示用的就是这个合成图。
常见问题
为什么加了 Mask 没反应?
按顺序查这三条:
- 四个角颜色不一致。 这是最常见的原因。
用
getImageData读一下四个角,肉眼比对一下。 threshold太小。 即使背景是"纯色", JPEG 压缩也会让四个角有几个数值的差异。默认的0.5对 JPEG 几乎肯定不够, 从10开始试。- 忘了
cache()。 所有滤镜的通病。
threshold 该设多少?
它同时控制两件事,这一点容易被忽略: 既是四角相似性检查的阈值,也是逐像素抠除的容差。
调大它会让四角检查更容易通过,但也会让更多像素被判定为背景而抠掉。 所以不存在"先调大让它生效、再调小做精细控制"这种两步走—— 两者是同一个旋钮。
实践上:纯色背景的 PNG 从 10 开始;
有压缩噪点的 JPEG 从 30 开始;
超过 80 通常会开始吃掉主体。
主体碰到边缘怎么办?
抠不干净,而且会连主体一起抠掉—— 因为角上的像素就是主体的颜色,算出的"背景色"是错的。
Mask 没有任何办法处理这种情况。
只能先给图片加一圈纯色边距(在离屏 canvas 上画一个更大的纯色矩形,
再把原图居中画上去),保证四个角落在背景上。
抠完边缘有毛刺?
Mask 是硬判定:距离小于阈值就完全透明,否则完全不透明,
没有羽化。半透明的抗锯齿边缘会被判成"不是背景"而保留,
形成一圈背景色的描边(绿幕抠图里典型的"绿边")。
Konva 内置的 Mask 没有去边功能。要处理只能自己写
自定义滤镜,
在抠除之后对边缘像素做一次去饱和或颜色替换。
与其他方案的取舍
Mask 的定位是"纯色背景、要求不高"的场景,
比如把设计师给的白底产品图放到彩色卡片上。
它免费、同步、不依赖任何外部服务,这是它全部的优势。
它的局限是结构性的,不是调参能解决的:只看四个角、 只按颜色距离判定、没有羽化、没有边缘处理。
需要更好的效果时:
背景复杂或要求精细 → 让设计师直接提供带透明通道的 PNG。 这听起来像废话,但在可控的项目里这是成本最低的方案—— 前端抠图永远不如源头就做对。
用户上传的任意图片 → 走服务端。rembg、各家云厂商的抠图 API
都是基于分割模型的,效果和 Mask 不在一个量级。
放到服务端还有一个好处:抠一次存起来,不用每个访客的浏览器都算一遍。
只是要一个视觉上的"去背"效果 → 考虑用
裁剪或
混合模式绕过抠图。
比如 globalCompositeOperation: 'multiply' 能让白底在浅色背景上"消失",
不需要真的处理像素。