前言
之前我写过一篇关于Fabric.js实时播放视频并扣除绿幕的文章,里面的视频绿幕抠图功能是通过fabricjs
内置的removeColor滤镜实现的;但是removeColor滤镜实现绿幕抠图的效果其实并不是很好,有明显的颜色残留:
而且在我们认知中相近的颜色,也不能一起去除:
其实绿幕抠图,可以更完美,本篇就来通过fabricjs自定义滤镜,实现更好的抠图效果。
Demo示例在此,可以自行体验,使用方式1即可:fabricjs-demo.videocovert.online/#/removebg
removeColor滤镜实现
实现自定义滤镜之前,我们先来讨论下removeColor滤镜的实现方案。
function(options) {
var imageData = options.imageData,
data = imageData.data, i,
distance = this.distance * 255,
r, g, b,
source = new fabric.Color(this.color).getSource(),
lowC = [
source[0] - distance,
source[1] - distance,
source[2] - distance,
],
highC = [
source[0] + distance,
source[1] + distance,
source[2] + distance,
];
for (i = 0; i < data.length; i += 4) {
r = data[i];
g = data[i + 1];
b = data[i + 2];
if (r > lowC[0] &&
g > lowC[1] &&
b > lowC[2] &&
r < highC[0] &&
g < highC[1] &&
b < highC[2]) {
data[i + 3] = 0;
}
}
}
这是fabricjs removeColor滤镜canvas实现的源码,从这段代码中,我们可以了解到removeColor滤镜的原理其实比较简单,就是比较像素点的RGB色值是否在需要移除的色值范围内,如果在,则将像素点的透明度设置为0。
该方法抠图存在两个缺陷:
- 色彩范围问题,代码只是简单地比较RGB值的上下限,没有考虑色彩空间转换或感知色差。RGB色彩空间中的距离不一定是视觉上最合理的,说简单点就是RGB色值不符合人对颜色的感官,我们认为是相近的颜色,但是在RGB中并不接近。
- 透明图统一设置为0,代码将符合条件的像素透明度(
data[i + 3]
)直接设置为0
,完全透明。这样会导致无法保留原有的透明度信息,也无法进行部分透明的处理,图形显示不够顺滑。
所以我们需要更好的处理方式。
自定义实现
完整实现代码:
import { fabric } from 'fabric';
fabric.Image.filters.RemoveGreen = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.RemoveGreen.prototype */ {
/**
* Filter type
* @param {String} type
* @default
*/
type: 'RemoveGreen',
/**
* Color to remove, in any format understood by fabric.Color.
* @param {String} type
* @default
*/
color: '#00FF00',
/**
* Fragment source for the brightness program
*/
fragmentSource: `precision highp float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform vec3 keyColor;
// 色度的相似度计算
uniform float similarity;
// 透明度的平滑度计算
uniform float smoothness;
// 降低绿幕饱和度,提高抠图准确度
uniform float spill;
vec2 RGBtoUV(vec3 rgb) {
return vec2(
rgb.r * -0.169 + rgb.g * -0.331 + rgb.b * 0.5 + 0.5,
rgb.r * 0.5 + rgb.g * -0.419 + rgb.b * -0.081 + 0.5
);
}
void main() {
// 获取当前像素的rgba值
vec4 rgba = texture2D(uTexture, vTexCoord);
// 计算当前像素与绿幕像素的色度差值
vec2 chromaVec = RGBtoUV(rgba.rgb) - RGBtoUV(keyColor);
// 计算当前像素与绿幕像素的色度距离(向量长度), 越相像则色度距离越小
float chromaDist = sqrt(dot(chromaVec, chromaVec));
// 设置了一个相似度阈值,baseMask为负,则表明是绿幕,为正则表明不是绿幕
float baseMask = chromaDist - similarity;
// 如果baseMask为负数,fullMask等于0;baseMask为正数,越大,则透明度越低
float fullMask = pow(clamp(baseMask / smoothness, 0., 1.), 1.5);
rgba.a = fullMask; // 设置透明度
// 如果baseMask为负数,spillVal等于0;baseMask为整数,越小,饱和度越低
float spillVal = pow(clamp(baseMask / spill, 0., 1.), 1.5);
float desat = clamp(rgba.r * 0.2126 + rgba.g * 0.7152 + rgba.b * 0.0722, 0., 1.); // 计算当前像素的灰度值
rgba.rgb = mix(vec3(desat, desat, desat), rgba.rgb, spillVal);
gl_FragColor = rgba;
}`,
similarity: 0.02,
smoothness: 0.02,
spill: 0.02,
/**
* distance to actual color, as value up or down from each r,g,b
* between 0 and 1
**/
distance: 0.02,
/**
* For color to remove inside distance, use alpha channel for a smoother deletion
* NOT IMPLEMENTED YET
**/
useAlpha: false,
/**
* Constructor
* @memberOf fabric.Image.filters.RemoveWhite.prototype
* @param {Object} [options] Options object
* @param {Number} [options.color=#RRGGBB] Threshold value
* @param {Number} [options.distance=10] Distance value
*/
/**
* Applies filter to canvas element
* @param {Object} canvasEl Canvas element to apply filter to
*/
applyTo2d: function(options) {
var imageData = options.imageData,
data = imageData.data, i,
distance = this.distance * 255,
r, g, b,
source = new fabric.Color(this.color).getSource(),
lowC = [
source[0] - distance,
source[1] - distance,
source[2] - distance,
],
highC = [
source[0] + distance,
source[1] + distance,
source[2] + distance,
];
for (i = 0; i < data.length; i += 4) {
r = data[i];
g = data[i + 1];
b = data[i + 2];
if (r > lowC[0] &&
g > lowC[1] &&
b > lowC[2] &&
r < highC[0] &&
g < highC[1] &&
b < highC[2]) {
data[i + 3] = 0;
}
}
},
/**
* Return WebGL uniform locations for this filter's shader.
*
* @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader.
* @param {WebGLShaderProgram} program This filter's compiled shader program.
*/
getUniformLocations: function(gl, program) {
return {
similarity: gl.getUniformLocation(program, 'similarity'),
smoothness: gl.getUniformLocation(program, 'smoothness'),
spill: gl.getUniformLocation(program, 'spill'),
keyColor: gl.getUniformLocation(program, 'keyColor'),
};
},
/**
* Send data from this filter to its shader program's uniforms.
*
* @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader.
* @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects
*/
sendUniformData: function(gl, uniformLocations) {
// var source = new fabric.Color(this.color).getSource(),
// distance = parseFloat(this.distance),
// lowC = [
// 0 + source[0] / 255 - distance,
// 0 + source[1] / 255 - distance,
// 0 + source[2] / 255 - distance,
// 1
// ],
// highC = [
// source[0] / 255 + distance,
// source[1] / 255 + distance,
// source[2] / 255 + distance,
// 1
// ];
gl.uniform3fv(
uniformLocations.keyColor,
(new fabric.Color(this.color).getSource()).slice(0, 3).map((v) => v / 255),
);
gl.uniform1f(uniformLocations.similarity, this.similarity);
gl.uniform1f(uniformLocations.smoothness, this.smoothness);
gl.uniform1f(uniformLocations.spill, this.spill);
},
/**
* Returns object representation of an instance
* @return {Object} Object representation of an instance
*/
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
color: this.color,
similarity: this.similarity,
smoothness: this.smoothness,
spill: this.spill,
});
}
});
/**
* Returns filter instance from an object representation
* @static
* @param {Object} object Object to create an instance from
* @param {Function} [callback] to be invoked after filter creation
* @return {fabric.Image.filters.RemoveGreen} Instance of fabric.Image.filters.RemoveWhite
*/
fabric.Image.filters.RemoveGreen.fromObject = fabric.Image.filters.BaseFilter.fromObject;
使用UV色值
代码中的WebGL滤镜部分就是我们的实现了,在这段代码中,我们将RGB色值转换为UV色值。
vec2 RGBtoUV(vec3 rgb) {
return vec2(
rgb.r * -0.169 + rgb.g * -0.331 + rgb.b * 0.5 + 0.5,
rgb.r * 0.5 + rgb.g * -0.419 + rgb.b * -0.081 + 0.5
);
}
使用UV色值(UV color space)进行抠图相对于传统的RGB色值有几个显著的优势,尤其在复杂的抠图场景中,这些优势可以帮助实现更精准、更自然的抠图效果。
更好的色彩分离能力:
UV色值是在YUV色彩空间中的两个色度分量(U和V),与亮度(Y)分开。由于UV通道仅承载颜色信息,而不涉及亮度,这使得在抠图时更容易区分和分离颜色相近但亮度不同的像素。对于复杂背景或颜色相似的前景,UV色值可以更有效地分离目标和背景。
减少光照影响:
在RGB色彩空间中,亮度信息混杂在R、G、B通道中,这意味着光照变化会显著影响颜色的表现,进而影响抠图效果。而UV色值分离了亮度信息,因此在光照条件不均匀的场景下,抠图效果更稳定,不易受到光照变化的干扰。
更接近人眼视觉的颜色感知:
YUV色彩空间设计的初衷是更接近人眼对颜色的感知,特别是在低亮度情况下,人眼对色度变化更敏感。使用UV色值抠图,更符合人眼的感知规律,可以得到更自然、更细腻的边缘处理效果。
处理高动态范围场景的优势:
在RGB空间中,高动态范围的场景(例如,高亮度的光源或反光物体)容易出现颜色溢出或失真问题。而在YUV空间中,亮度和色度是独立处理的,UV色值不受亮度极端变化的直接影响,这有助于在高动态范围的场景中实现更稳定的抠图效果。
减少背景复杂度的干扰:
在复杂背景下,RGB色彩空间可能无法很好地区分前景和背景,尤其是在背景颜色与前景颜色相似时。UV色值通过色度信息进行抠图,能够更好地分离前景对象,减少背景颜色的干扰。
更精细的控制:similarity、smoothness、spill
除了目标色值,这段代码我们还提供了三个参数similarity、smoothness、spill,用来更加精准的控制抠图效果。
-
similarity
(和removeColor滤镜的distance作用基本一致):- 作用: 控制色度差异的阈值,也就是说,用来确定哪些像素应该被视为接近指定的键颜色(通常是绿幕颜色)。
- 原理: 当计算当前像素与绿幕像素的色度差值时,如果差值小于这个相似度阈值(
similarity
),则说明该像素的颜色与键颜色非常接近,应被视为需要抠出的部分,即其透明度会被设置为0或接近0。 - 影响:
similarity
的值越小,只有与键颜色非常接近的像素才会被抠出;值越大,则更多的颜色范围会被认为接近键颜色,可能会导致更多的区域被抠出。
-
smoothness
(平滑度):- 作用: 控制透明度的平滑过渡区域,以避免产生明显的边界。
- 原理:
smoothness
控制的是色度差值转换为透明度时的平滑度。它决定了从完全透明到完全不透明的过渡区域的宽度。较低的smoothness
值会使过渡更加突然,而较高的值会产生更平滑的透明度过渡。 - 影响:
smoothness
的值越大,透明度的过渡区域越宽,图像的边缘处理会更加平滑,减少硬边现象;值越小,透明度过渡区域会更窄,边缘可能会显得更加锐利。
-
spill
(溢出):- 作用: 控制与键颜色接近的颜色的饱和度,减少色度溢出的效果。
- 原理:
spill
控制的是对与键颜色相近但不完全相同的颜色的处理,特别是那些受色度溢出影响的像素。在计算色度差值后,spill
用于调整这些像素的饱和度,将其颜色向灰度拉近,以减少颜色溢出的视觉效果。 - 影响:
spill
的值越大,图像中与键颜色相近的部分将更加显著地降低饱和度,减少溢出的绿光效应;值越小,这些区域会保留更多的原始颜色,可能会导致更明显的溢出现象。
效果对比
相比较removeColor滤镜,我们的滤镜在相同的参数下(color是同一个值、distance和similarity是相同值),自定义滤镜没有明显的绿色残留:
对于颜色的处理,也更接近人类的认知,removeColor滤镜不能处理的颜色,在相同的参数下,自定义滤镜也能进行处理:
Demo示例在此,可以自行体验,使用方式2即可:fabricjs-demo.videocovert.online/#/removebg