C#で画像処理を高速化する方法(ParallelとSIMDの検証)

コンピュータ

WPFのBitmapSourceをbyte[]へ変換するコンバーターを使って、画像処理の高速化を試してみました。

比較的計算がシンプルなグレースケール化を並列処理で、高速化する試みです。

マルチコアCPUによる並列処理で高速化が見込める処理は限られています。
CPUとメモリだけで完結し、外部I/Oの待ち時間が少なく、さらに一定の計算量がある処理であることが重要です。

その条件を満たす分野として、画像処理は非常に相性が良い処理の一つです。
多くの画像処理アルゴリズムはピクセル単位で独立した計算を行えるため、処理を分割しやすく、マルチコアCPUによる並列化の効果が出やすい特徴があります。

グレースケール化(通常版)

/ グレースケール化(通常版)
public static ImageBufferHelper.ImageBuffer ToGrayNormal(ImageBufferHelper.ImageBuffer src)
{
    int width = src.Width;
    int height = src.Height;

    var dst = ImageBufferHelper.Create(width, height, 1);

    byte[] sp = src.Pixels;
    byte[] dp = dst.Pixels;

    int si = 0;
    int di = 0;

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            byte r = sp[si];
            byte g = sp[si + 1];
            byte b = sp[si + 2];

            int gray = (299 * r + 587 * g + 114 * b) / 1000;

            dp[di] = (byte)gray;

            si += 3;
            di++;
        }
    }

    return dst;
}

グレースケール化(Parallel版)

// グレースケール化(Parallel版)
public static ImageBufferHelper.ImageBuffer ToGrayParallel(ImageBufferHelper.ImageBuffer src)
{
    int width = src.Width;
    int height = src.Height;

    var dst = ImageBufferHelper.Create(width, height, 1);

    byte[] sp = src.Pixels;
    byte[] dp = dst.Pixels;

    Parallel.For(0, height, y =>
    {
        int si = y * src.Stride;
        int di = y * width;

        for (int x = 0; x < width; x++)
        {
            byte r = sp[si];
            byte g = sp[si + 1];
            byte b = sp[si + 2];

            int gray = (299 * r + 587 * g + 114 * b) / 1000;

            dp[di] = (byte)gray;

            si += 3;
            di++;
        }
    });

    return dst;
}

グレースケール化(SIMD版)

// グレースケール化(SIMD版)
using System.Numerics;

public static ImageBufferHelper.ImageBuffer ToGraySimd(ImageBufferHelper.ImageBuffer src)
{
    int width = src.Width;
    int height = src.Height;

    var dst = ImageBufferHelper.Create(width, height, 1);

    byte[] sp = src.Pixels;
    byte[] dp = dst.Pixels;

    int simd = Vector<float>.Count;
    int i = 0;

    var wr = new Vector<float>(0.299f);
    var wg = new Vector<float>(0.587f);
    var wb = new Vector<float>(0.114f);

    while (i <= dp.Length - simd)
    {
        float[] rf = new float[simd];
        float[] gf = new float[simd];
        float[] bf = new float[simd];

        for (int j = 0; j < simd; j++)
        {
            int p = (i + j) * 3;

            rf[j] = sp[p];
            gf[j] = sp[p + 1];
            bf[j] = sp[p + 2];
        }

        var vr = new Vector<float>(rf);
        var vg = new Vector<float>(gf);
        var vb = new Vector<float>(bf);

        var gray = vr * wr + vg * wg + vb * wb;
        
        float[] tmp = new float[simd];
        gray.CopyTo(tmp);
        
        for (int j = 0; j < simd; j++)
        {
            dp[i + j] = (byte)tmp[j];
        }

        i += simd;
    }

    // 余り処理
    for (; i < dp.Length; i++)
    {
        int p = i * 3;

        byte r = sp[p];
        byte g = sp[p + 1];
        byte b = sp[p + 2];

        dp[i] = (byte)(0.299f * r + 0.587f * g + 0.114f * b);
    }
    return dst;
}

ベンチマーク

// ベンチマーク
static void Benchmark(ImageBufferHelper.ImageBuffer img)
{
    var sw = new Stopwatch();

    sw.Start();
    ToGrayNormal(img);
    sw.Stop();
    Console.WriteLine($"Normal  : {sw.ElapsedMilliseconds} ms");

    sw.Restart();
    ToGrayParallel(img);
    sw.Stop();
    Console.WriteLine($"Parallel: {sw.ElapsedMilliseconds} ms");

    sw.Restart();
    ToGraySimd(img);
    sw.Stop();
    Console.WriteLine($"SIMD    : {sw.ElapsedMilliseconds} ms");

}

実行結果


Normal  : 104 ms

Parallel: 82 ms

SIMD    : 375 ms

C#のSIMD(System.Numerics.Vector)を使ったコードとしては動作させることができましたが、実装はかなり複雑で、現時点では高速化の恩恵を引き出すところまでには至っていません。

おそらく工夫すればもっと高速な実装は可能だと思いますが、今回はそこまで深追いせず、このあたりで一区切りとしたいと思います。

コメント