Как быстро смешать RGBA беззнаковый байтовый цвет? - PullRequest
20 голосов
/ 09 июля 2009

Я использую c ++, я хочу сделать альфа-смешивание, используя следующий код.

#define CLAMPTOBYTE(color) \
    if ((color) & (~255)) { \
        color = (BYTE)((-(color)) >> 31); \
    } else { \
        color = (BYTE)(color); \
    }
#define GET_BYTE(accessPixel, x, y, scanline, bpp) \
    ((BYTE*)((accessPixel) + (y) * (scanline) + (x) * (bpp))) 

    for (int y = top ; y < bottom; ++y)
    {
        BYTE* resultByte = GET_BYTE(resultBits, left, y, stride, bytepp);
        BYTE* srcByte = GET_BYTE(srcBits, left, y, stride, bytepp);
        BYTE* srcByteTop = GET_BYTE(srcBitsTop, left, y, stride, bytepp);
        BYTE* maskCurrent = GET_GREY(maskSrc, left, y, width);
        int alpha = 0;
        int red = 0;
        int green = 0;
        int blue = 0;
        for (int x = left; x < right; ++x)
        {
            alpha = *maskCurrent;
            red = (srcByteTop[R] * alpha + srcByte[R] * (255 - alpha)) / 255;
            green = (srcByteTop[G] * alpha + srcByte[G] * (255 - alpha)) / 255;
            blue = (srcByteTop[B] * alpha + srcByte[B] * (255 - alpha)) / 255;
            CLAMPTOBYTE(red);
            CLAMPTOBYTE(green);
            CLAMPTOBYTE(blue);
            resultByte[R] = red;
            resultByte[G] = green;
            resultByte[B] = blue;
            srcByte += bytepp;
            srcByteTop += bytepp;
            resultByte += bytepp;
            ++maskCurrent;
        }
    }

однако я считаю, что это все еще медленно, это занимает около 40 - 60 мс, когда составляется два 600 * 600 изображения. Есть ли способ улучшить скорость до 16 мс?

Может ли кто-нибудь помочь мне ускорить этот код? Большое спасибо!

Ответы [ 16 ]

26 голосов
/ 09 июля 2009

Использовать SSE - начать со страницы 131.

Основной рабочий процесс

  1. Загрузка 4 пикселей из src (16 1-байтовых чисел) RGBA RGBA RGBA RGBA (потоковая загрузка)

  2. Загрузить еще 4, которые вы хотите смешать с srcbytetop RGBx RGBx RGBx RGBx

  3. Сделайте немного, чтобы член A в 1 заполнял каждый слот I.e

    xxxA xxxB xxxC xxxD -> AAAA BBBB CCCC DDDD

    В моем решении, приведенном ниже, я решил вместо этого повторно использовать существующий массив «maskcurrent», но интеграция альфа в поле «A», равное 1, потребует меньше нагрузки из памяти и, следовательно, будет быстрее. Swizzling в этом случае, вероятно, будет: И с маской для выбора A, B, C, D. Сдвиг вправо 8, Или с оригиналом, сдвиг вправо 16 или снова.

  4. Добавьте приведенное выше к вектору, в котором все -255 в каждом слоте

  5. Умножьте 1 * 4 (источник с 255-альфа) и 2 * 3 (результат с альфа).

    Для этого вы должны быть в состоянии использовать инструкцию SSE2 «умножить и отбросить младшие 8 битов».

  6. сложите эти два (4 и 5) вместе

  7. Храните их в другом месте (если возможно) или в верхней части пункта назначения (если необходимо)

Вот отправная точка для вас:

    //Define your image with __declspec(align(16)) i.e char __declspec(align(16)) image[640*480]
    // so the first byte is aligned correctly for SIMD.
    // Stride must be a multiple of 16.

    for (int y = top ; y < bottom; ++y)
    {
        BYTE* resultByte = GET_BYTE(resultBits, left, y, stride, bytepp);
        BYTE* srcByte = GET_BYTE(srcBits, left, y, stride, bytepp);
        BYTE* srcByteTop = GET_BYTE(srcBitsTop, left, y, stride, bytepp);
        BYTE* maskCurrent = GET_GREY(maskSrc, left, y, width);
        for (int x = left; x < right; x += 4)
        {
            //If you can't align, use _mm_loadu_si128()
            // Step 1
            __mm128i src = _mm_load_si128(reinterpret_cast<__mm128i*>(srcByte)) 
            // Step 2
            __mm128i srcTop = _mm_load_si128(reinterpret_cast<__mm128i*>(srcByteTop)) 

            // Step 3
            // Fill the 4 positions for the first pixel with maskCurrent[0], etc
            // Could do better with shifts and so on, but this is clear
            __mm128i mask = _mm_set_epi8(maskCurrent[0],maskCurrent[0],maskCurrent[0],maskCurrent[0],
                                        maskCurrent[1],maskCurrent[1],maskCurrent[1],maskCurrent[1],
                                        maskCurrent[2],maskCurrent[2],maskCurrent[2],maskCurrent[2],
                                        maskCurrent[3],maskCurrent[3],maskCurrent[3],maskCurrent[3],
                                        ) 

            // step 4
            __mm128i maskInv = _mm_subs_epu8(_mm_set1_epu8(255), mask) 

            //Todo : Multiply, with saturate - find correct instructions for 4..6
            //note you can use Multiply and add _mm_madd_epi16

            alpha = *maskCurrent;
            red = (srcByteTop[R] * alpha + srcByte[R] * (255 - alpha)) / 255;
            green = (srcByteTop[G] * alpha + srcByte[G] * (255 - alpha)) / 255;
            blue = (srcByteTop[B] * alpha + srcByte[B] * (255 - alpha)) / 255;
            CLAMPTOBYTE(red);
            CLAMPTOBYTE(green);
            CLAMPTOBYTE(blue);
            resultByte[R] = red;
            resultByte[G] = green;
            resultByte[B] = blue;
            //----

            // Step 7 - store result.
            //Store aligned if output is aligned on 16 byte boundrary
            _mm_store_si128(reinterpret_cast<__mm128i*>(resultByte), result)
            //Slow version if you can't guarantee alignment
            //_mm_storeu_si128(reinterpret_cast<__mm128i*>(resultByte), result)

            //Move pointers forward 4 places
            srcByte += bytepp * 4;
            srcByteTop += bytepp * 4;
            resultByte += bytepp * 4;
            maskCurrent += 4;
        }
    }

Чтобы узнать, какие процессоры AMD будут запускать этот код (в настоящее время он использует инструкции SSE2), см. Список микропроцессоров AMD Turion в Википедии . Вы также можете посмотреть другие списки процессоров в Википедии, но мое исследование показывает, что процессоры AMD, выпущенные около 4 лет назад, поддерживают как минимум SSE2.

Вы должны ожидать, что хорошая реализация SSE2 будет работать примерно в 8-16 раз быстрее, чем ваш текущий код. Это связано с тем, что мы исключаем ветви в цикле, обрабатываем 4 пикселя (или 12 каналов) одновременно и повышаем производительность кэша с помощью потоковых инструкций. В качестве альтернативы SSE вы, вероятно, могли бы сделать ваш существующий код намного быстрее, исключив проверки if, которые вы используете для насыщения. Помимо этого мне нужно будет запустить профилировщик для вашей рабочей нагрузки.

Конечно, лучшее решение - использовать аппаратную поддержку (т.е. код вашей проблемы в DirectX) и сделать это на видеокарте.

20 голосов
/ 09 июля 2009

Вы всегда можете рассчитать альфа красного и синего одновременно. Вы также можете использовать этот трюк с реализацией SIMD, упомянутой ранее.

unsigned int blendPreMulAlpha(unsigned int colora, unsigned int colorb, unsigned int alpha)
{
    unsigned int rb = (colora & 0xFF00FF) + ( (alpha * (colorb & 0xFF00FF)) >> 8 );
    unsigned int g = (colora & 0x00FF00) + ( (alpha * (colorb & 0x00FF00)) >> 8 );
    return (rb & 0xFF00FF) + (g & 0x00FF00);
}


unsigned int blendAlpha(unsigned int colora, unsigned int colorb, unsigned int alpha)
{
    unsigned int rb1 = ((0x100 - alpha) * (colora & 0xFF00FF)) >> 8;
    unsigned int rb2 = (alpha * (colorb & 0xFF00FF)) >> 8;
    unsigned int g1  = ((0x100 - alpha) * (colora & 0x00FF00)) >> 8;
    unsigned int g2  = (alpha * (colorb & 0x00FF00)) >> 8;
    return ((rb1 | rb2) & 0xFF00FF) + ((g1 | g2) & 0x00FF00);
}

0 <= альфа <= 0x100 </p>

17 голосов
/ 09 ноября 2011

Для людей, которые хотят разделить на 255, я нашел идеальную формулу:

pt->r = (r+1 + (r >> 8)) >> 8; // fast way to divide by 255
6 голосов
/ 09 июля 2009

Вот несколько указателей.

Рассмотрите возможность использования предварительно умноженных изображений переднего плана, как описано Porter and Duff . Помимо того, что вы, возможно, быстрее, вы избегаете многих потенциальных эффектов цветовой окраски.

Уравнение композитинга меняется с

r =  kA + (1-k)B

... до ...

r =  A + (1-k)B

Кроме того, вы можете переработать стандартное уравнение, чтобы убрать одно умножение.

r =  kA + (1-k)B
==  kA + B - kB
== k(A-B) + B

Я могу ошибаться, но я думаю, что вам не нужно зажимать тоже ...

4 голосов
/ 09 июля 2009

Нет точного ответа на вопрос, но ...

Одна вещь, чтобы сделать это быстро, другая вещь, чтобы сделать это правильно. Альфа-композитинг - опасный зверь, он выглядит прямо и интуитивно, но распространенные ошибки были распространены в течение десятилетий, и никто не заметил этого (почти)!

Самая известная и распространенная ошибка - НЕ использовать предварительно умноженную альфа . Я настоятельно рекомендую это: Альфа-смешивание для листьев

3 голосов
/ 26 ноября 2014

Я не могу комментировать, потому что у меня недостаточно репутации, но я хочу сказать, что версия Джаспера не переполнит для правильного ввода. Маскирование результата умножения необходимо, потому что иначе умножение красного + синего оставило бы биты в зеленом канале (это также было бы верно, если бы вы умножили красный и синий по отдельности, вам все равно нужно было бы маскировать биты в голубом канале) и умножение зеленого цвета оставило бы биты в синем канале. Это биты, которые теряются при сдвиге вправо, если вы разделяете компоненты, как это часто бывает в случае альфа-смешения. Так что они не переполнены или не переполнены. Это просто бесполезные биты, которые нужно маскировать для достижения ожидаемых результатов.

Тем не менее, версия Джаспера неверна. Это должно быть 0xFF-альфа (255-альфа), а не 0x100-альфа (256-альфа). Это, вероятно, не приведет к видимой ошибке. Что приведет к видимой ошибке, так это его использование | вместо + при объединении результатов умножения.

Я обнаружил, что адаптация кода Jasper быстрее, чем мой старый код альфа-смешивания, который был уже приличным, и в настоящее время использую его в своем проекте программного рендерера. Я работаю с 32-битными пикселями ARGB:

Pixel AlphaBlendPixels(Pixel p1, Pixel p2)
{
    static const int AMASK = 0xFF000000;
    static const int RBMASK = 0x00FF00FF;
    static const int GMASK = 0x0000FF00;
    static const int AGMASK = AMASK | GMASK;
    static const int ONEALPHA = 0x01000000;
    unsigned int a = (p2 & AMASK) >> 24;
    unsigned int na = 255 - a;
    unsigned int rb = ((na * (p1 & RBMASK)) + (a * (p2 & RBMASK))) >> 8;
    unsigned int ag = (na * ((p1 & AGMASK) >> 8)) + (a * (ONEALPHA | ((p2 & GMASK) >> 8)));
    return ((rb & RBMASK) | (ag & AGMASK));
}
3 голосов
/ 05 августа 2009

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

Вы начинаете с этого:

  v = ( 1-t ) * v0 + t * v1

, где t = параметр интерполяции [0..1] v0 = значение исходного цвета v1 = передать значение цвета V = выходное значение

Изменяя условия, мы можем сократить количество операций:

  v = v0 + t * (v1 - v0)

Вы должны выполнить этот расчет один раз для цветового канала (3 раза для RGB).

Для 8-разрядных компонентов без знака необходимо использовать правильную математику с фиксированной точкой:

  i = i0 + t * ( ( i1 - i0 ) + 127 ) / 255

, где t = параметр интерполяции [0..255] i0 = значение исходного цвета [0..255] i1 = значение передачи цвета [0..255] я = цвет вывода

Если вы пропустите +127, то ваши цвета будут смещены в сторону более темного конца. Очень часто люди используют / 256 или >> 8 для скорости. Это не правильно! Если вы разделите на 256, вы никогда не сможете достичь чистого белого (255 255 255), потому что 255/256 немного меньше единицы.

Надеюсь, это поможет.

3 голосов
/ 09 июля 2009

Можно использовать 4 байта на пиксель в обоих изображениях (для выравнивания памяти), а затем использовать инструкции SSE для обработки всех каналов вместе. Поиск "visual studio sse intrinsics".

2 голосов
/ 09 июля 2009

Основной проблемой будет плохая конструкция цикла, возможно, усугубленная компилятором, который не может устранить CSE. Переместите настоящие общие биты за пределы петель. int red необычно, ты - это должно быть во внутренней петле.

Кроме того, красный, зеленый и синий независимы. Если вы рассчитываете их по очереди, вам не нужно сохранять промежуточные красные результаты в регистрах при расчете зеленых результатов. Это особенно важно для процессоров с ограниченными регистрами, таких как x86.

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

Как уже отмечалось, зажим не требуется. В alphablending вы создаете линейную комбинацию двух изображений a [x] [y] и b [x] [y]. Так как 0 <= alpha <= 255, вы знаете, что каждый выход ограничен max (255 * a [x] [y], 255 * b [x] [y]). И так как ваш выходной диапазон совпадает с обоими входными диапазонами (0-255), это нормально. </p>

С небольшой потерей точности вы можете рассчитать (a[x][y]*alpha * b[x][y]*(256-alpha))>>8. Bitshift часто быстрее, чем деление.

2 голосов
/ 09 июля 2009

Переместите его в графический процессор.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...