将BYTE缓冲区(0-255)转换为浮点数缓冲区(0.0-1.0)

7

如何将BYTE缓冲区(从0到255)转换为浮点数缓冲区(从0.0到1.0)?当然,这两个值之间应该存在关系,例如:字节缓冲区中的0将成为浮点缓冲区中的.0.f,字节缓冲区中的128将成为浮点缓冲区中的.5f,字节缓冲区中的255将成为浮点缓冲区中的1.f。

实际上,这是我拥有的代码:

for (int y=0;y<height;y++) {
    for (int x=0;x<width;x++) {
        float* floatpixel = floatbuffer + (y * width + x) * 4;
        BYTE* bytepixel = (bytebuffer + (y * width + x) * 4);
        floatpixel[0] = bytepixel[0]/255.f;
        floatpixel[1] = bytepixel[1]/255.f;
        floatpixel[2] = bytepixel[2]/255.f;
        floatpixel[3] = 1.0f; // A
    }
}

这个运行速度非常慢。我的一个朋友建议我使用转换表,但我想知道是否有其他方法。


1
仅仅为了完整性,字节缓冲区中的128将在浮点缓冲区中表示为.5019607843f,而不是.5f。 - sam hocevar
7个回答

9
无论您选择使用查找表还是不使用,您的代码在每个循环迭代中都会做很多不必要的工作——这很可能足以超过转换和乘法的成本。
声明指针为restrict,并将只读取的指针声明为const。用1/255乘以而非除以255。不要在内部循环的每次迭代中计算指针,只需计算初始值并递增它们。将内部循环展开几次。如果目标支持,使用矢量SIMD操作。不要递增并与最大值进行比较,而应该递减并与零进行比较。
类似以下内容:
float* restrict floatpixel = floatbuffer;
BYTE const* restrict bytepixel = bytebuffer;
for( int size = width*height; size > 0; --size )
{
    floatpixel[0] = bytepixel[0]*(1.f/255.f);
    floatpixel[1] = bytepixel[1]*(1.f/255.f);
    floatpixel[2] = bytepixel[2]*(1.f/255.f);
    floatpixel[3] = 1.0f; // A
    floatpixel += 4;
    bytepixel += 4;
}

会是一个开始。

1
一些非常好的建议。但是它们无法打败查找表。;-) - Konrad Rudolph
1
取决于架构。乘法和转换可能比加载更便宜,特别是如果他可以利用架构的SIMD功能(MMX、SSE、Altivec或其他)在单个指令中对整个像素进行操作。但这个决定可以独立于以上所有建议而做出。 - moonshadow
这将更有助于使编译器的工作变得更容易,而不是真正提高速度。除了对齐指针和启用SIMD - 它可以真正提升性能。 - ima
我接受这个答案,因为它是唯一一个没有提到查找表的答案,而我已经知道了。我只是想要另一种方法,而这就是答案。 - Jorjon
说到在每次迭代中做更多的工作,为什么不预先计算(1.f/255.f)呢?我想这可能会被优化掉,但如果没有其他问题的话,这样做会更加清晰。 - Geobits
编译器将为在编译时完全已知的表达式生成一个单一的常量,它不会在运行时计算。以这种方式编写代码比引入额外的行和变量略微更清晰,虽然这是主观的。 - moonshadow

8

我知道这是一个老问题,但由于没有人使用IEEE浮点表示法提供解决方案,所以在这里提供一个。

// Use three unions instead of one to avoid pipeline stalls
union { float f; uint32_t i; } t, u, v, w;
t.f = 32768.0f;
float const b = 256.f / 255.f;

for(int size = width * height; size > 0; --size)
{
    u.i = t.i | bytepixel[0]; floatpixel[0] = (u.f - t.f) * b;
    v.i = t.i | bytepixel[1]; floatpixel[1] = (v.f - t.f) * b;
    w.i = t.i | bytepixel[2]; floatpixel[2] = (w.f - t.f) * b;
    floatpixel[3] = 1.0f; // A
    floatpixel += 4;
    bytepixel += 4;
}

以下代码比我的计算机(Core 2 Duo CPU)上的int到float转换快了两倍以上

以下是上面代码的SSE3版本,每次可处理16个浮点数。它要求bytepixelfloatpixel为128位对齐,并且总大小是4的倍数。请注意,SSE3内置的int到float转换在这里并没有太大帮助,因为它们仍需要进行额外的乘法运算。我认为这是指令方面最简短的方式,但如果你的编译器不够聪明,你可能需要手动展开和调度。

/* Magic values */
__m128i zero = _mm_set_epi32(0, 0, 0, 0);
__m128i magic1 = _mm_set_epi32(0xff000000, 0xff000000, 0xff000000, 0xff000000);
__m128i magic2 = _mm_set_epi32(0x47004700, 0x47004700, 0x47004700, 0x47004700);
__m128 magic3 = _mm_set_ps(32768.0f, 32768.0f, 32768.0f, 32768.0f);
__m128 magic4 = _mm_set_ps(256.0f / 255.0f, 256.0f / 255.0f, 256.0f / 255.0f, 256.0f / 255.0f);

for(int size = width * height / 4; size > 0; --size)
{
    /* Load bytes in vector and force alpha value to 255 so that
     * the output will be 1.0f as expected. */
    __m128i in = _mm_load_si128((__m128i *)bytepixel);
    in = _mm_or_si128(in, magic1);

    /* Shuffle bytes into four ints ORed with 32768.0f and cast
     * to float (the cast is free). */
    __m128i tmplo = _mm_unpacklo_epi8(in, zero);
    __m128i tmphi = _mm_unpackhi_epi8(in, zero);
    __m128 in1 = _mm_castsi128_ps(_mm_unpacklo_epi16(tmplo, magic2));
    __m128 in2 = _mm_castsi128_ps(_mm_unpackhi_epi16(tmplo, magic2));
    __m128 in3 = _mm_castsi128_ps(_mm_unpacklo_epi16(tmphi, magic2));
    __m128 in4 = _mm_castsi128_ps(_mm_unpackhi_epi16(tmphi, magic2));

    /* Subtract 32768.0f and multiply by 256.0f/255.0f */
    __m128 out1 = _mm_mul_ps(_mm_sub_ps(in1, magic3), magic4);
    __m128 out2 = _mm_mul_ps(_mm_sub_ps(in2, magic3), magic4);
    __m128 out3 = _mm_mul_ps(_mm_sub_ps(in3, magic3), magic4);
    __m128 out4 = _mm_mul_ps(_mm_sub_ps(in4, magic3), magic4);

    /* Store 16 floats */
    _mm_store_ps(floatpixel, out1);
    _mm_store_ps(floatpixel + 4, out2);
    _mm_store_ps(floatpixel + 8, out3);
    _mm_store_ps(floatpixel + 12, out4);

    floatpixel += 16;
    bytepixel += 16;
}

编辑:使用(f + c/b) * b代替f * b + c以提高准确性。

编辑:添加SSE3版本。


现在,使用SSE指令集也可以完成这个任务吗?这看起来像是一个典型的SIMD代码示例。(当然,原始代码也是如此...) - Konrad Rudolph
是的!SSE有限的洗牌功能,但它们在这里可能很有用。 - sam hocevar

2
你需要找出瓶颈在哪里:
  • 如果你以“错误”的方向迭代数据表,你会不断遇到缓存未命中。没有任何查找能帮助解决这个问题。
  • 如果你的处理器在扩展方面比查找快得慢,你可以通过查找来提高性能,前提是查找表适合缓存。
另一个提示:
struct Scale {
    BYTE operator()( const float f ) const { return f * 1./255; }
};
std::transform( float_table, float_table + itssize, floatpixel, Scale() );

2
请使用静态查找表来实现。在我曾经在一家计算机图形公司工作时,我们最终采用了一个硬编码的查找表,并将其与项目链接起来。

1

查找表是最快的转换方式 :) 这里是:

生成 byte_to_float.h 文件的 Python 代码:

#!/usr/bin/env python

def main():
    print "static const float byte_to_float[] = {"

    for ii in range(0, 255):
        print "%sf," % (ii/255.0)

    print "1.0f };"    
    return 0

if __name__ == "__main__":
    main()

获取转换的C++代码:

floatpixel[0] = byte_to_float[ bytepixel[0] ];

很简单吧?


1

是的,查找表肯定比在循环中进行大量除法要快。只需生成一个256个预计算浮点值的表格,并使用字节值索引该表格即可。

您还可以通过删除索引计算并执行类似以下操作来优化循环:

float *floatpixel = floatbuffer;
BYTE *bytepixel = bytebuffer;

for (...) {
  *floatpixel++ = float_table[*bytepixel++];
  *floatpixel++ = float_table[*bytepixel++];
  *floatpixel++ = float_table[*bytepixel++];
  *floatpixel++ = 1.0f;
}

0

不要每次计算1/255。无法确定编译器是否足够聪明以删除此操作。计算一次并在每次需要时重新应用它。更好的做法是将其定义为常量。


编译器执行常量折叠,因此这不是一个问题。 - Konrad Rudolph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接