OpenGL.Shader: Zhige le enseña a escribir un cliente de filtro en vivo (11) Filtro visual: optimización del filtro gaussiano, operación de reducción de dimensionalidad de convolución

OpenGL.Shader: Zhige te enseña a escribir un cliente de filtro en vivo (11)

 

1. La necesidad de optimización

¿Por qué volver a hablar de filtrado gaussiano? Debido a que el filtrado gaussiano se usa ampliamente en la práctica, y se puede usar un núcleo de convolución relativamente grande (por ejemplo, [5x5] [7x7] [9x9], tenga en cuenta que todos tienen tamaños impares). En este momento, si se vuelve a utilizar la implementación del filtro gaussiano simple introducida anteriormente, la memoria de la GPU aumentará y el rendimiento del programa no será satisfactorio en este momento. Este es el fondo del artículo, ¿cómo optimizarlo? El uso de la separabilidad convolucional es una idea para resolver este problema. Cooperar con la variable de combinación de múltiples sombreadores de OpenGL.Shader puede realizar la optimización de manera efectiva.

A continuación, introduzcamos brevemente qué es la separabilidad convolucional. Suponiendo que A es un vector de columna y B es un vector de fila, entonces A * B = B * A. Por ejemplo, cuando n = 3, como se muestra en la siguiente figura:

    

De acuerdo con la teoría anterior, el kernel gaussiano Kernel2D mencionado anteriormente puede entenderse como KernelX · KernelY, entonces el proceso de convolución se puede expresar como: Dst = Src * Kernel2D = (Src * KernelX) * KernelY = (Src * KernelY) * KernelX. En términos generales, no importa qué tipo de filtrado de convolución, siempre que el núcleo de convolución se pueda desensamblar en el producto de dos vectores de fila y vectores de columna, entonces la convolución es separable. Este proceso también puede entenderse como la reducción del núcleo de convolución bidimensional a un procesamiento unidimensional.

 

2. Utilice FBO para lograr la reducción de dimensionalidad del núcleo de convolución

Todo el mundo comprende la verdad, entonces, ¿cómo realizar su lógica de optimización de reducción de dimensionalidad? Podemos usar el renderizado fuera de pantalla de FBO, primero procesar la parte lógica de Src * KernelX, guardar el resultado en FBO1 y luego usar FBO1 como entrada y KernelY para calcular la salida final nuevamente. GPUImage también implementa otras optimizaciones de convolución complejas y algunas operaciones de fusión de filtros a través de esta idea, que imita la clase de implementación central GpuBaseFilterGroup.hpp, el código es el siguiente:

#ifndef GPU_FILTER_GROUP_HPP
#define GPU_FILTER_GROUP_HPP

#include <list>
#include <vector>
#include "GpuBaseFilter.hpp"

class GpuBaseFilterGroup : public GpuBaseFilter {
    // GpuBaseFilter virtual method
public:
    GpuBaseFilterGroup()
    {
        mFBO_IDs = NULL;
        mFBO_TextureIDs = NULL;
    }

    virtual void onOutputSizeChanged(int width, int height) {
        if (mFilterList.empty()) return;

        destroyFrameBufferObjs();

        std::vector<GpuBaseFilter>::iterator itr;
        for(itr=mFilterList.begin(); itr!=mFilterList.end(); itr++)
        {
            GpuBaseFilter filter = *itr;
            filter.onOutputSizeChanged(width, height);
        }

        createFrameBufferObjs(width, height);
    }

    virtual void destroy() {
        destroyFrameBufferObjs();

        std::vector<GpuBaseFilter>::iterator itr;
        for(itr=mFilterList.begin(); itr!=mFilterList.end(); itr++)
        {
            GpuBaseFilter filter = *itr;
            filter.destroy();
        }
        mFilterList.clear();

        GpuBaseFilter::destroy();
    }


private:
    void createFrameBufferObjs(int _width, int _height ) {
        const int num = mFilterList.size() -1;
        // 最后一次draw是在显示屏幕上
        mFBO_IDs = new GLuint[num];
        mFBO_TextureIDs = new GLuint[num];
        glGenFramebuffers(num, mFBO_IDs);
        glGenTextures(num, mFBO_TextureIDs);

        for (int i = 0; i < num; i++) {
            glBindTexture(GL_TEXTURE_2D, mFBO_TextureIDs[i]);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // GL_REPEAT
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // GL_REPEAT
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, _width, _height, 0, GL_RGBA, GL_FLOAT, 0);

            glBindFramebuffer(GL_FRAMEBUFFER, mFBO_IDs[i]);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBO_TextureIDs[i], 0);

            glBindTexture(GL_TEXTURE_2D, 0);
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
        }
    }

    void destroyFrameBufferObjs() {
        if (mFBO_TextureIDs != NULL) {
            glDeleteTextures(length(mFBO_TextureIDs), mFBO_TextureIDs);
            delete[] mFBO_TextureIDs;
            mFBO_TextureIDs = NULL;
        }
        if (mFBO_IDs != NULL) {
            glDeleteFramebuffers(length(mFBO_IDs), mFBO_IDs);
            delete[] mFBO_IDs;
            mFBO_IDs = NULL;
        }
    }


    inline int length(GLuint arr[]) {
        return sizeof(arr) / sizeof(arr[0]);
    }


public:
    std::vector<GpuBaseFilter> mFilterList;
    void addFilter(GpuBaseFilter filter) {
        mFilterList.push_back(filter);
    }

    GLuint* mFBO_IDs;
    GLuint* mFBO_TextureIDs;
};
#endif // GPU_FILTER_GROUP_HPP

El código es claro y fácil de entender, aunque hereda GpuBaseFilter, de hecho es compatible con la referencia de clase padre anterior. Luego sobrescriba este método básico. La clave es que en onOutputSizeChanged, lo tomamos por separado:

virtual void onOutputSizeChanged (int width, int height) {     if (mFilterList.empty ()) return;     // Comprueba si la lista de filtros está vacía, no hay necesidad de continuar si está vacía     destroyFrameBufferObjs ();     // Destruye el futuro fbo y su enlace Lista de caché de texturas     std :: vector <GpuBaseFilter> :: iterator itr;     for (itr = mFilterList.begin (); itr! = MFilterList.end (); itr ++)     {         GpuBaseFilter filter = * itr;         filter.onOutputSizeChanged (ancho, height);     }     // activar el método de cambio de tamaño de todos los filtros     createFrameBufferObjs (width, height);     // crear el fbo correspondiente y su textura de enlace }













El siguiente es el método privado createFrameBufferObjs. Para el código específico, consulte lo anterior. Tenga en cuenta que el número de creaciones es mFilterList.size () -1, porque la imagen de salida final de la última vez se muestra en la pantalla.

 

Tres, GpuGaussianBlurFilter2

El último y más complicado paso es transformar la realización de nuestro filtro gaussiano. Mira el código de color primero

atributo vec4 posición;
atributo vec4 inputTextureCoordinate;
const int GAUSSIAN_SAMPLES = 9;
widthFactor de flotación uniforme;
heightFactor de flotación uniforme;
Variando vec2 blurCoordinates [GAUSSIAN_SAMPLES];
void main ()
{     gl_Position = posición;     vec2 singleStepOffset = vec2 (widthFactor, heightFactor);     int multiplicador = 0;     vec2 blurStep;     para (int i = 0; i <GAUSSIAN_SAMPLES; i ++)     {         multiplicador = (i - ((GAUSSIAN_SAMPLES - 1) / 2));         // - 4 , -3 , -2 , -1,0,1,2,3,4         blurStep = float (multiplicador) * singleStepOffset;         blurCoordinates [i] = inputTextureCoordinate.xy + blurStep;










    }
}

Tomando el vértice actual como centro, genere las posiciones de coordenadas de 9 puntos de muestreo, entre los cuales el primer código de lectura singleStepOffset puede tener preguntas. En el pasado, solíamos pasar tanto el factor de ancho como el factor de altura, de modo que la posición de los 9 puntos de muestreo se convierte en 45 ° Inclinar en diagonal. No lo explicaré aquí, lo explicaré en detalle a continuación.

Sampler2D uniforme SamplerY;
sampler2D SamplerU uniforme;
Sampler2D SamplerV uniforme;
Sampler2D uniforme SamplerRGB;
mat3 colorConversionMatrix = mat3 (
                   1.0, 1.0, 1.0,
                   0.0, -0.39465, 2.03211,
                   1.13983, -0.58060, 0.0);
vec3 yuv2rgb (vec2 pos)
{    vec3 yuv;    yuv.x = textura2D (SamplerY, pos) .r;    yuv.y = texture2D (SamplerU, pos) .r - 0.5;    yuv.z = texture2D (SamplerV, pos) .r - 0.5;    return colorConversionMatrix * yuv; } uniform int drawMode; // 0 为 YUV, 1 为 RGB  const int GAUSSIAN_SAMPLES = 9; Variando vec2 blurCoordinates [GAUSSIAN_SAMPLES];









void main ()
{     vec3 fragmentColor = vec3 (0.0);      if (drawMode == 0)      {         fragmentColor + = (yuv2rgb (blurCoordinates [0]) * 0.05);          fragmentColor + = (yuv2rgb (blurCoordinates [1]) * 0.09);          fragmentColor + = (yuv2rgb (blurCoordinates [2]) * 0.12);          fragmentColor + = (yuv2rgb (blurCoordinates [3]) * 0.15);          fragmentColor + = (yuv2rgb (blurCoordinates [4]) * 0.18);          fragmentColor + = (yuv2rgb (blurCoordinates [5]) * 0.15);          fragmentColor + = (yuv2rgb (blurCoordinates [6]) * 0.12);          fragmentColor + = (yuv2rgb (blurCoordinates [7]) * 0.09);          fragmentColor + = (yuv2rgb (blurCoordinates [8]) * 0.05); 












        gl_FragColor = vec4 (fragmentColor, 1.0);
    }
    else 
    { 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [0]). rgb * 0.05); 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [1]). rgb * 0.09); 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [2]). rgb * 0.12); 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [3]). rgb * 0.15); 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [4]). rgb * 0.18); 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [5]). rgb * 0.15); 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [6]). rgb * 0.12); 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [7]). rgb * 0.09); 
        fragmentColor + = (texture2D (SamplerRGB, blurCoordinates [8]). rgb * 0.05); 
        gl_FragColor = vec4 (fragmentColor, 1.0);
    } 
}

Parece muy complicado, yuv y rgb. De hecho, el diseño original de GpuBaseFilter es compatible con dos modos. Fui vago y no lo escribí todo. El contenido real es muy simple de escribir aquí. El valor del color de la textura se muestrea de acuerdo con 9 puntos de coordenadas, y luego se realiza la operación de convolución. También se deja el kernel de Gauss. Se simplifica a 9 coeficientes de ponderación. Cabe señalar que estos 9 coeficientes no se definen aleatoriamente, se generan según la fórmula general de Gauss, y todos han sido normalizados. La suma de los 9 coeficientes es igual a ¡Uno!

 

Entonces, ¿por qué no puedo ser perezoso y simplemente escribir un drawMode, cómo entiendo la inclinación de 45 ° de las coordenadas del vértice causada por el singleStepOffset del sombreador de vértices? Entonces sigue.

class GpuGaussianBlurFilter2 : public GpuBaseFilterGroup {

    GpuGaussianBlurFilter2()
    {
        GAUSSIAN_BLUR_VERTEX_SHADER = "...";
        GAUSSIAN_BLUR_FRAGMENT_SHADER = "..."; //上方代码
    }
    
    ~GpuGaussianBlurFilter2()
    {
        if(!GAUSSIAN_BLUR_VERTEX_SHADER.empty()) GAUSSIAN_BLUR_VERTEX_SHADER.clear();
        if(!GAUSSIAN_BLUR_FRAGMENT_SHADER.empty()) GAUSSIAN_BLUR_FRAGMENT_SHADER.clear();
    }

    void init() {
        GpuBaseFilter filter1;
        filter1.init(GAUSSIAN_BLUR_VERTEX_SHADER.c_str(), GAUSSIAN_BLUR_FRAGMENT_SHADER.c_str());
        mWidthFactorLocation1  = glGetUniformLocation(filter1.getProgram(), "widthFactor");
        mHeightFactorLocation1 = glGetUniformLocation(filter1.getProgram(), "heightFactor");
        mDrawModeLocation1     = glGetUniformLocation(filter1.getProgram(), "drawMode");
        addFilter(filter1);

        GpuBaseFilter filter2;
        filter2.init(GAUSSIAN_BLUR_VERTEX_SHADER.c_str(), GAUSSIAN_BLUR_FRAGMENT_SHADER.c_str());
        mWidthFactorLocation2  = glGetUniformLocation(filter2.getProgram(), "widthFactor");
        mHeightFactorLocation2 = glGetUniformLocation(filter2.getProgram(), "heightFactor");
        mDrawModeLocation2     = glGetUniformLocation(filter2.getProgram(), "drawMode");
        addFilter(filter2);
    }
    ... ...
}

Observe el método sin parámetros init () que sobrescribe la clase principal (GpuBaseFilter) de la clase principal (GpuBaseFilterGroup) para facilitar la gestión unificada y la referencia de código. El contenido no es difícil, es decir, crear dos objetos sombreadores, ambos usan el mismo conjunto de sombreadores, pero se distinguen las referencias del objeto sombreador.

class GpuGaussianBlurFilter2 : public GpuBaseFilterGroup {
    ... ... 接上
public:
    void onOutputSizeChanged(int width, int height) {
        GpuBaseFilterGroup::onOutputSizeChanged(width, height);
    }
    void setAdjustEffect(float percent) {
        mSampleOffset = range(percent * 100.0f, 0.0f, 2.0f);
    }
}

Luego, sobrescriba el método sin parámetros onOutputSizeChanged de la clase principal (GpuBaseFilter) de la clase principal (GpuBaseFilterGroup), no se requiere ningún procesamiento especial y la lógica de código de la clase principal GpuBaseFilterGroup se usa directamente (el contenido se muestra en la Tabla 2 anterior)

class GpuGaussianBlurFilter2 : public GpuBaseFilterGroup {
    ... ... 接上
public:
    void onDraw(GLuint SamplerY_texId, GLuint SamplerU_texId, GLuint SamplerV_texId,
                void* positionCords, void* textureCords)
    {
        if (mFilterList.size()==0) return;
        GLuint previousTexture = 0;
        int size = mFilterList.size();
        for (int i = 0; i < size; i++) {
            GpuBaseFilter filter = mFilterList[i];
            bool isNotLast = i < size - 1;
            if (isNotLast) {
                glBindFramebuffer(GL_FRAMEBUFFER, mFBO_IDs[i]);
            }
            glClearColor(0, 0, 0, 0);
            if (i == 0) {
                drawFilter1YUV(filter, SamplerY_texId, SamplerU_texId, SamplerV_texId, positionCords, textureCords);
            }
            if (i == 1) { //isNotLast=false, not bind FBO, draw on screen.
                drawFilter2RGB(filter, previousTexture, positionCords, mNormalTextureCords);
            }
            if (isNotLast) {
                glBindFramebuffer(GL_FRAMEBUFFER, 0);
                previousTexture = mFBO_TextureIDs[i];
            }
        }
    }
}

Vaya al método de representación de claves onDraw, que anula la clase Grandpa GpuBaseFilter, que es un método común para todas las interfaces de filtro. La lógica del código se simplifica con referencia a GPUImage. Um, no tengo mucho que decir, porque este método onDraw es una implementación específica de GpuGaussianBlurFilter2, y no es universal. Simplemente siga la lógica de implementación optimizada del filtro Gaussiano en (Catálogo 1).

Cuando i == 0, primero realice la renderización fuera de pantalla de src * kernelX. Ingrese para ver el contenido de drawFilter1YUV.

class GpuGaussianBlurFilter2 : public GpuBaseFilterGroup {
    ... ... 接上
private:
    void drawFilter1YUV(GpuBaseFilter filter,
                 GLuint SamplerY_texId, GLuint SamplerU_texId, GLuint SamplerV_texId,
                 void* positionCords, void* textureCords)
    {
        if (!filter.isInitialized())
            return;
        glUseProgram(filter.getProgram());
        glUniform1i(mDrawModeLocation1, 0);
        //glUniform1f(mSampleOffsetLocation1, mSampleOffset);
        glUniform1f(mWidthFactorLocation1, mSampleOffset / filter.mOutputWidth);
        glUniform1f(mHeightFactorLocation1, 0);

        glVertexAttribPointer(filter.mGLAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, positionCords);
        glEnableVertexAttribArray(filter.mGLAttribPosition);
        glVertexAttribPointer(filter.mGLAttribTextureCoordinate, 2, GL_FLOAT, GL_FALSE, 0, textureCords);
        glEnableVertexAttribArray(filter.mGLAttribTextureCoordinate);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, SamplerY_texId);
        glUniform1i(filter.mGLUniformSampleY, 0);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, SamplerU_texId);
        glUniform1i(filter.mGLUniformSampleU, 1);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, SamplerV_texId);
        glUniform1i(filter.mGLUniformSampleV, 2);

        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        glDisableVertexAttribArray(filter.mGLAttribPosition);
        glDisableVertexAttribArray(filter.mGLAttribTextureCoordinate);
        glBindTexture(GL_TEXTURE_2D, 0);
    }  
}

Preste atención, todos los índices de objetos de la aplicación Universal Shader son filtros entrantes especificados, y solo se procesan especialmente tres índices de objetos especiales. Cuando i == 0, primero realice la renderización fuera de pantalla de src * kernelX. Por primera vez, muestreamos imágenes de los datos de video originales yuv, así que drawMode = 0, es decir, modo YUV. Luego, widthFactor pasa SampleOffset / screen width como el factor de ancho del sombreador de vértices. ¡pero! ! ! heightFactor se pasa en 0! Es decir, no se realiza el desplazamiento vertical actual, por lo que el sombreador de vértices no tendrá un desplazamiento de muestreo escalonado de 45 ° En este momento, se completa la representación fuera de pantalla de src * kernalX.

 

Golpee mientras la plancha está caliente, cuando i == 1, esta vez es la última vez del bucle, no se necesita renderizado fuera de la pantalla y se envía directamente a la pantalla. Tenga en cuenta que previousTexture almacena en caché el ID de textura enlazado de i == 0 renderizado fuera de la pantalla, que lleva el resultado de renderizado de i == 0, que es src * kernelX. Tomamos esto como entrada y realizamos drawFilter2RGB

class GpuGaussianBlurFilter2 : public GpuBaseFilterGroup {
    ... ... 接上
private:
    void drawFilter2RGB(GpuBaseFilter filter, GLuint _texId, void* positionCords, void* textureCords)
    {
        if (!filter.isInitialized())
            return;
        glUseProgram(filter.getProgram());
        glUniform1i(mDrawModeLocation2, 1);
        //glUniform1f(mSampleOffsetLocation2, mSampleOffset);
        glUniform1f(mWidthFactorLocation2, 0);
        glUniform1f(mHeightFactorLocation2, mSampleOffset / filter.mOutputHeight);

        glVertexAttribPointer(filter.mGLAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, positionCords);
        glEnableVertexAttribArray(filter.mGLAttribPosition);

        glVertexAttribPointer(filter.mGLAttribTextureCoordinate, 2, GL_FLOAT, GL_FALSE, 0, textureCords);
        glEnableVertexAttribArray(filter.mGLAttribTextureCoordinate);

        glActiveTexture(GL_TEXTURE3);
        glBindTexture(GL_TEXTURE_2D, _texId);
        glUniform1i(filter.mGLUniformSampleRGB, 3);

        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        glDisableVertexAttribArray(filter.mGLAttribPosition);
        glDisableVertexAttribArray(filter.mGLAttribTextureCoordinate);
        glBindTexture(GL_TEXTURE_2D, 0);
    }
}

También son los tres índices de referencia de sombreadores especialmente procesados. En este momento, se pasa la textura rgb, drawMode usa el modo rgb 1, y luego esta vez widthFactor es 0, y heightFactor se pasa en mSampleOffset / screen height para completar el último paso (src * kernelX) * kernelY.

 

Cuatro, resumen

La prueba final puede estar en GpuFilterRender :: checkFilterChange, reemplace la referencia de GpuGaussianBlurFilter con GpuGaussianBlurFilter2. Puede comparar el efecto y encontrar que la implementación de 2 es más obvia que la de 1, porque GpuGaussianBlurFilter es solo un núcleo gaussiano 3x3 simple, y GpuGaussianBlurFilter2 es el resultado de 9x9. Aunque parece que la cantidad de cálculo es mayor que la de 2 a 1, pero mirando la situación de la memoria de la GPU, se reduce casi a la mitad y el rendimiento mejora significativamente.

Este artículo no solo trata sobre la optimización de la reducción de dimensionalidad del kernel de convolución, sino que también obtiene el método de renderizado jerárquico de múltiples sombreadores ¿Es posible considerar el efecto combinado de múltiples filtros? Sincronización de código con: https://github.com/MrZhaozhirong/NativeCppApp                       /src/main/cpp/gpufilter/filter/GpuGaussianBlurFilter2.hpp

Supongo que te gusta

Origin blog.csdn.net/a360940265a/article/details/107861956
Recomendado
Clasificación