【发布时间】: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>
我使用第三种方法调整图像大小(使用图像处理软件),其结果与参考图像相同。 在我将图像数据作为原始 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 filter 和 this 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