Изменение размера изображения с большим коэффициентом масштабирования - PullRequest
0 голосов
/ 22 января 2019

Для контекста этот вопрос следовал за этим .

Цель этого шейдера - иметь алгоритм предсказуемого изменения размера изображения, чтобы я знал, если полученное изображение из 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, как предложено, но я не знаю, как сделать связь с выходом текстурных координат / вершинного шейдера.)

1 Ответ

0 голосов
/ 22 января 2019

Я не слишком много смотрел на код, но есть несколько мест, которые могут сломаться.

WebGL по умолчанию использует сглаженный холст

, вам нужно отключить его, передав {antialias: false} в getContext, как в

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

Другими словами, ваш рисунок больше пикселейчем вы думаете, и WebGL сокращает их с помощью встроенного сглаживания OpenGL.В этом случае результаты могут быть такими же, но вам, вероятно, следует отключить эту функцию.

Загрузчик RUST может применять цветовое пространство PNG

Формат файла PNG имеет настройки цветового пространства.Применяют ли эти настройки загрузчики и как они применяются, у каждого загрузчика разные, поэтому, другими словами, вам нужно проверить код ржавчины.Существует несколько небольших PNG с экстремальными настройками цветового пространства / цветового профиля, на которые ссылается этот тест

Браузер может применять цветовое пространство PNG

Следующее место, где он может разбитьсяесли браузер применяет как цветовую коррекцию монитора, так и цветовое пространство из файла

Для WebGL вы можете отключить это приложение из любого цветового пространства, установив gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE) перед загрузкой изображения в качестве текстуры.

К сожалению, такой настройки нет при использовании изображений в Canvas 2D, поэтому вместо получения данных сравнения путем рисования изображения в 2D-холсте и вызова getImageData вам может потребоваться найти другой способ.Одним из способов было бы загрузить изображение сравнения в WebGL (после установки вышеуказанного параметра), отрендерить его и прочитать обратно, используя gl.readPixels

Canvas2d использует предварительно умноженную альфа

В другом месте оно может сломатьсяно я предполагаю, что здесь неважно, что Canvas 2d использует предварительно умноженную альфа, что означает, что если какая-либо альфа в изображении не 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результаты.

Мы надеемся, что это избавит от проблем с точностью без знака -> float -> unsigned byte.

Полагаю, если вы посмотрите на код ржавчины, он, вероятно, сделает всю работу.в целочисленном пространстве?

...