如何使用 GPU.js 将繁重的 JavaScript 数学运算传递给 GPU
How to pass off heavy JavaScript math operations to GPU with GPU.js
背景
我构建了一个基于 Web 的小应用程序,它会弹出 windows 以显示您的网络摄像头。我想为您的提要添加色度键控功能,并且已经成功地让几种不同的算法发挥作用。然而,我发现的最佳算法对于 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) ];
};
这里发生的是我将 RGB 值发送到 rgbToLab
,它返回 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 中的所有代码并仅添加一个空 return 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
我找到了 Edward Cannon 的 Whosebug answer that has a very fast RGB to YUV conversion function written in C. Later I also found Greenscreen Code and Hints,它有一个 C 函数可以将 RGB 转换为 YCbCr。我把这两个都拿走了,将它们转换为 JavaScript,并测试了哪个实际上更适合色度键控。好吧,Edward Cannon 的功能很有用,但事实证明它并不比 Camille Goudeseune 的代码好多少;上面的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 颜色的接近程度
再次感谢 Greenscreen Code and Hints by Edward Cannon comparing two YUV colors was fairly simple. We can ignore Y here and only need the U and V values; if you want to know why you will need to study up on YUV (YCbCr),特别是关于亮度和色度的部分。这是转换为 JavaScript:
的 C 代码
let colorClose = function( u, v, cu, cv ){
return Math.sqrt( ( cu - u ) * ( cu - u ) + ( cv - v ) * ( cv - v ) );
};
如果您阅读了这篇文章,您会发现这不是完整的功能。在我的应用程序中,我处理的是视频而不是静止图像,因此提供背景和前景色以包含在计算中将很困难。它还会增加计算负荷。下一步有一个简单的解决方法。
第 3 步:检查公差并清理边缘
由于我们在这里处理视频,因此我们遍历每一帧的像素数据并检查 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 的 Real-Time Chroma Key With Delta E 76 and Delta E 101 对我找到这些解决方案有很大帮助。
背景
我构建了一个基于 Web 的小应用程序,它会弹出 windows 以显示您的网络摄像头。我想为您的提要添加色度键控功能,并且已经成功地让几种不同的算法发挥作用。然而,我发现的最佳算法对于 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) ];
};
这里发生的是我将 RGB 值发送到 rgbToLab
,它返回 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 中的所有代码并仅添加一个空 return 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
我找到了 Edward Cannon 的 Whosebug answer that has a very fast RGB to YUV conversion function written in C. Later I also found Greenscreen Code and Hints,它有一个 C 函数可以将 RGB 转换为 YCbCr。我把这两个都拿走了,将它们转换为 JavaScript,并测试了哪个实际上更适合色度键控。好吧,Edward Cannon 的功能很有用,但事实证明它并不比 Camille Goudeseune 的代码好多少;上面的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 颜色的接近程度
再次感谢 Greenscreen Code and Hints by Edward Cannon comparing two YUV colors was fairly simple. We can ignore Y here and only need the U and V values; if you want to know why you will need to study up on YUV (YCbCr),特别是关于亮度和色度的部分。这是转换为 JavaScript:
let colorClose = function( u, v, cu, cv ){
return Math.sqrt( ( cu - u ) * ( cu - u ) + ( cv - v ) * ( cv - v ) );
};
如果您阅读了这篇文章,您会发现这不是完整的功能。在我的应用程序中,我处理的是视频而不是静止图像,因此提供背景和前景色以包含在计算中将很困难。它还会增加计算负荷。下一步有一个简单的解决方法。
第 3 步:检查公差并清理边缘
由于我们在这里处理视频,因此我们遍历每一帧的像素数据并检查 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 的 Real-Time Chroma Key With Delta E 76 and Delta E 101 对我找到这些解决方案有很大帮助。