Реализация операции scatter на GPU на примере построения гистограмм.

Ряд операций по обработке данных можно разделить как операции типа gather и операции типа scatter.

Типичным примером операции типа gather является применение матричного фильтра к изображению - каждый результирующий пиксел (для сглаживания с ядром kij) будет задаваться следующей формулой:

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

Типичным примером операции scatter является построение гистограммы заданного изображения - весь диапазон возможных значений некоторого параметра (например, яркости) разбивается на ряд отрезков ("корзин", bins) и для каждого такого отрезка считается сколько исходных текселов попали в него.

Классическая реализация построения гистограмм на CPU инициализирует массив "корзин" и для каждого пиксела определяет номер соответствующей ему корзины и увеличивает счетчик по этому номеру (см. псевдокод ниже).

for pix in image:
    index       = colorToBinIndex ( pix )   # get bin index for a pixel
    bin [index] = bin [index] + 1           # increment bin's counter

Однако если операции первого типа (gather) очень легко и эффективно реализуются на GPU, то операции второго типа (scatter) обычно крайне плохо ложатся на архитектуру современных GPU, так как требуют доступа к памяти типа random write.

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

Для этого сопоставим каждому текселу исходного изображения одну точку. Массиву "корзин" (bins) сопоставим текстуру в которую мы будем осуществлять рендеринг.

После этого выведем каждую точку исходного изображения именно как точку. Воспользовавшись операций чтения из текстуры в вершинном шейдере (поддерживаемой начиная с GPU GeForce 6xxx) можно легко прочесть цвет соответствующей точки, определить индекс корзины и изменить координаты данной точки таким образом, чтобы при рендеринге она попадала точно в нужную корзину.

Теперь если задать в качестве режима вывода alpha-blinding с параметрами (GL_ONE, GL_ONE), то вывод каждой точки будет увеличивать значение в соответствующей корзине и в результате вывода всех точек мы получим в массиве корзин интересующие нас количества точек (может быть только отмасштабированные некоторым множителем).

Ниже приводится вершинный шейдер, как раз осуществляющий перенаправление точек по корзинам в зависимости от яркости соответствующего тексела текстуры.

//
// Fast GPU scatter via vertex texture fetch - vertex shader
//

uniform sampler2D   srcMap;             // map to compute histogram
uniform float       numBins;            // number of bins (in [0,1] segment]

void main(void)
{
    vec4    color = texture2D ( srcMap, gl_MultiTexCoord0.xy );     // get color
    float   lum   = dot       ( color.rgb, vec3 ( 0.3, 0.59, 0.11 ) );
    float   bin   = floor     ( lum * numBins );
    
    gl_Position     = gl_ModelViewProjectionMatrix * vec4 ( bin, 0.0, 0.0, 1.0 );
}

Соответствующий код на С++ приводится на листинге ниже.

#include    "libExt.h"

#ifdef  MACOSX
    #include    <GLUT/glut.h>
#else
    #include    <glut.h>
#endif

#include    <stdio.h>
#include    <stdlib.h>

#include    "libTexture.h"
#include    "Vector3D.h"
#include    "Vector2D.h"
#include    "Vector4D.h"
#include    "GlslProgram.h"
#include    "FrameBuffer.h"
#include    "utils.h"
#include    "fpTexture.h"

#define TEX_SIZE    256             // width & height of texture
#define NUM_BINS    128

GLenum      mapFormat;              // texture format used for vertex map
unsigned    srcMap;                 // map with source image

FrameBuffer buffer  ( NUM_BINS, 1 );
GlslProgram program;
float       res [NUM_BINS*4];

void reshape ( int w, int h );

void display ()
{
    float   h  = 1.0 / (float) TEX_SIZE;
    float   h2 = h * 0.5;
    
    startOrtho    ( buffer.getWidth (), buffer.getHeight () );
    glDisable     ( GL_DEPTH_TEST );
    glEnable      ( GL_BLEND );
    glBlendFunc   ( GL_ONE, GL_ONE );
    glBindTexture ( GL_TEXTURE_2D, srcMap );
    glPointSize   ( 1.0 );
    glDisable     ( GL_POINT_SMOOTH );
    buffer.bind   ();
    program.bind  ();
    
    glClear ( GL_COLOR_BUFFER_BIT );
    glBegin ( GL_POINTS );
    
    for ( int i = 0; i < TEX_SIZE; i++ )
        for ( int j = 0; j < TEX_SIZE; j++ )
        {
            glTexCoord2f ( i*h + h2, j*h + h2 );
            glVertex2f   ( 0, 0 );
        }
            
    glEnd ();
    
    program.unbind ();
    
    glBindTexture ( GL_TEXTURE_2D, 0 );
    glBindTexture ( GL_TEXTURE_RECTANGLE_ARB, buffer.getColorBuffer () );
    glReadPixels  ( 0, 0, NUM_BINS, 1, GL_RGBA, GL_FLOAT, res );
    glBindTexture ( GL_TEXTURE_RECTANGLE_ARB, 0 );

    buffer.unbind  ();
    
    endOrtho        ();
    glutSwapBuffers ();

    float   sum = 0;

    for ( int i = 0; i < NUM_BINS; i++ )
    {
        sum += res [4*i];

        printf ( "%4d ", (int)res [4*i] );
    }

    printf ( "\nTotal: %d pixels.\n", (int) sum );

    exit ( 0 );
}

void reshape ( int w, int h )
{
    glViewport     ( 0, 0, (GLsizei)w, (GLsizei)h );
    glMatrixMode   ( GL_PROJECTION );
    glLoadIdentity ();
    glMatrixMode   ( GL_MODELVIEW );
    glLoadIdentity ();
}

void key ( unsigned char key, int x, int y )
{
    if ( key == 27 || key == 'q' || key == 'Q' )    //  quit requested
        exit ( 0 );
}

int main ( int argc, char * argv [] )
{
                                // initialize glut
    glutInit            ( &argc, argv );
    glutInitDisplayMode ( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH );
    glutInitWindowSize  ( 512, 512 );

                                // create window
    glutCreateWindow ( "Fast GPU scatter via vertex texture fetch" );

                                // register handlers
    glutDisplayFunc  ( display );
    glutReshapeFunc  ( reshape );
    glutKeyboardFunc ( key     );

    init                      ();
    initExtensions            ();
    assertExtensionsSupported ( "ARB_pixel_buffer_object" );

    if ( (mapFormat = fpFormatWithPrecision ( 32 ) ) == 0 )
    {
        printf ( "Floating-point textures not supported !\n" );
        
        return 1;
    }
    
    if ( !FrameBuffer :: isSupported () )
    {
        printf ( "Fbo not supported\n" );
        
        return 1;
    }
    
    if ( !GlslProgram :: isSupported () )
    {
        printf ( "GLSL not supported\n" );
        
        return 1;
    }
    
    srcMap = createTexture2D ( false, "../../Textures/floor.bmp" );     //256*256 pixels
    
    if ( srcMap == 0 )
    {
        printf ( "Texture floor.bmp not found.\n" );
        
        return 1;
    }
    
    unsigned tempMap = buffer.createColorRectTexture ( GL_RGBA, mapFormat );
    
    buffer.create ();
    buffer.bind   ();
    if ( !buffer.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, tempMap ) )
        printf ( "buffer error with color attachment\n");

    if ( !buffer.isOk () )
        printf ( "Error with framebuffer\n" );
                    
    buffer.unbind ();

    if ( !program.loadShaders ( "scatter-1.vsh", "scatter-1.fsh" ) )
    {
        printf ( "Error loading shaders:\n%s\n", program.getLog ().c_str () );

        return 3;
    }
    
    program.bind            ();
    program.setTexture      ( "srcMap",  0 );
    program.setUniformFloat ( "numBins", NUM_BINS );
    program.unbind          ();
    
    glutMainLoop ();

    return 0;
}

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

После этого остается только вывести данный вершинный массив как набор точек, используя alpha-blinding.

Ниже приводятся соответствующие вершинный и фрагментный шейдеры, осуществляющие построение такого массива вершин.

//
// Fast GPU scatter via render-to-vertex-buffer - vertex shader
//

void main(void)
{
    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord [0] = gl_MultiTexCoord0;
}

//
// Fragment shader GPU scatter via vertex texture fetch - fragment shader
//

uniform sampler2D   srcMap;             // map to compute histogram
uniform float       numBins;            // number of bins (in [0,1] segment]

void    main ()
{
    vec3    clr = texture2D ( srcMap, gl_TexCoord [0].xy ).xyz;
    float   lum = dot       ( clr, vec3 ( 0.3, 0.59, 0.11 ) );
    float   bin = floor     ( lum * numBins );

    gl_FragColor = vec4 ( bin, 0.0, 0.0, 1.0 );
}

Ниже приводится фрагмент кода на С++, реализующий данный подход.

#include    "libExt.h"

#ifdef  MACOSX
    #include    <GLUT/glut.h>
#else
    #include    <glut.h>
#endif

#include    <stdio.h>
#include    <stdlib.h>

#include    "libTexture.h"
#include    "Vector3D.h"
#include    "Vector2D.h"
#include    "Vector4D.h"
#include    "GlslProgram.h"
#include    "FrameBuffer.h"
#include    "utils.h"
#include    "fpTexture.h"
#include    "VertexBuffer.h"

#define TEX_SIZE    256             // width & height of texture
#define NUM_BINS    128

GLenum      mapFormat;              // texture format used for vertex map
unsigned    srcMap;                 // map with source image

FrameBuffer    buffer  ( NUM_BINS, 1 );
FrameBuffer    buffer2 ( TEX_SIZE, TEX_SIZE );
GlslProgram    program;
GlslProgram    program2;
VertexBuffer * vertexBuffer = NULL;     // vertex coordinates

float       res [NUM_BINS*4];
float       res2[TEX_SIZE*TEX_SIZE*4];

void reshape ( int w, int h );

void    createVertexBuffer ()
{
    if ( vertexBuffer != NULL )
        return;

    vertexBuffer = new VertexBuffer ();
    
    vertexBuffer -> bind    ( GL_PIXEL_PACK_BUFFER_ARB );
    vertexBuffer -> setData ( buffer2.getWidth () * buffer2.getHeight () * 4 * sizeof ( float ), NULL, GL_DYNAMIC_DRAW );
    vertexBuffer -> unbind  ();
}

//
// create vertex coordinates using r2vb
//
void    buildVertices ()
{
    createVertexBuffer ();
                                                        // load source image map
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glBindTexture      ( GL_TEXTURE_2D, srcMap );

    program.bind   ();                                  // perform rendering to FBO texture
    buffer2.bind   ();                                  // render to vertex buffer
    startOrtho     ( TEX_SIZE, TEX_SIZE );
    drawQuad       ( TEX_SIZE, TEX_SIZE );
    endOrtho       ();
    program.unbind ();
                                                        // copy vertex data from texture to vertex buffer
    vertexBuffer -> bind   ( GL_PIXEL_PACK_BUFFER_ARB );
    glReadPixels           ( 0, 0, TEX_SIZE, TEX_SIZE, GL_RGBA, GL_FLOAT, NULL );
    buffer2.unbind         ();
    vertexBuffer -> getSubData ( 0, TEX_SIZE*TEX_SIZE*4*sizeof(float), res2 );
    vertexBuffer -> unbind ();
}

void display ()
{
    float   h  = 1.0 / (float) TEX_SIZE;
    float   h2 = h * 0.5;
    
    buildVertices ();
    
    startOrtho    ( buffer.getWidth (), buffer.getHeight () );
    glDisable     ( GL_DEPTH_TEST );
    glEnable      ( GL_BLEND );
    glBlendFunc   ( GL_ONE, GL_ONE );
    glPointSize   ( 1.0 );
    glDisable     ( GL_POINT_SMOOTH );
    
    glEnableClientState  ( GL_VERTEX_ARRAY );
    vertexBuffer -> bind ( GL_ARRAY_BUFFER );
    glVertexPointer      ( 4, GL_FLOAT, 0, NULL );
    
    program2.bind ();
    buffer.bind   ();

    glClear   ( GL_COLOR_BUFFER_BIT );
    
    glDrawArrays ( GL_POINTS, 0, TEX_SIZE*TEX_SIZE );
    
    glBindTexture ( GL_TEXTURE_2D, 0 );
    glBindTexture ( GL_TEXTURE_RECTANGLE_ARB, buffer.getColorBuffer () );
    glReadPixels  ( 0, 0, NUM_BINS, 1, GL_RGBA, GL_FLOAT, res );
    glBindTexture ( GL_TEXTURE_RECTANGLE_ARB, 0 );

    buffer.unbind   ();
    program2.unbind ();
    endOrtho        ();
    glutSwapBuffers ();

    float   sum = 0;

    for ( int i = 0; i < NUM_BINS; i++ )
    {
        sum += res [4*i];

        printf ( "%4d ", (int)res [4*i] );
    }

    printf ( "\nTotal: %d pixels.\n", (int) sum );

    exit ( 0 );
}

void reshape ( int w, int h )
{
    glViewport     ( 0, 0, (GLsizei)w, (GLsizei)h );
    glMatrixMode   ( GL_PROJECTION );
    glLoadIdentity ();
    glMatrixMode   ( GL_MODELVIEW );
    glLoadIdentity ();
}

void key ( unsigned char key, int x, int y )
{
    if ( key == 27 || key == 'q' || key == 'Q' )    //  quit requested
        exit ( 0 );
}

int main ( int argc, char * argv [] )
{
                                // initialize glut
    glutInit            ( &argc, argv );
    glutInitDisplayMode ( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH );
    glutInitWindowSize  ( 512, 512 );

                                // create window
    glutCreateWindow ( "Fast GPU scatter via r2vb" );

                                // register handlers
    glutDisplayFunc  ( display );
    glutReshapeFunc  ( reshape );
    glutKeyboardFunc ( key     );

    init                      ();
    initExtensions            ();
    assertExtensionsSupported ( "ARB_pixel_buffer_object" );

    if ( (mapFormat = fpFormatWithPrecision ( 32 ) ) == 0 )
    {
        printf ( "Floating-point textures not supported !\n" );
        
        return 1;
    }
    
    if ( !FrameBuffer :: isSupported () )
    {
        printf ( "Fbo not supported\n" );
        
        return 1;
    }
    
    if ( !GlslProgram :: isSupported () )
    {
        printf ( "GLSL not supported\n" );
        
        return 1;
    }
    
    srcMap = createTexture2D ( false, "../../Textures/floor.bmp" );     //256*256 pixels
    
    if ( srcMap == 0 )
    {
        printf ( "Texture floor.bmp not found.\n" );
        
        return 1;
    }
    
    unsigned tempMap = buffer.createColorRectTexture ( GL_RGBA, mapFormat );
    
    buffer.create ();
    buffer.bind   ();
    if ( !buffer.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, tempMap ) )
        printf ( "buffer error with color attachment\n");

    if ( !buffer.isOk () )
        printf ( "Error with framebuffer\n" );
                    
    buffer.unbind ();

    tempMap = buffer2.createColorRectTexture ( GL_RGBA, mapFormat );
    
    buffer2.create ();
    buffer2.bind   ();
    if ( !buffer2.attachColorTexture ( GL_TEXTURE_RECTANGLE_ARB, tempMap ) )
        printf ( "buffer error with color attachment\n");

    if ( !buffer2.isOk () )
        printf ( "Error with framebuffer\n" );
                    
    buffer2.unbind ();

    if ( !program.loadShaders ( "r2vb.vsh", "r2vb.fsh" ) )
    {
        printf ( "Error loading shaders:\n%s\n", program.getLog ().c_str () );

        return 3;
    }
    
    if ( !program2.loadShaders ( "scatter2.vsh", "scatter-2.fsh" ) )
    {
        printf ( "Error loading shaders:\n%s\n", program.getLog ().c_str () );

        return 3;
    }
    
    program.bind            ();
    program.setTexture      ( "srcMap",  0 );
    program.setUniformFloat ( "numBins", NUM_BINS );
    program.unbind          ();
    
    glutMainLoop ();

    return 0;
}

При использовании данных подходов следует обратить внимание на точность представление числе в текстуре, в которой происходит накопление результатов - если использовать обычную текстуру типа GL_RBA8, то 8 бит дадут нам только 256 различных значений счетчика, после чего происходит переполнение.

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

Второй подход заключается в использовании форматов текстур с большей разрешающей способностью - так использование 16-битовых floating point чисел позволяет получать до 2048 различных значения счетчика, а если использовать GPU серии GeForce 8xxx, поддерживающие alpha blending в 32-битовые целочисленные и floating point-форматы, то проблема фактически полностью решается (правда ценой более высоких требований к используемому GPU).

Данная статья написана по материалам статьи Efficient Histogram Generation Using Scattering on GPUs.

По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows, Linux и Mac OS X.

Используются технологии uCoz