繁体   English   中英

如何使用矢量 SSE 操作将图像像素数据的字节数组转换为灰度

[英]How to convert byte array of image pixels data to grayscale using vector SSE operation

我在将存储在byte[] array中的图像数据转换为灰度时遇到问题。 我想使用矢量 SIMD 操作,因为将来需要编写 ASM 和 C++ DLL 文件来测量操作时间。

当我阅读有关 SIMD 的信息时,我发现 SSE 命令是在 128 位寄存器上运行的,所以存在一个问题,因为我需要将我的byte[] array转换为存储到List<T>.中的几个Vector<T> T>。

图像是四通道 RGBA JPEG,所以我还需要知道如何使用基于单个 128 位Vector<T>的 R、G、B 数据创建矢量。 之后,我可以使用灰度算法

fY(R, G, B) = R x 0.29891 + G x 0.58661 + B x 0.11448


总而言之,问题是:

  1. 如何将byte[] array块加载到 128 位寄存器Vector<T>中。
  2. 如何为一个Vector<T>分离 R、G、B 值以将其相乘并复制到源 Vector。

它需要 System.Runtime.Intrinsics.Experimental.dll 并且不安全,但它相对简单,并且对于许多实际应用来说可能足够快。

/// <summary>Load 4 pixels of RGB</summary>
static unsafe Vector128<int> load4( byte* src )
{
    return Sse2.LoadVector128( (int*)src );
}

/// <summary>Pack red channel of 8 pixels into ushort values in [ 0xFF00 .. 0 ] interval</summary>
static Vector128<ushort> packRed( Vector128<int> a, Vector128<int> b )
{
    Vector128<int> mask = Vector128.Create( 0xFF );
    a = Sse2.And( a, mask );
    b = Sse2.And( b, mask );
    return Sse2.ShiftLeftLogical128BitLane( Sse41.PackUnsignedSaturate( a, b ), 1 );
}

/// <summary>Pack green channel of 8 pixels into ushort values in [ 0xFF00 .. 0 ] interval</summary>
static Vector128<ushort> packGreen( Vector128<int> a, Vector128<int> b )
{
    Vector128<int> mask = Vector128.Create( 0xFF00 );
    a = Sse2.And( a, mask );
    b = Sse2.And( b, mask );
    return Sse41.PackUnsignedSaturate( a, b );
}

/// <summary>Pack blue channel of 8 pixels into ushort values in [ 0xFF00 .. 0 ] interval</summary>
static Vector128<ushort> packBlue( Vector128<int> a, Vector128<int> b )
{
    a = Sse2.ShiftRightLogical128BitLane( a, 1 );
    b = Sse2.ShiftRightLogical128BitLane( b, 1 );
    Vector128<int> mask = Vector128.Create( 0xFF00 );
    a = Sse2.And( a, mask );
    b = Sse2.And( b, mask );
    return Sse41.PackUnsignedSaturate( a, b );
}

/// <summary>Load 8 pixels, split into RGB channels.</summary>
static unsafe void loadRgb( byte* src, out Vector128<ushort> red, out Vector128<ushort> green, out Vector128<ushort> blue )
{
    var a = load4( src );
    var b = load4( src + 16 );
    red = packRed( a, b );
    green = packGreen( a, b );
    blue = packBlue( a, b );
}

const ushort mulRed = (ushort)( 0.29891 * 0x10000 );
const ushort mulGreen = (ushort)( 0.58661 * 0x10000 );
const ushort mulBlue = (ushort)( 0.11448 * 0x10000 );

/// <summary>Compute brightness of 8 pixels</summary>
static Vector128<short> brightness( Vector128<ushort> r, Vector128<ushort> g, Vector128<ushort> b )
{
    r = Sse2.MultiplyHigh( r, Vector128.Create( mulRed ) );
    g = Sse2.MultiplyHigh( g, Vector128.Create( mulGreen ) );
    b = Sse2.MultiplyHigh( b, Vector128.Create( mulBlue ) );
    var result = Sse2.AddSaturate( Sse2.AddSaturate( r, g ), b );
    return Vector128.AsInt16( Sse2.ShiftRightLogical( result, 8 ) );
}

/// <summary>Convert buffer from RGBA to grayscale.</summary>
/// <remarks>
/// <para>If your image has line paddings, you'll want to call this once per line, not for the complete image.</para>
/// <para>If width of the image is not multiple of 16 pixels, you'll need to do more work to handle the last few pixels of every line.</para>
/// </remarks>
static unsafe void convertToGrayscale( byte* src, byte* dst, int count )
{
    byte* srcEnd = src + count * 4;
    while( src < srcEnd )
    {
        loadRgb( src, out var r, out var g, out var b );
        var low = brightness( r, g, b );
        loadRgb( src + 32, out r, out g, out b );
        var hi = brightness( r, g, b );

        var bytes = Sse2.PackUnsignedSaturate( low, hi );
        Sse2.Store( dst, bytes );

        src += 64;
        dst += 16;
    }
}

但是,等效的 C++ 实现会更快。 C# 在内联这些函数方面做得不错,即convertToGrayscale不包含 function 调用。 但是 function 的代码远非最佳。 .NET 无法传播常量,因为它在循环中发出如下代码的幻数:

mov         r8d,962Ch
vmovd       xmm1,r8d
vpbroadcastw xmm1,xmm1

生成的代码仅使用 16 个寄存器中的 6 个。 对于所有涉及的幻数,有足够的可用寄存器。

此外 .NET 发出许多冗余指令,这些指令只是将数据打乱:

vmovaps xmm2, xmm0
vmovaps xmm3, xmm1

与其他答案相比,精度略低,可以使用 7 位定点标度代替 16 位定点标度,从而可以使用 PMADDUBSW。 这也不需要在乘法之前进行任何改组。 那么 PMADDWD 可以被滥用为成对的水平加法,所以乘法之后仍然没有洗牌。 这具有相对较差的延迟,但这会被指令级并行性所隐藏,CPU 不仅仅是 go 坐在那里什么都不做。

由于此代码应该写入与读取不同的缓冲区,因此如果剩下的像素少于 16 个,则可以安全地使用“后退并执行一次未对齐的迭代”来处理最后一个像素块.

我将蓝色权重更改为 128 * 0.118,因为它的结果为 15,更接近 14.65344(未舍入的缩放权重)。 此外,让它向下舍入到 14 会使总权重为 127,这意味着稍后除以 128 会失去亮度。

全部加起来,

static unsafe void convertToGrayscale(byte* src, byte* dst, int count)
{
    int countMain = count & -16;
    byte* srcEnd = src + countMain * 4;
    byte* srcRealEnd = src + count * 4;
    byte* dstRealEnd = dst + count;
    sbyte scaleR = (sbyte)(128 * 0.29891);
    sbyte scaleG = (sbyte)(128 * 0.58661);
    sbyte scaleB = (sbyte)(128 * 0.118);
    Vector128<sbyte> scales = Vector128.Create(scaleR, scaleG, scaleB, 0, scaleR, scaleG, scaleB, 0, scaleR, scaleG, scaleB, 0, scaleR, scaleG, scaleB, 0);
    Vector128<short> ones = Vector128.Create((short)1);
    do
    {
        while (src < srcEnd)
        {
            var block0 = Sse2.LoadVector128(src);
            var block1 = Sse2.LoadVector128(src + 16);
            var block2 = Sse2.LoadVector128(src + 32);
            var block3 = Sse2.LoadVector128(src + 48);
            var scaled0 = Ssse3.MultiplyAddAdjacent(block0, scales);
            var scaled1 = Ssse3.MultiplyAddAdjacent(block1, scales);
            var scaled2 = Ssse3.MultiplyAddAdjacent(block2, scales);
            var scaled3 = Ssse3.MultiplyAddAdjacent(block3, scales);
            var t0 = Sse2.MultiplyAddAdjacent(scaled0, ones);
            var t1 = Sse2.MultiplyAddAdjacent(scaled1, ones);
            var t2 = Sse2.MultiplyAddAdjacent(scaled2, ones);
            var t3 = Sse2.MultiplyAddAdjacent(scaled3, ones);
            var c01 = Sse2.PackSignedSaturate(t0, t1);
            c01 = Sse2.ShiftRightLogical(c01, 7);
            var c23 = Sse2.PackSignedSaturate(t2, t3);
            c23 = Sse2.ShiftRightLogical(c23, 7);
            var c0123 = Sse2.PackUnsignedSaturate(c01, c23);
            Sse2.Store(dst, c0123);
            src += 64;
            dst += 16;
        }
        // hack to re-use the main loop for the "tail"
        if (src == srcRealEnd)
            break;
        srcEnd = srcRealEnd;
        src = srcRealEnd - 64;
        dst = dstRealEnd - 16;
    } while (true);
}

在我的 PC 上,这大约是基于 PMULHUW 的解决方案的两倍。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM