0

背景
我已经构建了一个基于 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 或内核代码。

  1. 未捕获的错误:未定义标识符
  2. 未捕获的错误:编译片段着色器时出错:错误:0:463:';' : 语法错误
  3. 未捕获的错误:getDependencies 中未处理的类型 FunctionExpression
4

2 回答 2

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]))
于 2020-09-25T00:52:41.913 回答
0

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

第 1 步:将 RGB 转换为 YUV
我找到了一个StackOverflow 答案,它具有用 C 编写的非常快的 RGB 到 YUV 转换函数。后来我还发现了 Edward Cannon 的Greenscreen Code and Hints ,它具有将 RGB 转换为 YCbCr 的 C 函数。我拿了这两个,将它们转换为 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 代码和Edward Cannon 的提示,比较两种 YUV 颜色非常简单。我们可以在这里忽略 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 ) );
};

如果您阅读这篇文章,您会发现这不是完整的功能。在我的应用程序中,我处理的是视频而不是静止图像,因此提供背景和前景色以包含在计算中会很困难。它还会增加计算负载。在下一步中有一个简单的解决方法。

第 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 的 Delta E 76和Delta E 101的实时色度键对我获得这些解决方案有很大帮助。

于 2020-09-25T19:05:20.377 回答