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

标签 c# image-processing vectorization sse simd

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

当我读到 SIMD 时,我发现 SSE 命令是在 128 位寄存器上运行的,所以有一个问题,因为我需要转换我的 byte[] array成几Vector<T>存储到 List<T>.
图像是四 channel RGBA JPEG,所以我还需要知道如何基于单个 128 位使用 R、G、B 数据创建矢量 Vector<T> .之后,我可以使用灰度算法

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



总而言之,问题是:
  • 如何加载 byte[] array 的块进入 128 位寄存器 Vector<T> .
  • 如何分离一个Vector<T> R、G、B 值将其相乘并复制到源向量。
  • 最佳答案

    它需要 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不包含函数调用。
    但是该函数的代码远非最佳。 .NET 未能传播常量,因为它在循环内发出了这样的代码:
    mov         r8d,962Ch
    vmovd       xmm1,r8d
    vpbroadcastw xmm1,xmm1
    

    生成的代码仅使用 16 个寄存器中的 6 个。所有涉及的魔数(Magic Number)都有足够的可用寄存器。

    此外,.NET 会发出许多冗余指令,这些指令只是将数据打乱:
    vmovaps xmm2, xmm0
    vmovaps xmm3, xmm1
    

    关于c# - 如何使用矢量 SSE 操作将图像像素数据的字节数组转换为灰度,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58881359/

    相关文章:

    c# - 我们可以从 C# 中的基类中的子类方法捕获异常吗?

    C# 线程 : Console application in new thread is invisible

    ios - 如何在 iOS 应用程序中重现 Adob​​e Lightroom 的高光和阴影效果

    image-processing - 图像中的自动菌群检测

    python - 在 Python 中骨架化图像

    c++ - 使用 g++ 对带位操作的循环进行自动矢量化

    arrays - 通过在元胞数组中存储为向量的索引对向量的子集求和

    c# - 这个复杂的大东西等于这个吗?或这个?或这个?

    c# - Visual Studio - 单元测试加载项目中的资源

    c - 从 RGB 到 BGRA 的快速矢量化转换