【问题标题】:Efficient way to convert from premultiplied float RGBA to 8-bit RGBA?从预乘浮点 RGBA 转换为 8 位 RGBA 的有效方法?
【发布时间】:2014-03-08 01:33:12
【问题描述】:

我正在寻找一种更有效的方法,将 预乘色彩空间中存储为双精度的 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 的解决方案。

【问题讨论】:

  • 您是否已经尝试过-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 的范围内

标签: c++ c


【解决方案1】:

这是带有一些代码的大纲(未经测试)。这将一次转换四个像素。这种方法的主要优点是它只需要进行一次除法(而不是四次)。分工很慢。但它必须进行转换(AoS 到 SoA)才能做到这一点。它主要使用 SSE,除了将双精度数转换为浮点数(需要 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);

【讨论】:

  • +1 用于按 4 块进行“平铺”处理,这优化了 SIMD 的使用
  • 你有点被骗了:所有的计算都是浮点数而不是双精度数。不是我怪你,如果由我决定,我会在花车上做所有事情;)
  • @Antoine,我认为我们可以使用浮点数。它从双倍到 8 位 - 这是精度的巨大损失。你只需要假设你从 double 开始。所以在你的代码中可以使用浮点数,但输入必须是双倍的。
【解决方案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;
        }
    }
}

编辑:在我的原始答案中,我使用浮点数而不是双精度数,如果有人感兴趣,请在下面感谢@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;
}

我现在只有 Visual Studio,所以我无法使用 gcc 的优化器进行测试,但我得到了 x1.8 加速 用于双精度和 x4.5 用于浮点数,它可能是使用 gcc -O3 更少,但我的代码可以得到更多优化。

【讨论】:

  • OP 说他的值是双精度数而不是浮点数。当你除以零时会发生什么 (_mm_set1_ps(255.0f / 0);)
  • @Zboson:确实,我已经习惯了漂浮物,我没听懂……对不起!除以零将导致NaNs,根据用例,add a compare + mask 可能值得也可能不值得
  • 感谢您的链接!我做 cvt = alpha >0 吗? 255.0/alpha : 0 使用_mm_blendv_ps(255.0/alpha, 0, alpha&gt;0)。不知道有没有必要。我的代码中的主要问题是两次转置。你不这样做。你也不要压缩两次。您的代码可能更好 (+1)。
  • 谢谢,我将我的解决方案转换为双打,但性能提升并不好。我对洗牌不是很满意,也许其他一些指令可以更优雅地移动字节?
  • 实际上,我被自己的方法分心了。我一次做四个像素,所以我们确实将你所有的指令乘以四。我的方法的主要优点是我只需要做一次除法,你必须做四次。此外,我分三步将 32 位转换为 8 位,而您分 4 步完成(对于四个像素)。我为此付出的代价是转置两次。但是,我认为现在可以使用_mm_shuffle_epi8 在一个内在函数中完成最后一个转置。所以主要是四个分区与八次随机播放的问题(_MM_TRANSPOSE_PS 使用八次随机播放)。
【解决方案3】:

需要调查的三件事

  1. 使用 OpenGL 使用着色器执行此操作。
  2. 使用单指令多数据 (SIMD) - 您可能会获得一些并行化。
  3. 看看使用饱和算术运算(ARM 上的 SADD 和 SMULL)

【讨论】:

  • 我来看看饱和算法。我认为在某种程度上 GCC 已经在进行矢量化,甚至可能正在做一些有限的饱和数学。我想我会发现的。
  • +1 使用 OpenGL/OpenCL,非常适合快速图像处理
猜你喜欢
  • 2020-05-13
  • 1970-01-01
  • 2015-04-08
  • 1970-01-01
  • 2020-08-22
  • 2021-03-08
  • 2011-12-20
  • 2012-04-11
  • 2014-03-01
相关资源
最近更新 更多