Обработка изображений

На этот раз мы поработаем с изображением средствами WebGL. Само собой, изображение будет представлено текстурой, которую мы наложим на простой квадрат. Технику наложения текстур я взял из шестого урока практически без изменений, поэтому на ней останавливаться не будем и сосредоточимся на работе с изображениями.

Для того, чтобы иметь возможность загружать произвольные изображения (не только квадратные и не только размером равным степени двойки), в инициализацию текстур добавлен следующий фрагмент кода.


        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);

Далее через varying-переменную vTextureCoord передаем из вершинного шейдера во фрагментный шейдер текстурные координаты:


    attribute vec3 aVertexPosition;
    attribute vec2 aTextureCoord;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    varying vec2 vTextureCoord;

    void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vTextureCoord = aTextureCoord;
    }

… и во фрагментном шейдере используем эти координаты для получения цвета:


    precision mediump float;

    varying vec2 vTextureCoord;

    uniform sampler2D uSampler;

    void main(void) {
        gl_FragColor = texture2D(uSampler, vTextureCoord);
    }

В итоге мы получаем вот такое изображение на странице:

Толстая серая обводка вокруг изображения — это вовсе не рамка. Это просто цвет сцены, так как мы отдалились немного дальше, чем нужно, и квадрат с текстурой не заполняет собой весь canvas.

Итак, мы получили изображение на веб-странице. Немного сложный способ, если сравнивать с тегом img, не находите? Но посмотрите, какие возможности он для нас открывает. Например, мы можем увеличить яркость:

Для этого нам достаточно лишь немного изменить код фрагментного шейдера:


        vec4 brightness = vec4(0.5, 0.5, 0.5, 0);
        gl_FragColor = texture2D(uSampler, vTextureCoord) + brightness;

Еще мы запросто можем поменять местами красный и зеленый цвет:

Фрагментный шейдер в этом случае будет выглядеть следующим образом:


    gl_FragColor = texture2D(uSampler, vTextureCoord).grba;

Ну и раз уж мы заговорили за обработку изображений, то нельзя пройти мимо фильтров. Идея фильтров в том, что для генерации итогового пикселя берется исходный пиксель и несколько пикселей по соседству, а затем все они объединяются по определенному правилу. Для начала рассмотрим один из самых простых фильтров — фильтр размытия (Blur). Матрица для него выглядит следующим образом:

Она означает, что нужно взять текущий пиксель (единичка в середине матрицы), затем сложить его с правым, левым, нижним и верхним пикселем и результат разделить на 5. Как же нам получить значения соседних пикселей во фрагментном шейдере? Очень просто! Допустим, что у нас есть картинка 200х200 пикселей. А текстурные координаты принимают значения в диапазоне от 0 до 1, в которые помещаются все 200 пикселей изображения. А поэтому для получения следующего пикселя нам нужно прибавить 1/200 к текущему значению текстурных координат. Для реализации нам необходимо внести два изменения. Во-первых, при инициализации текстуры нужно передать текущий размер изображения во фрагментный шейдер:


    var textureSizeLocation = gl.getUniformLocation(shaderProgram, "uTextureSize");
    gl.uniform2f(textureSizeLocation, texture.image.width, texture.image.height);

Во-вторых, переписать код шейдера, чтобы он учитывал соседние пиксели:


    precision mediump float;

    varying vec2 vTextureCoord;

    uniform vec2 uTextureSize;

    uniform sampler2D uSampler;

    void main(void) {
        vec2 pixelSize = vec2(1.0, 1.0) / uTextureSize;
        gl_FragColor = (
            texture2D(uSampler, vTextureCoord) +
            texture2D(uSampler, vTextureCoord + vec2(pixelSize.x, 0.0)) +
            texture2D(uSampler, vTextureCoord + vec2(-pixelSize.x, 0.0)) +
            texture2D(uSampler, vTextureCoord + vec2(0.0, pixelSize.y)) +
            texture2D(uSampler, vTextureCoord + vec2(0.0, -pixelSize.y))
        ) / 5.0;
    }

Здесь pixelSize — это смещение на один пиксель в пересчете на текстурные координаты. Как видите, к значению текущего пикселя мы прибавляем два соседних значения по X и два соседних значения по Y, а затем делим результат на 5 — то есть все в соответствии с приведенной выше матрицей. В результате у нас получится следующее изображение (справа показан оригинал, чтобы было хорошо видно разницу):

Наш фильтр работает — это отлично! Но у этого кода есть проблема. Она заключается в том, что шейдер ориентирован только на один фильтр. Для еще одного фильтра нам понадобится еще один шейдер. Куча шейдеров нам не нужна, поэтому мы будем передавать в шейдер матрицу коэффициентов и делитель.

Для следующего примера мы возьмем уже другой фильтр — выделение границ (Edge detection). Матрицу для него возьмем следующую:

Итак, вершинный шейдер:


    precision mediump float;

    varying vec2 vTextureCoord;

    uniform vec2 uTextureSize;
    
    uniform float uKernel[9];
    uniform float uKernelWeight;

    uniform sampler2D uSampler;

    void main(void) {
        vec2 pixelSize = vec2(1.0, 1.0) / uTextureSize;
        vec4 colorSum =
            texture2D(uSampler, vTextureCoord + pixelSize * vec2(-1, -1)) * uKernel[0] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 0, -1)) * uKernel[1] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 1, -1)) * uKernel[2] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2(-1,  0)) * uKernel[3] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 0,  0)) * uKernel[4] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 1,  0)) * uKernel[5] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2(-1,  1)) * uKernel[6] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 0,  1)) * uKernel[7] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 1,  1)) * uKernel[8] ;

        gl_FragColor = colorSum / uKernelWeight;
    }

У нас появилось две uniform-переменных: uKernel, которая представляет собой рассмотренную немного выше матрицу, и uKernelWeight — делитель. Делитель, в отличие от фильтра размытия, в этом случае будет равен единице. В colorSum записывается результат от сложения текущего пикселя и всех пикселей-соседей, а затем результат делится на соответствующий коэффициент. Осталось лишь передать все данные из JavaScript:


    var kernelLocation = gl.getUniformLocation(shaderProgram, "uKernel[0]");
    var edgeDetectKernel = [
        0, 1, 0,
        1, -4, 1,
        0, 1, 0
    ];
    gl.uniform1fv(kernelLocation, edgeDetectKernel);
    
    var kernelWeightLocation = gl.getUniformLocation(shaderProgram, "uKernelWeight");
    gl.uniform1f(kernelWeightLocation, 1.0);

С помощью uniform1fv мы передаем массив чисел с плавающей точкой в uniform-переменную uKernel, а через uniform1f — одно число с плавающей точкой в uKernelWeight.

Готово! После этого можно смотреть результат в браузере. Но увидим мы, вопреки ожиданиям, белый квадрат вообще без изображения. Разгадка такой неожиданности кроется в том, что в вычислении участвует альфа-канал. Исключим его влияние и установим его значение в единицу, независимо от результатов вычислений:


    precision mediump float;

    varying vec2 vTextureCoord;

    uniform vec2 uTextureSize;
    
    uniform float uKernel[9];
    uniform float uKernelWeight;

    uniform sampler2D uSampler;

    void main(void) {
        vec2 pixelSize = vec2(1.0, 1.0) / uTextureSize;
        vec4 colorSum =
            texture2D(uSampler, vTextureCoord + pixelSize * vec2(-1, -1)) * uKernel[0] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 0, -1)) * uKernel[1] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 1, -1)) * uKernel[2] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2(-1,  0)) * uKernel[3] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 0,  0)) * uKernel[4] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 1,  0)) * uKernel[5] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2(-1,  1)) * uKernel[6] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 0,  1)) * uKernel[7] +
            texture2D(uSampler, vTextureCoord + pixelSize * vec2( 1,  1)) * uKernel[8] ;

        gl_FragColor = vec4((colorSum / uKernelWeight).rgb, 1.0);
    }

И теперь мы получаем наше долгожданное изображение:

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

P.S. На написание данной статьи меня вдохновил этот пост. Также рекомендую посмотреть на более продвинутую работу с изображениями.

Обработка изображений
Метки:

2 thoughts on “Обработка изображений

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *