Вопрос по c++, performance – Как быстро смешать RGBA беззнаковый байтовый цвет?

20

Я использую 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 мс?

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

Как это получилось? Tom Leys
Дайте мне знать, если у вас возникли проблемы с написанием остальных инструкций SIMD в моем решении Tom Leys
Какой компилятор вы используете? Для какой платформы вы разрабатываете это программное обеспечение? Готовы ли вы использовать готовые инструменты? Tom Leys
Я использую VS2005, программное обеспечение предназначено для платформы Windows. Я готов использовать любой метод для ускорения этого кода. Я думаю, что это может быть ускорено много user25749

Ваш Ответ

14   ответов
26

Используйте SSE - начать около страницы 131.

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

Load 4 pixels from src (16 1 byte numbers) RGBA RGBA RGBA RGBA (streaming load)

Load 4 more which you want to blend with srcbytetop RGBx RGBx RGBx RGBx

Do some swizzling so that the A term in 1 fills every slot I.e

xxxA xxxB xxxC xxxD -> AAAA BBBB CCCC DDDD

In my solution below I opted instead to re-use your existing "maskcurrent" array but having alpha integrated into the "A" field of 1 will require less loads from memory and thus be faster. Swizzling in this case would probably be: And with mask to select A, B, C, D. Shift right 8, Or with origional, shift right 16, or again.

Add the above to a vector that is all -255 in every slot

Multiply 1 * 4 (source with 255-alpha) and 2 * 3 (result with alpha).

You should be able to use the "multiply and discard bottom 8 bits" SSE2 instruction for this.

add those two (4 and 5) together

Store those somewhere else (if possible) or on top of your destination (if you must)

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

    //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) и сделать это на видеокарте.

Спасибо, это улучшит производительность процессора AMD? user25749
Смотрите изменения в моем оригинальном сообщении, чтобы ответить на ваш вопрос. Краткий ответ - да, если не древний процессор.
Будет ли это работать только на Windows или на других платформах? (Если я определю BYTe ofc) Главный вопрос: являются ли инструкции SIMD кроссплатформенными?
Да, инструкции SIMD требуют, чтобы их поддерживал ЦП, но они не заботятся об ОС (Windows и т. Д.). Компилятор также должен перевести внутренние компоненты (такие как_mm_set_epi8) но я считаю, что GCC может сделать это.
3

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

Тем не менее, версия Джаспера неверна. Это должно быть 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));
}
Это именно то, что я искал. Вы уверены в точностиna = 255 - a а не 256 или это что-то, что не может помочь в этом случае?
6

Рассмотреть возможность использованияpre-multiplied изображения переднего плана, как описаноПортер и Дафф, Помимо того, что вы, возможно, быстрее, вы избегаете многих потенциальных эффектов цветопередачи.

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

r =  kA + (1-k)B

... чтобы ...

r =  A + (1-k)B

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

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

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

1

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

Поместите на место следующий расчет для RGB

R = TopR + (SourceR * alpha) >> 8;
G = TopG + (SourceG * alpha) >> 8;
B = TopB + (SourceB * alpha) >> 8; 

Это более эффективный расчет.

Также используйте инструкцию shift left в макросе get pixel вместо умножения на BPP.

SSE довольно хорошо принят программистами и производителями микросхем.
2

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

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

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

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

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

Современные процессоры предпочитают чередующиеся инструкции в максимально возможной степени. Это связано с тем, что независимая работа (то есть вычисление R во время обработки G) хорошо подходит для конвейерной природы современных процессоров. Смотрите руководство по оптимизации Intel:intel.com/Assets/PDF/manual/248966.pdf, - Регистры могут показаться вам ограниченными, но у ЦПУ гораздо больше реальных регистров, чем вы думаете, используя & quot; переименование регистров & quot;
0
; In\   EAX = background color (ZRBG) 32bit (Z mean zero, always is zero)
; In\   EDX = foreground color (RBGA) 32bit
; Out\  EAX = new color
; free registers (R10, RDI, RSI, RSP, RBP)
abg2:
    mov r15b, dl                ; av
    movzx ecx, dl
    not ecx                     ; faster than 255 - dl
    mov r14b, cl                ; rem

    shr edx, 8
    and edx, 0x00FFFFFF
    mov r12d, edx
    mov r13d, eax               ; RBGA ---> ZRGB

    ; s: eax
    ; d: edx

    ;=============================red = ((s >> 16) * rem + (d >> 16) * av) >> 8;
    mov edx, r12d
    shr edx, 0x10
    movzx eax, r14b
    imul edx, eax
    mov ecx, r13d
    shr ecx, 0x10
    movzx eax, r15b
    imul eax, ecx
    lea eax, [eax + edx]                    ; faster than add eax, edx
    shr eax, 0x8
    mov r9b, al
    shl r9d, 8

    ;=============================green = (((s >> 8) & 0x0000ff) * rem + ((d >> 8) & 0x0000ff) * av) >> 8;
    mov eax, r12d
    shr eax, 0x8
    movzx edx, al
    movzx eax, r14b
    imul edx, eax
    mov eax, r13d
    shr eax, 0x8
    movzx ecx, al
    movzx eax, r15b
    imul eax, ecx
    lea eax, [eax, + edx]                   ; faster than add eax, edx
    shr eax, 0x8
    mov r9b, al
    shl r9d, 8

    ;=============================blue = ((s & 0x0000ff) * rem + (d & 0x0000ff) * av) >> 8;
    movzx edx, r12b
    movzx eax, r14b
    imul edx, eax
    movzx ecx, r13b
    movzx eax, r15b
    imul eax, ecx
    lea eax, [eax + edx]                ; faster than add eax, edx
    shr eax, 0x8
    mov r9b, al


    mov eax, r9d
    ret
3

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

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

  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 или & gt; & gt; 8 для скорости. Это не правильно! Если вы разделите на 256, вы никогда не сможете достичь чистого белого (255,255,255), потому что 255/256 чуть меньше единицы.

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

Да, i = i0 + t * ((i1 - i0) + 127) / 255 более эффективен, чем ваша формула, которая для целых чисел будет (я думаю): i = ((255 - t) * i0 + (t *) i1)) / 255
Большинство изображений на ПК имеют сгоревшую гамму. Таким образом, если значение пикселя равно, скажем, 127, это НЕ точно посередине между белым и черным. Фактическая яркость равна .. powf ((c) / 255.f, гамма) .. или примерно 0,19. Таким образом, все ваши вычисления, в которых предполагается, что яркость пикселей является линейной, неверны.
Интересные идеи есть, но вы платите большую цену за ваш / 255. Вы должны вычислить промежуточный 16-битный результат, используя t * ((v1 - v0) + 127), который затем делите. Вы уверены, что ваша формула действительно проще, чем (1-t) * v0 + t * v1? Помните, что 1-т рассчитывается заранее и / часто дороже, чем *
С ответом Guilerme это делает для правильного и быстрого смешивания. Спасибо!
Формула является ссылкой на то, как выглядит численно правильная формула. Это, конечно, медленнее, однако результаты точны. Полезно знать, как выглядит правильный ответ, чтобы определить, является ли ошибка в оптимизированном результате приемлемой или нет.
0

которая хорошо работает для 2 беззнаковых целых чисел.

Мой код немного отличается, так как код выше в основном всегда предполагает, что целевая альфа 255.

При достойном оптимизирующем компиляторе большинство вычислений должно быть в регистрах, поскольку область действия большинства переменных очень мала. Я также решил постепенно сдвигать результат & lt; & lt; 8 постепенно, чтобы избежать & lt; & lt; 24, & lt; & lt; 16 при сборке ARGB. Я знаю, что это давным-давно ... но я помню, что в 286 циклах для сдвига было (1 + 1 * каждый сдвинутый бит), поэтому предположим, что для больших сдвигов все еще есть какое-то наказание.

Также ... вместо "/ 255" Я выбрал & quot; & gt; & gt; 8 & Quot; которые могут быть изменены по желанию.

/*
    alpha blend source and destination, either may have an alpha!!!!

    Src  AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB
    Dest AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB

    res  AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB

    NOTE - α = αsrc + αdest(1.0-αsrc)  where α = 0.0 - 1.0

    ALSO - DWORD is unsigned int so (F8000000 >> 24) = F8 not FFFFFFF8 as it would with int (signed)
    */

    inline DWORD raw_blend(const DWORD src, const DWORD dest)
    {       
        // setup and calculate α

        DWORD src_a = src >> 24;       
        DWORD src_a_neg = 255 - src_a;
        DWORD dest_a = dest >> 24;

        DWORD res = src_a + ((dest_a * src_a_neg) >> 8);

        // setup and calculate R

        DWORD src_r = (src >> 16) & 255;
        DWORD dest_r = (dest >> 16) & 255;

        res = (res << 8) | (((src_r * src_a) + (dest_r * src_a_neg)) >> 8);

        // setup and calculate G

        DWORD src_g = (src >> 8) & 255;
        DWORD dest_g = (dest >> 8) & 255;

        res = (res << 8) | (((src_g * src_a) + (dest_g * src_a_neg)) >> 8);

        // setup and calculate B

        DWORD src_b = src & 255;
        DWORD dest_b = dest & 255;

        return (res << 8) | (((src_b * src_a) + (dest_b * src_a_neg)) >> 8);
    }
17

pt->r = (r+1 + (r >> 8)) >> 8; // fast way to divide by 255
+1, но учтите, что он действителен только для r & lt; 65535
((x+1)*257)>>16 // integer div 255 for [0..65790) - альтернативная формулировка, которая может быть быстрее на некоторых платформах - интересные заметки:Division via Multiplication
(x+1+((x+1)>>8))>>8 // integer div 255 for [0..65790) -- немного лучше
@nobar: также стоит рассмотреть стандартную хитрость компилятора деления с мультипликативным обратным:n/255 compiles to = asm that does (n*0x8081) >> 23, Это также работает для всех 16-битныхn, (Я только что заметил, что ваша верхняя граница была выше, чем 65536). С x86 SSE2 это один_mm_mulhi_epu16 и один_mm_srli_epu16(mul, 23-16). x+1 * 257 это один paddw и один pmulhuw, так что это на самом деле лучше (поскольку mul и shift могут конкурировать за один и тот же порт).
Это может быть расширено до двух 16-битных слов: ((r + 0x10001 + ((r & gt; 8) и 0xFF00FF)) & gt; & gt; 8) & amp; 0xFF00FF и это позволяет мультиплексировать операции xRxB и AxGx в ARGB, аналогично в RGBA и других вариантах
2

что аппаратная поддержка поможет вам. попытаться перенести логику с программного обеспечения на аппаратное обеспечение, если это возможно

20

акже можете использовать этот трюк с реализацией 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 & lt; = альфа & lt; = 0x100

Переполнение является преднамеренным, оно обрабатывается в операторе возврата.
@MSalters, возможно, из-за похмелья, но я не вижу переполнения; или хорошо, я вижу намеренное переполнение в rb и g, но они маскируются в операторе возврата. (Пока int 32 бита).
Он получил довольно грубую обработку переполнения: обтекание вместо насыщения.
Хороший трюк. Вы также должны добавить обработку насыщенности (сейчас она переполняется)
@JasperBekkers, ты действительно попробовал свой пример? При альфа = 0xff (непрозрачный) результат равен 0xff80 (красный полностью исчезает, другие цвета также ошибочны).
4

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

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

Нет необходимости использовать предварительно умноженную альфу, только чтобы убедиться, что цвет фона удален из частично прозрачных пикселей. Удаление цвета фона может быть частью процесса преобразования в предварительно умноженную альфа, но это также может быть сделано независимо.
2

по которой вы не просматриваете каждый пиксель напрямую? Зачем использовать все вызовы BYTE * и GET_BYTE ()? Это, вероятно, часть проблемы со скоростью.

Как выглядит GET_GRAY?

Что еще более важно, вы уверены, что ваша платформа не предоставляет возможности альфа-смешивания? На какую платформу вы ориентируетесь? Вики сообщает мне, что следующие поддерживают его из коробки:

Mac OS X Windows 2000, XP, Server 2003, Windows CE, Vista and Windows 7 The XRender extension to the X Window System (this includes modern Linux systems) RISC OS Adjust QNX Neutrino Plan 9 Inferno AmigaOS 4.1 BeOS, Zeta and Haiku Syllable MorphOS
Даже если вы не отображаете изображение, вы все равно можете использовать возможности платформы. Например, в Windows вы можете использовать GDI + или оболочки .NET для альфа-смешивания, даже не отображая его. Я предполагаю, что другие платформы аналогичны.
Эта альфа-смесь используется для определенного алгоритма улучшения изображения, а не для отображения. Поэтому я не могу использовать возможности платформы. Спасибо! удалить большинство GET_BYTE () кажется бесполезным, возможно, проблема заключается в операции умножения и операции деления 255. user25749
3

равнивания памяти), а затем использовать инструкции SSE для обработки всех каналов вместе. Поиск & quot; визуальная студия sse intrinsics & quot ;.

Похожие вопросы