【问题标题】:Image resize with large scaling factor使用大比例因子调整图像大小
【发布时间】:2019-01-22 20:55:33
【问题描述】:

关于上下文,这个问题跟在this one之后。

这个着色器的目的是有一个可预测的图像大小调整算法,这样我就知道在感知散列的上下文中,来自webgl 端的结果图像是否可以与来自服务器端的图像进行比较。

我正在使用这个库 method 在服务器端调整大小,并且我正在尝试使用纹理查找使用着色器复制它。

我一直在尝试实现基本版本(使用库中的Nearest/Box 内核),包括将输入图像划分为多个框,并对所有包含的像素进行平均,所有像素共享相同的权重。

我附上了一个工作程序的 sn-p,显示了它的结果(左)显示在参考图像(右)旁边。即使缩放看起来有效,参考照片(从库计算)和webgl 版本(查看右侧第 7 行)之间也存在显着差异。控制台记录像素值并计算不同像素的数量(注意:基础图像是灰度的)。

我猜这个错误来自于纹理查找,无论选择的纹素是否正确属于框,我对纹理坐标的位置以及它们如何与特定纹素相关感到有点困惑。例如,我添加了 0.5 偏移来定位纹素中心,但结果不匹配。

基本图片尺寸:341x256

目标尺寸:9x9(纵横比确实不一样)

(根据这些尺寸,可以猜出不同的盒子,并添加相应的纹理查找指令,这里一个盒子的尺寸是38x29)

const targetWidth = 9;
const targetHeight = 9;

let referencePixels, resizedPixels;

const baseImage = new Image();
baseImage.src = 'https://i.imgur.com/O6aW2Tg.png';
baseImage.crossOrigin = 'anonymous';
baseImage.onload = function() {
  render(baseImage);
};

const referenceCanvas = document.getElementById('reference-canvas');
const referenceImage = new Image();
referenceImage.src = 'https://i.imgur.com/s9Mrsjm.png';
referenceImage.crossOrigin = 'anonymous';
referenceImage.onload = function() {
  referenceCanvas.width = referenceImage.width;
  referenceCanvas.height = referenceImage.height;
  referenceCanvas
    .getContext('2d')
    .drawImage(
      referenceImage,
      0,
      0,
      referenceImage.width,
      referenceImage.height
    );
  referencePixels = referenceCanvas
    .getContext('2d')
    .getImageData(0, 0, targetWidth, targetHeight).data;
  if (resizedPixels !== undefined) {
    compare();
  }
};

const horizontalVertexShaderSource = `#version 300 es
precision mediump float;

in vec2 position;
out vec2 textureCoordinate;

void main() {
  textureCoordinate = vec2(1.0 - position.x, 1.0 - position.y);
  gl_Position = vec4((1.0 - 2.0 * position), 0, 1);
}`;

const horizontalFragmentShaderSource = `#version 300 es
precision mediump float;

uniform sampler2D inputTexture;
in vec2 textureCoordinate;
out vec4 fragColor;

void main() {
    vec2 texelSize = 1.0 / vec2(textureSize(inputTexture, 0));
    float sumWeight = 0.0;
    vec3 sum = vec3(0.0);

    float cursorTextureCoordinateX = 0.0;
    float cursorTextureCoordinateY = 0.0;
    float boundsFactor = 0.0;
    vec4 cursorPixel = vec4(0.0);

    // These values corresponds to the center of the texture pixels,
    // that are belong to the current "box",
    // here we need 38 pixels from the base image
    // to make one pixel on the resized version.
    ${[
      -18.5,
      -17.5,
      -16.5,
      -15.5,
      -14.5,
      -13.5,
      -12.5,
      -11.5,
      -10.5,
      -9.5,
      -8.5,
      -7.5,
      -6.5,
      -5.5,
      -4.5,
      -3.5,
      -2.5,
      -1.5,
      -0.5,
      0.5,
      1.5,
      2.5,
      3.5,
      4.5,
      5.5,
      6.5,
      7.5,
      8.5,
      9.5,
      10.5,
      11.5,
      12.5,
      13.5,
      14.5,
      15.5,
      16.5,
      17.5,
      18.5,
    ]
      .map(texelIndex => {
        return `
    cursorTextureCoordinateX = textureCoordinate.x + texelSize.x * ${texelIndex.toFixed(
      2
    )};
    cursorTextureCoordinateY = textureCoordinate.y;
    cursorPixel = texture(
        inputTexture,
        vec2(cursorTextureCoordinateX, cursorTextureCoordinateY)
    );
    // Whether this texel belongs to the texture or not.
    boundsFactor = 1.0 - step(0.51, abs(0.5 - cursorTextureCoordinateX));
    sum += boundsFactor * cursorPixel.rgb * 1.0;
    sumWeight += boundsFactor * 1.0;`;
      })
      .join('')}

    fragColor = vec4(sum / sumWeight, 1.0);
}`;

const verticalVertexShaderSource = `#version 300 es
precision mediump float;

in vec2 position;
out vec2 textureCoordinate;

void main() {
  textureCoordinate = vec2(1.0 - position.x, position.y);
  gl_Position = vec4((1.0 - 2.0 * position), 0, 1);
}`;

const verticalFragmentShaderSource = `#version 300 es
precision mediump float;

uniform sampler2D inputTexture;
in vec2 textureCoordinate;
out vec4 fragColor;

void main() {
    vec2 texelSize = 1.0 / vec2(textureSize(inputTexture, 0));
    float sumWeight = 0.0;
    vec3 sum = vec3(0.0);

    float cursorTextureCoordinateX = 0.0;
    float cursorTextureCoordinateY = 0.0;
    float boundsFactor = 0.0;
    vec4 cursorPixel = vec4(0.0);

    ${[
      -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14
    ]
      .map(texelIndex => {
        return `
    cursorTextureCoordinateX = textureCoordinate.x;
    cursorTextureCoordinateY = textureCoordinate.y + texelSize.y * ${texelIndex.toFixed(
      2
    )};
    cursorPixel = texture(
        inputTexture,
        vec2(cursorTextureCoordinateX, cursorTextureCoordinateY)
    );
    boundsFactor = 1.0 - step(0.51, abs(0.5 - cursorTextureCoordinateY));
    sum += boundsFactor * cursorPixel.rgb * 1.0;
    sumWeight += boundsFactor * 1.0;`;
      })
      .join('')}

  fragColor = vec4(sum / sumWeight, 1.0);
}`;

function render(image) {
  const canvas = document.getElementById('canvas');
  const gl = canvas.getContext('webgl2');
  if (!gl) {
    return;
  }

  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]),
    gl.STATIC_DRAW
  );
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  const horizontalProgram = webglUtils.createProgramFromSources(gl, [
    horizontalVertexShaderSource,
    horizontalFragmentShaderSource,
  ]);
  const horizontalPositionAttributeLocation = gl.getAttribLocation(
    horizontalProgram,
    'position'
  );
  const horizontalInputTextureUniformLocation = gl.getUniformLocation(
    horizontalProgram,
    'inputTexture'
  );
  const horizontalVao = gl.createVertexArray();
  gl.bindVertexArray(horizontalVao);
  gl.enableVertexAttribArray(horizontalPositionAttributeLocation);
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.vertexAttribPointer(
    horizontalPositionAttributeLocation,
    2,
    gl.FLOAT,
    false,
    0,
    0
  );
  gl.bindVertexArray(null);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  const verticalProgram = webglUtils.createProgramFromSources(gl, [
    verticalVertexShaderSource,
    verticalFragmentShaderSource,
  ]);
  const verticalPositionAttributeLocation = gl.getAttribLocation(
    verticalProgram,
    'position'
  );
  const verticalInputTextureUniformLocation = gl.getUniformLocation(
    verticalProgram,
    'inputTexture'
  );
  const verticalVao = gl.createVertexArray();
  gl.bindVertexArray(verticalVao);
  gl.enableVertexAttribArray(verticalPositionAttributeLocation);
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.vertexAttribPointer(
    verticalPositionAttributeLocation,
    2,
    gl.FLOAT,
    false,
    0,
    0
  );
  gl.bindVertexArray(null);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  const rawTexture = gl.createTexture();
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, rawTexture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  const horizontalTexture = gl.createTexture();
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D, horizontalTexture);
  gl.texImage2D(
    gl.TEXTURE_2D,
    0,
    gl.RGBA,
    targetWidth,
    image.height,
    0,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    null
  );
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  const framebuffer = gl.createFramebuffer();

  // Step 1: Draw horizontally-resized image to the horizontalTexture;
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  gl.framebufferTexture2D(
    gl.FRAMEBUFFER,
    gl.COLOR_ATTACHMENT0,
    gl.TEXTURE_2D,
    horizontalTexture,
    0
  );
  gl.viewport(0, 0, targetWidth, image.height);
  gl.clearColor(0, 0, 0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.useProgram(horizontalProgram);
  gl.uniform1i(horizontalInputTextureUniformLocation, 0);
  gl.bindVertexArray(horizontalVao);
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, rawTexture);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  gl.bindVertexArray(null);

  // Step 2: Draw vertically-resized image to canvas (from the horizontalTexture);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);

  gl.viewport(0, 0, targetWidth, targetHeight);
  gl.clearColor(0, 0, 0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.useProgram(verticalProgram);
  gl.uniform1i(verticalInputTextureUniformLocation, 1);
  gl.bindVertexArray(verticalVao);
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D, horizontalTexture);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  gl.bindVertexArray(null);

  const _resizedPixels = new Uint8Array(4 * targetWidth * targetHeight);
  gl.readPixels(
    0,
    0,
    targetWidth,
    targetHeight,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    _resizedPixels
  );
  resizedPixels = _resizedPixels;
  if (referencePixels !== undefined) {
    compare();
  }
}

function compare() {
  console.log('= Resized (webgl) =');
  console.log(resizedPixels);
  console.log('= Reference (rust library) =');
  console.log(referencePixels);

  let differenceCount = 0;
  for (
    let pixelIndex = 0;
    pixelIndex <= targetWidth * targetHeight;
    pixelIndex++
  ) {
    if (resizedPixels[4 * pixelIndex] !== referencePixels[4 * pixelIndex]) {
      differenceCount++;
    }
  }
  console.log(`Number of different pixels: ${differenceCount}`);
}
body {
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
}
<canvas id="canvas" width="9" height="9" style="transform: scale(20); margin: 100px;"></canvas>
<canvas id="reference-canvas" width="9" height="9" style="transform: scale(20); margin: 100px;"></canvas>
<script src="https://webgl2fundamentals.org/webgl/resources/webgl-utils.js"></script>

跟进@gman's answer

我使用第三种方法调整图像大小(使用图像处理软件),其结果与参考图像相同。 在我将图像数据作为原始 Uint8Array 导入的用例中,屏幕上没有显示任何内容,但我使用画布准备了 sn-p 以使其更直观。

在任何一种情况下,在 sn-p 和我的内部用例中,结果都与参考结果不匹配,并且差异是“显着的”。如果你比较这两张图片,webgl 版本肯定比参考的更模糊(在两个方向上),边缘在参考中更加清晰。更可能的原因是webgl“框”的定义比较松散,并且捕获了太多的纹理像素。

我可能应该以更有针对性的方式提出这个问题。在考虑浮点错误和格式实现之前,我想确保着色器正常运行,当我对纹理映射不是很有信心时更是如此。

如何将纹理坐标从 0..1 转换为纹理查找,尤其是当 width/newWidth 不是彼此的倍数时?当片段着色器从顶点着色器接收到一个纹理坐标时,它是对应于渲染像素的质心,还是其他什么?

我应该使用gl_FragCoord 作为参考点而不是纹理坐标吗? (我尝试按照建议使用texFetch,但我不知道如何与纹理坐标/顶点着色器输出建立链接。)

【问题讨论】:

  • 查看 rust 代码没有“框”过滤器。有最近的、三角形的、CatmullRom、Gaussian、Lanczos。如果您选择“最近”,那么按照代码,它只是在 src 中为 dest 中的每个像素选择一个像素。没有求和也没有平均
  • 你是对的,我的结果与参考图像非常相似(+/- 1 位),所以我猜你的回答是预告(因为我正在寻找浮点转换问题......甚至如果这对我来说足够接近)。
  • 我不知道这些是否有用。这是使用 texelFetch 和 unsigned int 纹理的 2 个示例:this one just implements a nearest filterthis one tries to implement the rust code for nearest。两者都得出相同的答案,6个像素不同。我不知道为什么。浮点数到整数的转换?如果我真的想让它发挥作用,我会选择一些 2 或 4 像素的较小测试,并且更苛刻且更容易推理颜色。无论如何,第二个可能暗示如何在 GLSL 中实现 rust 算法。
  • 感谢您的帮助,我得到了类似的结果(几个像素出现 1 位差异)。由于某种原因,我没有设法使用基于整数的纹理,所以我不得不手动转换浮点值以使用fragColor = floor(X / 255.0) * 255.0; 模拟 rust 库(它确实使用整数来存储像素数据),但这无论如何都不重要对于我的用例。

标签: webgl opengl-es-2.0 webgl2


【解决方案1】:

我没有看太多代码,但有几个地方可能会中断。

WebGL 默认使用抗锯齿画布

您需要通过将{antialias: false} 传递给getContext 来关闭它

const gl = someCanvas.getContext('webgl2', {antialias: false});

换句话说,您绘制的像素比您想象的要多,WebGL 使用 OpenGL 内置的抗锯齿功能将它们缩小。对于这种情况,结果可能相同,但您可能应该关闭该功能。

RUST 加载程序可能正在应用 PNG 颜色空间

PNG 文件格式具有色彩空间设置。加载器是否应用这些设置以及它们应用它们的确切方式对于每个加载器都是不同的,所以换句话说,您需要检查 rust 代码。 this test 引用了几个具有极端色彩空间/颜色配置文件设置的小型 PNG 进行测试

浏览器可能正在应用 PNG 颜色空间

它可能破坏的下一个地方是浏览器可能会同时应用显示器颜色校正和/或文件中的颜色空间

对于 WebGL,您可以通过在将图像上传为纹理之前设置 gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE) 来关闭任何色彩空间应用程序。

不幸的是,在 2D 画布中使用图像时没有这样的设置,因此您可能需要找到其他方法,而不是通过将图像绘制到 2D 画布并调用 getImageData 来获取比较数据。一种方法是将比较图像加载到 WebGL 中(在设置上述设置之后),渲染它并使用 gl.readPixels 读回

Canvas2d 使用预乘 alpha

另一个可能会损坏但我猜这里不相关的地方是 canvas 2d 使用预乘 alpha,这意味着如果图像中的任何 alpha 不是 255,那么渲染到 2D 画布是有损的。

您可能会考虑使用不使用图像的硬编码测试,而不是进行所有工作。这样您就可以暂时避免色彩空间问题,并确保着色器正常工作。制作一个 76x76 的图像数据数组,将其转换为 2x2。

其他

精度

使用highp 而不是mediump。这不会影响桌面设备上的任何内容,但会影响移动设备。

texelFetch

仅供参考,在 WebGL2 中,您可以使用 texelFetch(samplerUniform, ivec2(intPixelX, intPixelY), mipLevel) 读取单个纹理像素/纹素,这比为 texture(sampleUniform, normalizedTextureCoords) 操作归一化纹理坐标要容易得多

循环

我注意到您没有使用循环,而是生成代码。循环只要它们可以在编译时展开就应该可以工作,所以你可以这样做

for (int i = -17; i < 19; ++i) {
  sum += texelFetch(sampler, ivec2(intX + i, intY), 0);
}

在着色器生成时

for (int i = ${start}; i < ${end}; ++i) {

或类似的东西。这可能更容易推理?

浮点数转换问题

您将数据上传到gl.RGBA 纹理并将数据用作浮点数。可能会有精度损失。您可以改为将纹理上传为 gl.RGBA8UI(无符号 8 位纹理)

gl.texImage2D(target, level, gl.RGBA8UI, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, image)

然后在着色器中使用usampler2D 并将像素读取为无符号整数

uvec4 color = texelFetch(someUnsignedSampler2D, ivec2(px, py), 0);

并在着色器中使用无符号整数完成所有其余操作

您还可以创建gl.RGBA8UI 纹理并将其附加到帧缓冲区,这样您就可以将结果写为无符号整数,然后readPixels 结果。

这有望摆脱任何无符号字节 -> 浮点 -> 无符号字节精度问题。

我猜如果你看一下 rust 代码,它可能会在整数空间中完成所有工作?

【讨论】:

  • 我编辑了主要问题以更好地突出我的疑问,我认为结果的差异超出了实现限制(= 我的纹理映射可能是错误的)。
猜你喜欢
  • 2011-07-15
  • 2011-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-09
  • 1970-01-01
  • 1970-01-01
  • 2011-12-29
相关资源
最近更新 更多