如何最有效地修改R/G/B值?

How to most efficiently modify R / G / B values?

所以我想在我的基于像素的渲染系统中实现照明,用谷歌搜索并发现显示 R / G / B 值更亮或更暗我必须将每个红色绿色和蓝色值乘以一个 < 1 的数字才能显示它更暗,数字 > 1 显示更亮。

所以我是这样实现的,但它确实拖累了我的性能,因为我必须对每个像素都这样做:

void PixelRenderer::applyLight(Uint32& color){
    Uint32 alpha = color >> 24;
    alpha << 24;
    alpha >> 24;

    Uint32 red = color >> 16;
    red = red << 24;
    red = red >> 24;

    Uint32 green = color >> 8;
    green = green << 24;
    green = green >> 24;

    Uint32 blue = color;
    blue = blue << 24;
    blue = blue >> 24;

    red = red * 0.5;
    green = green * 0.5;
    blue = blue * 0.5;
    color = alpha << 24 | red << 16 | green << 8 | blue;
}

关于如何提高速度有什么想法或例子吗?

试试这个:(编辑:事实证明,这只是提高了可读性,但请继续阅读以获得更多见解。)

void PixelRenderer::applyLight(Uint32& color)
{
    Uint32 alpha = color >> 24;
    Uint32 red = (color >> 16) & 0xff;
    Uint32 green = (color >> 8) & 0xff;
    Uint32 blue = color & 0xff;
    red = red * 0.5;
    green = green * 0.5;
    blue = blue * 0.5;
    color = alpha << 24 | red << 16 | green << 8 | blue;
}

话虽如此,您应该明白,使用计算机的 CPU 等通用处理器执行此类操作肯定会非常慢。这就是发明硬件加速显卡的原因。

编辑

如果你非要这样操作,那么为了提高效率,恐怕就得借助hack了。处理 8 位通道值时经常使用的一种 hack 是查找 tables。通过查找 table,您可以预先计算一个包含 256 个值的数组,而不是将每个单独的通道值乘以浮点数,其中数组的索引是一个通道值,该索引中的值是乘法的预先计算结果该浮点数的通道值。然后,在转换图像时,您只需使用通道值来查找数组的条目,而不是执行实际的浮点乘法。这要快得多。 (但仍然不如编程专用、大规模并行硬件为您完成这些工作快。)

编辑

正如其他人已经指出的那样,如果您不打算在 alpha 通道上进行操作,则无需将其提取然后再应用,您可以保持不变。所以,你可以做 color = (color & 0xff000000) | red << 16 | green << 8 | blue;

要保留前面的 alpha 值,请使用:

(color>>1)&0x7F7F7F | (color&0xFF000000)

(对 Wimmel 在评论中提供的内容进行调整)。

我认为这里的 'learning curve' 是您使用 shift 和 shift back 来屏蔽位。您应该使用带有掩码值的 &

对于更通用的解决方案(其中 0.0<=factor<=1.0):

void PixelRenderer::applyLight(Uint32& color, double factor){
    Uint32 alpha=color&0xFF000000;
    Uint32 red=  (color&0x00FF0000)*factor;
    Uint32 green= (color&0x0000FF00)*factor;
    Uint32 blue=(color&0x000000FF)*factor;

   color=alpha|(red&0x00FF0000)|(green&0x0000FF00)|(blue&0x000000FF);
}

注意在执行乘法之前不需要将组件向下移动到低位。

最终你可能会发现瓶颈是浮点转换和算术。

要减少这种情况,您应该考虑:

  1. 将其缩小为一个比例因子,例如在 0-256 范围内。

  2. factor*component 预计算为 256 个元素的数组并且 'pick' 组件不正确。

我建议的范围是 257,因为您可以按如下方式实现该系数:

对于更通用的解决方案(其中 0<=factor<=256):

void PixelRenderer::applyLight(Uint32& color, Uint32 factor){
    Uint32 alpha=color&0xFF000000;
    Uint32 red=  ((color&0x00FF0000)*factor)>>8;
    Uint32 green= ((color&0x0000FF00)*factor)>>8;
    Uint32 blue=((color&0x000000FF)*factor)>>8;

    color=alpha|(red&0x00FF0000)|(green&0x0000FF00)|(blue&0x000000FF);
}

这是说明第一个示例的可运行程序:

#include <stdio.h>
#include <inttypes.h>

typedef uint32_t Uint32;

Uint32 make(Uint32 alpha,Uint32 red,Uint32 green,Uint32 blue){
    return (alpha<<24)|(red<<16)|(green<<8)|blue;
}

void output(Uint32 color){
    printf("alpha=%"PRIu32" red=%"PRIu32" green=%"PRIu32" blue=%"PRIu32"\n",(color>>24),(color&0xFF0000)>>16,(color&0xFF00)>>8,color&0xFF);
}

Uint32 applyLight(Uint32 color, double factor){
    Uint32 alpha=color&0xFF000000;
    Uint32 red=  (color&0x00FF0000)*factor;
    Uint32 green= (color&0x0000FF00)*factor;
    Uint32 blue=(color&0x000000FF)*factor;

    return alpha|(red&0x00FF0000)|(green&0x0000FF00)|(blue&0x000000FF);
}

int main(void) {
    Uint32 color1=make(156,100,50,20);
    Uint32 result1=applyLight(color1,0.9);
    output(result1);

    Uint32 color2=make(255,255,255,255);
    Uint32 result2=applyLight(color2,0.1);
    output(result2);

    Uint32 color3=make(78,220,200,100);
    Uint32 result3=applyLight(color3,0.05);
    output(result3);

    return 0;
}

预期输出为:

alpha=156 red=90 green=45 blue=18
alpha=255 red=25 green=25 blue=25
alpha=78 red=11 green=10 blue=5

像这样的移位和掩码在现代处理器上通常非常快。我可能会看看其他一些东西:

  1. 遵循优化的第一条规则 - 分析您的代码。您可以简单地通过调用该方法数百万次并对其计时来完成此操作。您的计算速度慢,还是其他原因?什么是慢?尝试省略部分方法 - 事情会加速吗?
  2. 确保此函数声明为内联(并确保它实际上已被内联)。函数调用开销将大大超过像素操作(特别是如果它是虚拟的)。
  3. 考虑声明您的方法 Uint32 PixelRenderer::applyLight(Uint32 color) 并返回修改后的值,这可能有助于避免一些取消引用并为编译器提供一些额外的优化机会。
  4. 避免 fp 到整数的转换,它们可能非常昂贵。如果普通整数除法不够,请考虑使用定点数学。

最后,查看汇编程序以查看编译器生成的内容(进行了优化)。是否有任何分支或转换?你的方法真的被内联了吗?

  • 另一种不使用移位器的解决方案是将您的 32 bits uint 转换为 struct.
  • 尽量将您的实现保留在 .h 包含文件中,以便可以内联它
  • 如果您不想内联实现(见上文),请修改您的 applyLight 方法以接受像素数组。对于这么小的方法,方法调用开销可能很大
  • 在您的编译器上启用 "loop unroll" 优化,这将允许使用 SIMD 指令

实施:

class brightness {
private:
    struct pixel { uint8_t b, g, r, a; };
    float factor;

    static inline void apply(uint8_t& p, float f) {
        p = max(min(int(p * f), 255),0);
    }

public:
    brightness(float factor) : factor(factor) { }

    void apply(uint32_t& color){
        pixel& p = (pixel&)color;

        apply(p.b, factor);
        apply(p.g, factor);
        apply(p.r, factor);
    }
};

通过查找 table 实现(使用 "loop unroll" 时速度较慢):

class brightness {

    struct pixel { uint8_t b, g, r, a; };

    uint8_t table[256];

public:
    brightness(float factor) {
        for(int i = 0; i < 256; i++)
            table[i] = max(min(int(i * factor), 255), 0);
    }

    void apply(uint32_t& color){
        pixel& p = (pixel&)color;

        p.b = table[p.b];
        p.g = table[p.g];
        p.r = table[p.r];
    }
};




// usage
brightness half_bright(0.5);
uint32_t pixel = 0xffffffff;
half_bright.apply(pixel);

我没有看到其他人提到的一件事是并行化您的代码。至少有两种方法可以做到这一点:SIMD 指令和多线程。

SIMD instructions(如 SSE、AVX 等)同时对多条数据执行相同的数学运算。因此,例如,您可以在 1 条指令中将像素的红色、绿色、蓝色和 alpha 乘以相同的值,如下所示:

vec4 lightValue = vec4(0.5, 0.5, 0.5, 1.0);
vec4 result = vec_Mult(inputPixel, lightValue);

这相当于:

lightValue.red = 0.5;
lightValue.green = 0.5;
lightValue.blue = 0.5;
lightValue.alpha = 1.0;

result.red = inputPixel.red * lightValue.red;
result.green = inputPixel.green * lightValue.green;
result.blue = inputPixel.blue * lightValue.blue;
result.alpha = inputPixel.alpha * lightValue.alpha;

您还可以将图像切割成图块,然后使用多个内核上的线程 运行 一次对多个图块执行增亮操作。如果您使用的是 C++11,则可以使用 std::thread to start multiple threads. Otherwise your OS probably has functionality for threading, such as WinThreads, Grand Central Dispatch, pthreads, boost threads, Threading Building Blocks,等等

您可以将以上两者结合起来,并拥有一次对整个像素进行操作的多线程代码。

如果您想更进一步,可以使用 OpenGL, OpenCL, DirectX, Metal, Mantle, CUDA, or one of the other GPGPU 技术在机器的 GPU 上进行处理。 GPU 通常有数百个内核,可以非常快速地并行处理许多图块,每个图块一次处理整个像素(而不仅仅是通道)。

但更好的选择可能是根本不编写任何代码。极有可能有人已经完成了这项工作,您可以利用它。例如,在 MacOS 上有 CoreImage and the Accelerate framework. On iOS you also have CoreImage, and there's also GPUImage。我确定 Windows、Linux 和您可能正在使用的其他操作系统上有类似的库。