Для контекста этот вопрос следовал за этим .
Цель этого шейдера - иметь алгоритм предсказуемого изменения размера изображения, чтобы я знал, если полученное изображение из webgl
сторону можно сравнить с изображениями со стороны сервера в контексте перцептивных хэшей.
Я использую эту библиотеку метод для изменения размера на стороне сервера, и я пытаюсьреплицируйте его с помощью шейдеров, используя поиск по текстуре.
Я пытался реализовать базовую версию (используя ядро 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
Iиспользовал третий метод для изменения размера изображения (с использованием программного обеспечения для обработки изображений), и его результаты были идентичны эталонному изображению.В моем случае использования на экране ничего не отображается, когда данные изображения импортируются как необработанный массив Uint8Array, но я подготовил фрагмент, используя холст, чтобы сделать его более визуальным.
В любом случае, во фрагменте и в моем внутреннем сценарии использования результаты не совпадают с эталонным, и различия являются "значительными".Если вы сравните два изображения, версия webgl
определенно будет более размытой, чем эталонная (в обоих направлениях), края будут более четко определены в эталоне.Более вероятная причина заключается в том, что webgl
«поле» определено более свободно и захватывает слишком много пикселей текстуры.
Возможно, мне следовало сформулировать вопрос более целенаправленно.Я хочу быть уверен, что шейдер работает должным образом, прежде чем рассматривать ошибки с плавающей запятой и реализации форматов, даже больше, когда я не очень уверен в своем отображении текстуры.
Как преобразовать координату текстуры из 0..1, для поиска текстур, особенно когда width/newWidth
не кратны друг другу?Когда фрагментный шейдер получает координату текстуры от вершинного шейдера, соответствует ли он центроиду визуализированного пикселя или что-то еще?
Должен ли я использовать gl_FragCoord
в качестве контрольной точки вместо координат текстуры?(Я попытался использовать texFetch
, как предложено, но я не знаю, как сделать связь с выходом текстурных координат / вершинного шейдера.)