背景
我已经构建了一个基于web的小应用程序,它可以弹出窗口来显示您的网络摄像头。我想添加对你的提要进行色度键设置的能力,并且已经成功地使几种不同的算法发挥作用。然而,我发现的最好的算法对JavaScript来说是非常耗费资源的;单线程应用程序。
问题
有没有办法将密集的数学运算卸载到GPU?我试着让GPU.js工作,但我总是遇到各种各样的错误。以下是我想让GPU运行的功能:
let dE76 = function(a, b, c, d, e, f) {
return Math.sqrt( pow(d - a, 2) + pow(e - b, 2) + pow(f - c, 2) );
};
let rgbToLab = function(r, g, b) {
let x, y, z;
r = r / 255;
g = g / 255;
b = b / 255;
r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
return [ (116 * y) - 16, 500 * (x - y), 200 * (y - z) ];
};
这里发生的是,我向rgbToLab
发送一个RGB值,它会返回LAB值,该值可以与已经存储的带有dE76
的绿色屏幕的LAB值进行比较。然后在我的应用程序中,我们将dE76
值检查为三重,比如25,如果值小于这个值,我会在视频馈送中将像素不透明度设置为0。
GPU.js尝试
这是我最新的GUI.js尝试:
// Try to combine the 2 functions into a single kernel function for GPU.js
let tmp = gpu.createKernel( function( r, g, b, lab ) {
let x, y, z;
r = r / 255;
g = g / 255;
b = b / 255;
r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
let clab = [ (116 * y) - 16, 500 * (x - y), 200 * (y - z) ];
let d = pow(lab[0] - clab[0], 2) + pow(lab[1] - clab[1], 2) + pow(lab[2] - clab[2], 2);
return Math.sqrt( d );
} ).setOutput( [256] );
// ...
// Call the function above.
let d = tmp( r, g, b, chromaColors[c].lab );
// If the delta (d) is lower than my tolerance level set pixel opacity to 0.
if( d < tolerance ){
frame.data[ i * 4 + 3 ] = 0;
}
错误:
以下是我在调用tmp函数时尝试使用GPU.js时遇到的错误列表。1( 是我上面提供的代码。2( 用于擦除tmp中的所有代码并只添加一个空返回3(如果我尝试添加tmp函数中的函数;一个有效的JavaScript,但不是C或内核代码。
- 未捕获错误:未定义标识符
- 未捕获错误:编译片段着色器时出错:错误:0:463:";":语法错误
- 未捕获错误:getDependencies中未处理的类型FunctionExpression
的一些打字错误
pow should be Math.pow()
和
let x, y, z should be declare on there own
let x = 0
let y = 0
let z = 0
不能为参数变量赋值。他们变得统一。
完整的工作脚本
const { GPU } = require('gpu.js')
const gpu = new GPU()
const tmp = gpu.createKernel(function (r, g, b, lab) {
let x = 0
let y = 0
let z = 0
let r1 = r / 255
let g1 = g / 255
let b1 = b / 255
r1 = (r1 > 0.04045) ? Math.pow((r1 + 0.055) / 1.055, 2.4) : r1 / 12.92
g1 = (g1 > 0.04045) ? Math.pow((g1 + 0.055) / 1.055, 2.4) : g1 / 12.92
b1 = (b1 > 0.04045) ? Math.pow((b1 + 0.055) / 1.055, 2.4) : b1 / 12.92
x = (r1 * 0.4124 + g1 * 0.3576 + b1 * 0.1805) / 0.95047
y = (r1 * 0.2126 + g1 * 0.7152 + b1 * 0.0722) / 1.00000
z = (r1 * 0.0193 + g1 * 0.1192 + b1 * 0.9505) / 1.08883
x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116
y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116
z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116
const clab = [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]
const d = Math.pow(lab[0] - clab[0], 2) + Math.pow(lab[1] - clab[1], 2) + Math.pow(lab[2] - clab[2], 2)
return Math.sqrt(d)
}).setOutput([256])
console.log(tmp(128, 139, 117, [40.1332, 10.99816, 5.216413]))
这不是我最初问题的答案,我确实提出了一个计算速度快的穷人替代方案。我在这里为其他试图在JavaScript中进行色度键控的人提供了这段代码。从视觉上讲,输出视频非常接近OP中较重的Delta E 76代码。
步骤1:将RGB转换为YUV
我找到了一个StackOverflow答案,它有一个用C编写的非常快速的RGB到YUV转换函数。后来我还找到了Edward Cannon的Greenscreen Code and Hints,它有将RGB转换成YCbCr的C函数。我把这两个都取了下来,把它们转换成JavaScript,并测试了哪一个实际上更适合色度键控。爱德华·坎农的函数是有用的——事实证明它并不比卡米尔·古德修恩的代码更好;上述SO答案参考。爱德华的代码评论如下:
let rgbToYuv = function( r, g, b ) {
let y = 0.257 * r + 0.504 * g + 0.098 * b + 16;
//let y = Math.round( 0.299 * r + 0.587 * g + 0.114 * b );
let u = -0.148 * r - 0.291 * g + 0.439 * b + 128;
//let u = Math.round( -0.168736 * r - 0.331264 * g + 0.5 * b + 128 );
let v = 0.439 * r - 0.368 * g - 0.071 * b + 128;
//let v = Math.round( 0.5 * r - 0.418688 * g - 0.081312 * b + 128 );
return [ y, u, v ];
}
步骤2:检查两种YUV颜色的接近程度
再次感谢Edward Cannon的Greenscreen代码和提示,比较两种YUU颜色非常简单。我们可以忽略这里的Y,只需要U和V值;如果你想知道为什么你需要学习YUV(YCbCr(,特别是亮度和色度部分。以下是转换为JavaScript:的C代码
let colorClose = function( u, v, cu, cv ){
return Math.sqrt( ( cu - u ) * ( cu - u ) + ( cv - v ) * ( cv - v ) );
};
如果你读了这篇文章,你会注意到这并不是全部功能。在我的应用程序中,我处理的是视频,而不是静止图像,因此提供背景和前景色以包括在计算中会很困难。这也会增加计算量。下一步有一个简单的解决方法。
第三步:检查容差&Clean Edges
由于我们在这里处理视频,因此我们循环查看每帧的像素数据,并检查colorClose
值是否低于某个阈值。如果我们刚刚检查的颜色低于公差级别,我们需要将像素的不透明度设置为0,使其透明。
由于这是一个非常快速的穷人色度键,我们倾向于在剩余图像的边缘出现颜色流血。向上或向下调整公差值可以大大减少这种情况,但我们也可以添加一个简单的顺桨效果。如果一个像素没有被标记为透明,但接近公差水平,我们可以部分关闭它。下面的代码演示了这一点:
// ...My app specific code.
/*
NOTE: chromaColors is an array holding RGB colors the user has
selected from the video feed. My app requires the user to select
the lightest and darkest part of their green screen. If lighting
is bad they can add more colors to this array and we will do our
best to chroma key them out.
*/
// Grab the current frame data from our Canvas.
let frame = ctxHolder.getImageData( 0, 0, width, height );
let frames = frame.data.length / 4;
let colors = chromaColors.length - 1;
// Loop through every pxel of this frame.
for ( let i = 0; i < frames; i++ ) {
// Each pixel is stored as an rgba value; we don't need a.
let r = frame.data[ i * 4 + 0 ];
let g = frame.data[ i * 4 + 1 ];
let b = frame.data[ i * 4 + 2 ];
let yuv = rgbToYuv( r, g, b );
// Check the current pixel against our list of colors to turn transparent.
for ( let c = 0; c < colors; c++ ) {
// When the user selected a color for chroma keying we wen't ahead
// and saved the YUV value to save on resources. Pull it out for use.
let cc = chromaColors[c].yuv;
// Calc the closeness (distance) of the currnet pixel and chroma color.
let d = colorClose( yuv[1], yuv[2], cc[1], cc[2] );
if( d < tolerance ){
// Turn this pixel transparent.
frame.data[ i * 4 + 3 ] = 0;
break;
} else {
// Feather edges by lowering the opacity on pixels close to the tolerance level.
if ( d - 1 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.1;
break;
}
if ( d - 2 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.2;
break;
}
if ( d - 3 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.3;
break;
}
if ( d - 4 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.4;
break;
}
if ( d - 5 < tolerance ){
frame.data[ i * 4 + 3 ] = 0.5;
break;
}
}
}
}
// ...My app specific code.
// Put the altered frame data back into the video feed.
ctxMain.putImageData( frame, 0, 0 );
其他资源我应该提到Zachary Schuessler的实时Chroma Key With Delta E 76和Delta E 101对我获得这些解决方案有很大帮助。