使用大缩放比例调整图像大小

时间:2019-01-22 00:01:24

标签: webgl opengl-es-2.0 webgl2

对于上下文,此问题紧跟this one

此着色器的目的是要具有可预测的图像大小调整算法,以便在感知哈希的情况下,是否可以将webgl端的结果图像与服务器端的图像进行比较

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

我一直在尝试实现基本版本(使用库中的Nearest/Box内核),包括将输入图像分成多个框,然后对所有包含像素的像素求平均,所有像素均具有相同的权重。

我已经附上了工作程序的片段,其中显示了其结果(左)与参考图像(右)一起显示。即使缩放似乎可行,参考照片(从库中计算得出)与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导入的用例中,屏幕上没有显示任何内容,但是我使用画布准备了代码段以使其更加可视。

在这两种情况下,无论是在代码段中还是在我的内部用例中,结果都与参考值不匹配,并且差异是“显着的”。如果比较两个图像,webgl版本肯定比参考图像模糊(在两个方向上),边缘在参考图像中的定义更加清晰。更有可能的原因是webgl“框”的定义比较宽松,并且捕获了太多的纹理像素。

我应该以更有针对性的方式来构想这个问题。在考虑浮点错误和格式实现之前,我想确保着色器能够正常运行,甚至在我对纹理映射不太确定的时候更是如此。

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

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

1 个答案:

答案 0 :(得分:0)

我对代码的关注并不多,但是有很多地方可能会中断。

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)来关闭任何色彩空间应用程序。

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

读出

Canvas2d使用预乘alpha

它可能会折断的另一个地方,但我想这里不相关的是canvas 2d使用预乘alpha,这意味着如果图像中的任何alpha不为255,则渲染到2D canvas都会有损。

您可能会考虑使用硬编码测试而不是图像来代替进行所有工作。这样,您可以暂时避免颜色空间问题,只需确保着色器正常工作即可。制作一个图像数据的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代码,它是否可能在整数空间中完成所有工作?