Эффективный способ конвертировать из предварительно умноженного поплавкового RGBA в 8-битный RGBA?

Я ищу более эффективный способ конвертировать из RGBA, который хранится как удвоение в премультиплексированном цветовом пространстве, до 8-битного целочисленного/канального RGBA непремультивированного цветового пространства. Это значительная стоимость обработки изображений.

Для одного канала, скажем R, код выглядит примерно так:

double temp = alpha > 0 ? src_r / alpha : 0
uint8_t out_r = (uint8_t)min( 255, max( 0, int(temp * 255 + 0.5) ) )

Это связано с тремя условностями, которые, как я думаю, не позволяют компилятору/ЦП оптимизировать это, а также могут. Я думаю, что некоторые чипы, в частности x86_64, имеют специализированные операции двойного зажима, поэтому теоретически вышеупомянутое может выполняться без условностей.

Есть ли какой-то метод или специальные функции, которые могут сделать это преобразование быстрее?

Я использую GCC и буду удовлетворен решением на C или С++ или с встроенным ASM, если это необходимо.

Ответ 1

Вот контур с некоторым кодом (непроверенный). Это преобразует сразу четыре пикселя. Основным преимуществом этого метода является то, что он должен выполнять деление один раз (а не четыре раза). Отдел медленный. Но для этого нужно сделать tranpose (AoS to SoA). Он использует в основном SSE, кроме как для преобразования парных в float (для чего требуется AVX).

1.) Load 16 doubles
2.) Convert them to floats
3.) Transpose from rgba rgba rgba rgba to rrrr gggg bbbb aaaa
4.) Divide all 4 alphas in one instruction
5.) Round floats to ints
6.) Compress 32-bit to 8-bit with saturation for underflow and overflow
7.) Transpose back to rgba rgba rgba rgba
9.) Write 4 pixels as integers in rgba format

#include <immintrin.h>
double rgba[16];
int out[4];

//load 16 doubles and convert to floats
__m128 tmp1 = _mm256_cvtpd_ps(_mm256_load_pd(&rgba[0]));
__m128 tmp2 = _mm256_cvtpd_ps(_mm256_load_pd(&rgba[4]));
__m128 tmp3 = _mm256_cvtpd_ps(_mm256_load_pd(&rgba[8]));
__m128 tmp4 = _mm256_cvtpd_ps(_mm256_load_pd(&rgba[12]));
//rgba rgba rgba rgba -> rrrr bbbb gggg aaaa
_MM_TRANSPOSE4_PS(tmp1,tmp2,tmp3,tmp4);
//fact = alpha > 0 ? 255.0f/ alpha : 0
__m128 fact = _mm_div_ps(_mm_set1_ps(255.0f),tmp4); 
tmp1 = _mm_mul_ps(fact,tmp1); //rrrr
tmp2 = _mm_mul_ps(fact,tmp2); //gggg
tmp3 = _mm_mul_ps(fact,tmp3); //bbbb    
tmp4 = _mm_mul_ps(_mm_set1_ps(255.0f), tmp4); //aaaa

//round to nearest int
__m128i tmp1i = _mm_cvtps_epi32(tmp1);
__m128i tmp2i = _mm_cvtps_epi32(tmp2);
__m128i tmp3i = _mm_cvtps_epi32(tmp3);
__m128i tmp4i = _mm_cvtps_epi32(tmp4);

//compress from 32bit to 8 bit
__m128i tmp5i = _mm_packs_epi32(tmp1i, tmp2i);
__m128i tmp6i = _mm_packs_epi32(tmp3i, tmp4i);
__m128i tmp7i = _mm_packs_epi16(tmp5i, tmp6i);

//transpose back to rgba rgba rgba rgba
__m128i out16 = _mm_shuffle_epi8(in16,_mm_setr_epi8(0x0,0x04,0x08,0x0c, 0x01,0x05,0x09,0x0d, 0x02,0x06,0x0a,0x0e, 0x03,0x07,0x0b,0x0f));
_mm_store_si128((__m128i*)out, tmp7i);

Ответ 2

Хорошо, это псевдокод, но с SSE, как о чем-то вроде

const c = (1/255, 1/255, 1/255, 1/255)
floats = (r, g, b, a)
alpha =  (a, a, a, a)
alpha *= (c, c, c, c)
floats /= alpha
ints = cvt_float_to_int(floats)
ints = max(ints, (255, 255, 255, 255))

Здесь реализация

void convert(const double* floats, byte* bytes, const int width, const int height, const int step) {
    for(int y = 0; y < height; ++y) {
        const double* float_row = floats + y * width;
        byte*        byte_row  = bytes  + y * step;

        for(int x = 0; x < width; ++x) {
            __m128d src1  = _mm_load_pd(float_row);
            __m128d src2  = _mm_load_pd(float_row + 2);
            __m128d mul   = _mm_set1_pd(255.0f / float_row[3]);
            __m128d norm1 = _mm_min_pd(_mm_set1_pd(255), _mm_mul_pd(src1, mul));
            __m128d norm2 = _mm_min_pd(_mm_set1_pd(255), _mm_mul_pd(src2, mul));
            __m128i dst1 = _mm_shuffle_epi8(_mm_cvtpd_epi32(norm1), _mm_set_epi8(0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,4,0));
            __m128i dst2 = _mm_shuffle_epi8(_mm_cvtpd_epi32(norm2), _mm_set_epi8(0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,4,0,0x80,0x80));
            _mm_store_ss((float*)byte_row, _mm_castsi128_ps(_mm_or_si128(dst1, dst2)));

            float_row += 4;
            byte_row += 4;
        }
    }
}

Изменить: в моем исходном ответе я работал с float вместо double, ниже, если кому-то интересен благодаря @Z boson для его ловли - @OP: я не обрабатываю случаи alhpa==0 поэтому вы получите NaN с моим решением, если хотите эту обработку, займитесь решением @Z boson. Здесь версия с плавающей запятой:

void convert(const float* floats, byte* bytes, const int width, const int height, const int step) {
    for(int y = 0; y < height; ++y) {
        const float* float_row = floats + y * width;
        byte*        byte_row  = bytes  + y * step;

        for(int x = 0; x < width; ++x) {
            __m128 src = _mm_load_ps(float_row);
            __m128 mul = _mm_set1_ps(255.0f / float_row[3]);
            __m128i cvt = _mm_cvtps_epi32(_mm_mul_ps(src, mul));
            __m128i res = _mm_min_epi32(cvt, _mm_set1_epi32(255));
            __m128i dst = _mm_shuffle_epi8(res, _mm_set_epi8(0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,12,8,4,0));
            _mm_store_ss((float*)byte_row, _mm_castsi128_ps(dst));

            float_row += 4;
            byte_row += 4;
        }
    }
}

Из-за ограничений выравнивания SSE, убедитесь, что указатели ввода выравниваются по 16 байт и используйте step, чтобы убедиться, что каждая строка начинается с выровненного адреса, многие библиотеки принимают такой аргумент step, но если вы Это не нужно, вы можете упростить, используя один цикл.

Я быстро проверил это и получил хорошие значения:

int main() {
    __declspec(align(16)) double src[] = { 10,100,1000,255, 10,100,20,50 };
    __declspec(align(16)) byte  dst[8];
    convert(src, dst, 2, 1, 16); // dst == { 10,100,255,255 }
    return 0;
}

У меня только сейчас есть визуальная студия, поэтому я не могу тестировать ее с помощью gcc-оптимизатора, но я получаю x1.8 speedup для double и x4.5 для float, это может быть меньше с gcc-O3, но мой код может быть оптимизирован больше.

Ответ 3

Три вещи, которые нужно посмотреть в

  • Сделайте это с помощью OpenGL с помощью шейдера.
  • Использовать множественные данные с одной инструкцией (SIMD) - вы можете получить немного распараллеливания.
  • Посмотрите на использование насыщенных арифметических операций (SADD и SMULL на руке)