C#で画像変換で見る並列処理の効果「グレースケール化」

コンピュータ

for文とParallel.Forで処理時間を比較してみたいと思います。

プロジェクトの作成

cd (mkdir ConvertToGray)
dotnet new console -f net8.0

ソースコード

ファイル名:ConvertToGray.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:WpfBitmapUtility.cs

// WPFのBitmap関連

using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace CommonLib;

public static class WpfBitmapUtility
{
    public static BitmapSource LoadBitmap(string path)
    {
        using FileStream fs = new(path, FileMode.Open, FileAccess.Read);
        var decoder = BitmapDecoder.Create(fs, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
        var frame = decoder.Frames[0];
        return new FormatConvertedBitmap(frame, PixelFormats.Bgra32, null, 0); // 32bpp固定
    }

    public static void SaveBitmap(BitmapSource bitmap, string path)
    {
        using FileStream fs = new(path, FileMode.Create);
        var encoder = new PngBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(bitmap));
        encoder.Save(fs);
    }

    public static BitmapSource ConvertToGray(BitmapSource source, bool useParallel)
    {
        int width = source.PixelWidth;
        int height = source.PixelHeight;
        int stride = width * 4;
        byte[] pixels = new byte[height * stride];
        source.CopyPixels(pixels, stride, 0);

        Action<int> processLine = y =>
        {
            int offset = y * stride;
            for (int x = 0; x < width; x++)
            {
                int i = offset + x * 4;
                byte b = pixels[i + 0];
                byte g = pixels[i + 1];
                byte r = pixels[i + 2];
                byte gray = (byte)((r * 0.299 + g * 0.587 + b * 0.114));
                pixels[i + 0] = gray;
                pixels[i + 1] = gray;
                pixels[i + 2] = gray;
                // Alphaはそのまま
            }
        };

        if (useParallel)
            Parallel.For(0, height, processLine);
        else
            for (int y = 0; y < height; y++) processLine(y);

        return BitmapSource.Create(width, height, source.DpiX, source.DpiY, PixelFormats.Bgra32, null, pixels, stride);
    }
}

ファイル名:Program.cs

// コンソールアプリのエントリポイント

using System.Diagnostics;
using System.Windows.Media.Imaging;
using CommonLib;

// namespace ;
class Program
{
    static void Main()
    {
        string inputPath = @"J:\testdata\00001un.png";
        string outputPath1 = "./gray_for.png";
        string outputPath2 = "./gray_parallel.png";

        var sw = Stopwatch.StartNew();
        BitmapSource source = WpfBitmapUtility.LoadBitmap(inputPath);
        
        Console.WriteLine("=== グレースケール変換(for)===");

        sw.Restart();
        BitmapSource gray1 = WpfBitmapUtility.ConvertToGray(source, useParallel: false);
        sw.Stop();
        Console.WriteLine($"for時間: {sw.ElapsedMilliseconds}ms");
        WpfBitmapUtility.SaveBitmap(gray1, outputPath1);

        Console.WriteLine("=== グレースケール変換(Parallel.For)===");

        sw.Restart();
        BitmapSource gray2 = WpfBitmapUtility.ConvertToGray(source, useParallel: true);
        sw.Stop();
        Console.WriteLine($"Parallel.For時間: {sw.ElapsedMilliseconds}ms");
        WpfBitmapUtility.SaveBitmap(gray2, outputPath2);
        
    }
}

実行結果

dotnet run

#=== グレースケール変換(for)===
#for時間: 201ms
#=== グレースケール変換(Parallel.For)===
#Parallel.For時間: 131ms

通常のforが201msに対し並列のParallel.Forが131msで65%程度に処理時間が短縮されました。

画像の解像度は幅高さは4512×6400でRGBAの32bit
実行したPCはRyzen7 5700X 8コア16スレッド

コメント