如何使用 GPU.js 将繁重的 JavaScript 数学运算传递给 GPU

Posted

技术标签:

【中文标题】如何使用 GPU.js 将繁重的 JavaScript 数学运算传递给 GPU【英文标题】:How to pass off heavy JavaScript math operations to GPU with GPU.js 【发布时间】:2020-09-24 22:26:41 【问题描述】:

背景 我已经构建了一个基于 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) ];
;

这里发生的事情是我将一个 RGB 值发送到rgbToLab,它返回的 LAB 值可以与我的绿屏已存储的 LAB 值与dE76 进行比较。然后在我的应用程序中,我们将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

【问题讨论】:

您可能希望在 webgl 中使用片段着色器。他们也是这样的:github.com/turbo/js 我尝试了 turbojs,但我不断收到内核代码错误。我可能会花时间将这两个函数结合起来,并将它们转换为 C,然后再试一次 turbojs。 你能打印出你遇到的错误吗? 我不知道他们在这种情况下会有帮助,但我添加了一些。 【参考方案1】:

一些错别字

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]))

【讨论】:

抱歉 pow 是我不应该显示的剩余代码。它在我的实际代码中使用 Math,pow。 让 x, y, z 怎么样?我认为它可能会修复未定义标识符 导致一个新错误:未捕获的错误:编译片段着色器时出错:错误:0:466:'assign':需要左值(无法修改统一的“user_r”)我想我明白这是在说什么...... 您能否提供您在函数 r、g、b 和 chromaColors 中传递的值 这取决于帧,但是:128、139、117、[...] 该数组只是要比较的 Delta 值。所以像:[40.1332, 10.99816, 5.216413]【参考方案2】:

这不是我最初问题的答案,我确实想出了一个计算速度很快的穷人替代方案。我在此处包含此代码,以供其他人尝试在 JavaScript 中进行色度键控。从视觉上看,输出视频非常接近 OP 中较重的 Delta E 76 代码。

第 1 步:将 RGB 转换为 YUV 我发现了一个*** answer,它有一个用C 编写的非常快的RGB 到YUV 转换函数。后来我还发现了Edward Cannon 的Greenscreen Code and Hints,它有一个将RGB 转换为YCbCr 的C 函数。我拿了这两个,将它们转换为 JavaScript,并测试了哪个实际上更适合色度键控。好吧,Edward Cannon 的函数很有用,它并没有证明比 Camille Goudeseune 的代码更好;上面的SO答案参考。 Edward的代码在下面被注释掉了:

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 Code and Hints,比较两种 YUV 颜色非常简单。我们可以在这里忽略 Y,只需要 U 和 V 值;如果您想知道为什么需要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 和 Delta E 101 对我获得这些解决方案很有帮助。

【讨论】:

以上是关于如何使用 GPU.js 将繁重的 JavaScript 数学运算传递给 GPU的主要内容,如果未能解决你的问题,请参考以下文章

如何使用房间执行繁重的数据库操作?

如何将 QProcess 的执行与 QProgressBar 的推进联系起来以实现非常繁重的计算循环

Flutter - 如何在不阻止UI的情况下计算包含未来的繁重任务?

如何在繁重的工作中持续成长?

如何在繁重的工作中持续成长?

我应该如何使用 JavaScript/HTML5 处理繁重的音频负载?