【问题标题】:How to compare 2 textures in JavaScript / WebGL2?如何比较 JavaScript / WebGL2 中的 2 个纹理?
【发布时间】:2019-10-11 23:34:30
【问题描述】:

我正在为图像处理算法编写片段着色器。着色器将在循环中的两个帧缓冲区之间运行多次(乒乓)。在某些时候,当输入和输出纹理相同时,我需要停止循环。

我打算做的是 Canny 边缘检测算法的最后一步,“滞后边缘跟踪”。我想制作一个实时 GPU/WebGL2 版本的 Canny 算法并将其上传到网站。

最后一步如下:
给定一个包含“强”边缘像素(1.0)和“弱”边缘像素(0.5)的双阈值图像

  • 找到与强像素相连的所有弱像素链并将它们标记为“强”

  • 保留所有“强”像素并丢弃所有剩余的“弱”像素。

这可以在一个循环运行多次的片段着色器中实现。如果在其 8 像素邻域中至少有一个强像素,则当前“弱”像素被标记为“强”。在每次迭代中,我们应该有更多的强像素和更少的弱像素。最后,应该只保留孤立的弱像素链。这是片段着色器成为直通着色器的点,应检测到该点以停止循环。

2019 年 9 月更新:我在这里 http://www.oldrinb.info/dip/canny/ 上传了 GPU Canny 边缘检测器。它适用于支持 WebGL2 的浏览器,以及支持 WebGL1 和 'WEBGL_draw_buffers' 扩展的浏览器。一会儿我也会把源代码放到github上。

【问题讨论】:

  • “无输出”是指 (a) 结果完全相同还是您的意思是 (b) 没有实际写入像素,它们都被丢弃或剪裁。对于 (b) 您可以在 WebGL2 中使用遮挡查询。他们告诉你画了多少像素。对于(a),您可以制作一个着色器,给定 2 个纹理,将它们进行比较并写入 1 个像素与您使用 CPU 读取的答案......或者将其与(b)结合,使其进行比较并写入或不写入编写该像素并使用遮挡查询来检查
  • 我更正了我的问题,因为它是错误的。实际上,每次都是输出纹理中写入的内容。我需要停止循环的那一刻是输入纹理刚刚复制到输出的那一刻。

标签: javascript webgl textures fragment-shader webgl2


【解决方案1】:

我不是 100% 确定你在问什么。您要求在 CPU 上进行比较。您可以通过将纹理附加到帧缓冲区然后调用gl.readPixels 来读取纹理的内容。然后您可以比较所有像素。注意:并非所有纹理格式都可以附加到帧缓冲区,但假设您使用的格式可以。您已经为您的乒乓球附加了纹理到帧缓冲区,那么您还想要什么?

就像我在 GPU 的评论中所写,您可以编写一个着色器来比较 2 个纹理

#version 300 es
precision highp float;

uniform sampler2D tex1;
uniform sampler2D tex2;

out vec4 outColor;

void main() {
  ivec2 size = textureSize(tex1, 0);  // size of mip 0
  float len = 0.0;
  for (int y = 0; y < size.y; ++y) {
    for (int x = 0; x < size.x; ++x) {
      vec4 color1 = texelFetch(tex1, ivec2(x, y), 0);
      vec4 color2 = texelFetch(tex2, ivec2(x, y), 0);
      vec4 diff = color1 - color2;
      len = length(diff);
      if (len > 0.0) break;
    }
    if (len > 0.0) break;
  }
  outColor = mix(vec4(0), vec4(1), step(len, 0.0));
}

现在只需绘制 1 个像素并使用 readPixels 读取它。如果为 0,则纹理相同。如果不是,它们是不同的。

代码假定纹理大小相同,但当然,如果它们大小不同,那么我们已经知道它们不可能相同。

// make 3 canvaes as sources for textures
const canvases = ['A', 'B', 'B'].map((msg) => {
  const canvas = document.createElement('canvas');
  canvas.width = 128;
  canvas.height = 128;
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'blue';
  ctx.fillRect(0, 0, 128, 128);
  ctx.font = '80px monospace';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillStyle = 'yellow';
  ctx.fillText(msg, 64, 64);
  document.body.appendChild(canvas);
  return canvas;
});

const gl = document.createElement('canvas').getContext('webgl2');
if (!gl) { alert('need webgl2'); }

const vs = `#version 300 es
void main() {
  gl_PointSize = 1.0;
  gl_Position = vec4(0, 0, 0, 1);
}
`;

const fs = `#version 300 es
precision highp float;

uniform sampler2D tex1;
uniform sampler2D tex2;

out vec4 outColor;

void main() {
  ivec2 size = textureSize(tex1, 0);  // size of mip 0
  float len = 0.0;
  for (int y = 0; y < size.y; ++y) {
    for (int x = 0; x < size.x; ++x) {
      vec4 color1 = texelFetch(tex1, ivec2(x, y), 0);
      vec4 color2 = texelFetch(tex2, ivec2(x, y), 0);
      vec4 diff = color1 - color2;
      len = length(diff);
      if (len > 0.0) break;
    }
    if (len > 0.0) break;
  }
  outColor = mix(vec4(0), vec4(1), step(len, 0.0));
}
`;

// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

const textures = canvases.map((canvas) => {
  // gl.createTexture, gl.bindTexture, gl.texImage, etc.
  return twgl.createTexture(gl, {src: canvas});
});

compareTextures(0, 1);
compareTextures(1, 2);

function compareTextures(ndx1, ndx2) {
  gl.useProgram(programInfo.program);
  
  // gl.activeTexture, gl.bindTexture, gl.uniform
  twgl.setUniforms(programInfo, {
    tex1: textures[ndx1],
    tex2: textures[ndx2],
  });
  
  // draw the bottom right pixel
  gl.viewport(0, 0, 1, 1);
  
  gl.drawArrays(gl.POINTS, 0, 1);  // draw 1 point
  
  // read the pixel
  const result = new Uint8Array(4);
  gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, result);
  
  console.log('textures', ndx1, 'and', ndx2, 'are', result[0] ? 'the same' : 'not the same'); 
}
canvas { padding: 5px; }
&lt;script src="https://twgljs.org/dist/4.x/twgl.min.js"&gt;&lt;/script&gt;

您也可以使用遮挡查询。优点是它们可能不会像 readPixels 那样阻塞 GPU。缺点是您无法在同一个 JavaScript 事件中检查它们,因此它们可能不符合您的需求

// make 3 canvaes as sources for textures
const canvases = ['A', 'B', 'B'].map((msg) => {
  const canvas = document.createElement('canvas');
  canvas.width = 128;
  canvas.height = 128;
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = 'blue';
  ctx.fillRect(0, 0, 128, 128);
  ctx.font = '80px monospace';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillStyle = 'yellow';
  ctx.fillText(msg, 64, 64);
  document.body.appendChild(canvas);
  return canvas;
});

const gl = document.createElement('canvas').getContext('webgl2');
if (!gl) { alert('need webgl2'); }

const vs = `#version 300 es
void main() {
  gl_PointSize = 1.0;
  gl_Position = vec4(0, 0, 0, 1);
}
`;

const fs = `#version 300 es
precision highp float;

uniform sampler2D tex1;
uniform sampler2D tex2;

out vec4 outColor;

void main() {
  ivec2 size = textureSize(tex1, 0);  // size of mip 0
  float len = 0.0;
  for (int y = 0; y < size.y; ++y) {
    for (int x = 0; x < size.x; ++x) {
      vec4 color1 = texelFetch(tex1, ivec2(x, y), 0);
      vec4 color2 = texelFetch(tex2, ivec2(x, y), 0);
      vec4 diff = color1 - color2;
      len = length(diff);
      if (len > 0.0) break;
    }
    if (len > 0.0) break;
  }
  if (len > 0.0) {
    discard;
  }
  outColor = vec4(1);
}
`;

// compile shaders, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

const textures = canvases.map((canvas) => {
  // gl.createTexture, gl.bindTexture, gl.texImage, etc.
  return twgl.createTexture(gl, {src: canvas});
});

function wait(ms = 0) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function test() {
  await compareTextures(0, 1);
  await compareTextures(1, 2);
}
test();

async function compareTextures(ndx1, ndx2) {
  gl.clear(gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);
  gl.useProgram(programInfo.program);
  
  // gl.activeTexture, gl.bindTexture, gl.uniform
  twgl.setUniforms(programInfo, {
    tex1: textures[ndx1],
    tex2: textures[ndx2],
  });
  
  // draw the bottom right pixel
  gl.viewport(0, 0, 1, 1);
  
  const query = gl.createQuery();
  gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);
  gl.drawArrays(gl.POINTS, 0, 1);  // draw 1 point
  gl.endQuery(gl.ANY_SAMPLES_PASSED);
  gl.flush();
  
  let ready = false;
  while(!ready) {
    await wait();
    ready = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
  }
  
  const same = gl.getQueryParameter(query, gl.QUERY_RESULT);
  
  console.log('textures', ndx1, 'and', ndx2, 'are', same ? 'the same' : 'not the same'); 
}
canvas { padding: 5px; }
&lt;script src="https://twgljs.org/dist/4.x/twgl.min.js"&gt;&lt;/script&gt;

【讨论】:

  • 我尝试了这两种方法,并且都运行得很好而且很快。尚未决定保留哪一个:1)使用 gl.readPixels 读取两个纹理并在 JavaScript 中进行比较或 2)在着色器中比较纹理并在一个像素中绘制结果。我使用 512 x 512 gl.RED 纹理。顺便说一句,我在许多纹理格式上测试了 gl.readPixels,它似乎不适用于 RGB 纹理。我现在不需要 RGB 纹理,但是在未来的项目中使用比较纹理功能真是太好了。
  • WebGL2 (OpenGL ES 3.0) 仅保证您可以使用 3 种格式/类型对调用 gl.readPixelsgl.RGBA/gl.UNSIGNED_BYTEgl.RGBA_INTEGER/gl.INTgl.RGBA_INTEGER/gl.UNSIGNED_INT。因此,要读取 RGB/UNSIGNED_BYTE 纹理,如果您想要便携,则需要将gl.RGBA/gl.UNSIGNED_BYTE 与 readPixels 一起使用。实现可以为每个 internalFormat 定义一种其他格式/类型组合,您可以将其与 readPixels 一起使用,您可以查询它,但它返回的格式/类型组合是特定于实现的。
  • 我重新提出了问题并提供了更多细节。使用 gl.readPixels 读取两个纹理并在 JavaScript 中比较它们似乎工作得更快。但即便如此,这是更昂贵的操作,如果在每次迭代中都完成,它的速度就会太慢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多