C++ 将预乘浮点RGBA转换为8位RGBA的有效方法?

C++ 将预乘浮点RGBA转换为8位RGBA的有效方法?,c++,c,C++,C,我正在寻找一种更有效的方法,将预乘颜色空间中存储为双精度的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) ) ) 这涉及到三个条件,我认为这会阻止编译器/CPU尽可能地对此进行优化。我认为一些芯

我正在寻找一种更有效的方法,将预乘颜色空间中存储为双精度的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) ) )
这涉及到三个条件,我认为这会阻止编译器/CPU尽可能地对此进行优化。我认为一些芯片,特别是x86_64,具有专门的双钳位操作,因此理论上,上述操作可能不需要条件

是否有一些技术或特殊功能可以加快转换速度


我使用GCC,如果C或C++或内联ASM需要解决,则满意。

< P > >三项看< /P>
  • 使用着色器使用OpenGL执行此操作
  • 使用单指令多数据(SIMD)-您可能会得到一些并行化
  • 看看如何使用饱和算术运算(arm上的SADD和SMULL)

  • 好的,这是伪代码,但是对于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;
            }
        }
    }
    
    由于SSE对齐限制,请确保输入指针是16字节对齐的,并使用
    step
    确保每一行从对齐的地址开始,许多LIB使用这样的
    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;
    }
    

    我现在只有visual studio,所以我不能用gcc的优化器进行测试,但是我得到了双倍的x1.8加速,浮动的是x4.5加速,使用gcc-O3可能会少一些,但我的代码可以优化得更多。

    下面是一些代码的提纲(未测试)。这将一次转换四个像素。这种方法的主要优点是只需进行一次除法(而不是四次)。分裂是缓慢的。但要做到这一点,它必须进行转换(AoS到SoA)。除了将double转换为float(需要AVX)之外,它主要使用SSE

    1.)加载16倍
    2.)将它们转换为浮点数
    3.)从rgba rgba rgba rgba rgba转置到rrrr gggg bbbb aaaa
    4.)在一条指令中划分所有4个字母
    5.)圆形浮动到整数
    6.)将32位压缩到8位,并对下溢和溢出进行饱和
    7.)调回rgba rgba rgba rgba
    9.)以rgba格式写入4个像素作为整数
    #包括
    双rgba[16];
    int out[4];
    //加载16倍并转换为浮点数
    __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_转置4_PS(tmp1、tmp2、tmp3、tmp4);
    //事实=α>0?255.0f/α:0
    __m128事实=_mm_div_ps(_mm_set1_ps(255.0f),tmp4);
    tmp1=_mm_mul_ps(事实,tmp1)//rrrr
    tmp2=_mm_mul_ps(事实,tmp2)//gggg
    tmp3=_mm_mul_ps(事实,tmp3)//bbbb
    tmp4=_mm_mul_ps(_mm_set1_ps(255.0f),tmp4)//aaaa
    //四舍五入到最接近的整数
    __m128i tmp1i=_mm_cvtps_epi32(tmp1);
    __m128i tmp2i=_mm_cvtps_epi32(tmp2);
    __m128i tmp3i=_mm_cvtps_epi32(tmp3);
    __m128i tmp4i=_mm_cvtps_epi32(tmp4);
    //从32位压缩到8位
    __m128i tmp5i=_mm_packs_epi32(tmp1i,tmp2i);
    __m128i tmp6i=_mm_packs_epi32(tmp3i,tmp4i);
    __m128i tmp7i=_mm_packs_epi16(tmp5i,tmp6i);
    //调回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;
    
    您是否已经尝试了
    -ffast math
    标志?请注意,此标志可能会改变程序的行为,编译后测试程序。为什么要这样存储alpha?通常人们使用从0到1的数字进行乘法而不是除法。@n.m.这是将预乘空间转换回非预乘空间的方法。我不知道有什么方法不需要除法。注意:我所有的双精度值都有一个从0.0到1.0的正常范围,但有一些可能超出该范围(因此钳制)。@user2485710,-ffast math不会更改我的计时。我在-O3处编译。@edA-qamort-ora-y:
    n/x=n*(1/x)
    。去掉该分支,并将alpha值存储在
    0..1.0
    范围内。我来看看饱和算法。我认为在某种程度上,GCC已经在做矢量化,甚至可能在做一些有限的饱和数学。我想我会发现的。+1对于使用OpenGL/OpenCL,它非常适合快速图像处理。OP说他的值是双倍而不是浮点数。当你除以零(255.0f/0)时会发生什么@Zboson:事实上,我太习惯浮点数了,我没听清楚。。。很抱歉被零除将导致
    NaN
    s,这取决于用例,它可能值得,也可能不值得感谢链接!我做cvt=alpha>0?255.0/alpha:0使用
    \u mm\u blendv\u ps(255.0/alpha,0,alpha>0)
    。我不知道是否有必要。我代码中的主要问题是转置两次。你不能那样做。你也不会压缩两次。您的代码可能更好(+1)。谢谢,我将我的解决方案转换为Double,但性能提升不太好。我对洗牌不太满意,也许其他指令可以更优雅地移动字节?事实上,我被自己的方法分心了。我一次做四个像素,所以我们必须把你所有的指令乘以四。我的方法的主要优点是我只需要做一次除法,而你需要做四次。此外,我还分三步将32位转换为8位,您可以分四步完成(对于四个像素)。我为此付出的代价是两次换位。然而,我认为最后一个转置现在可以使用
    \u mm\u shuffle\u epi8
    在一个内部完成。因此,主要是四个部门对eig的问题
    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;
    }
    
    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);