C#で画像をグレースケールに変換するCLIコマンド

コンピュータ
Windowsのエクスプローラーの「送る」からカラー画像をグレースケールに変換するプログラムです。

実行環境構築

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new console
dotnet add package System.Drawing.Common

ソースプログラム

using System.Drawing;
using System.Drawing.Imaging;

public class Program1
{
    // グレースケールへ変換
    public static Bitmap? ToGray(Bitmap bmp)
    {
        if (!System.OperatingSystem.IsWindows()) return null;

        var width = bmp.Width;
        var height = bmp.Height;

        var dst = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
        var dstData = dst.LockBits(
            new Rectangle(0, 0, width, height),
            ImageLockMode.ReadWrite,
            dst.PixelFormat
        );
        var dstStride = Math.Abs(dstData.Stride);
        var dstPicData = new byte[dstStride * dstData.Height];
        System.Runtime.InteropServices.Marshal.Copy(
            dstData.Scan0,
            dstPicData,
            0,
            dstStride * dstData.Height
        );


        var bmpData = bmp.LockBits(
            new Rectangle(0,0,width, height),
            ImageLockMode.ReadWrite,
            bmp.PixelFormat      
        );
        var stride = Math.Abs(bmpData.Stride);
        int channel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
        var picData = new byte[stride * bmpData.Height];
        System.Runtime.InteropServices.Marshal.Copy(
            bmpData.Scan0,
            picData,
            0,
            stride * bmpData.Height
        );

        Parallel.For(0, height, y =>
        {
            int lineIndex = stride * y;
            int dstIndex = dstStride * y;
            for (int x = 0; x < width; x++)
            {
                var value = picData[lineIndex + x * channel] * 0.299;
                value += picData[lineIndex + x * channel + 1] * 0.587;
                value += picData[lineIndex + x * channel + 2] * 0.114;

                dstPicData[dstIndex + x] = (byte)value;
            }
        });

        System.Runtime.InteropServices.Marshal.Copy(
            dstPicData,
            0,
            dstData.Scan0,
            dstStride * dstData.Height
        );

        bmp.UnlockBits(bmpData);
        dst.UnlockBits(dstData);
        
        var pallet = dst.Palette;
        for(int i = 0; i < 255; i++)
        {
            pallet.Entries[i] = Color.FromArgb(i, i, i);
        }
        dst.Palette = pallet;

        return dst;
    }

    public static void Main(string[] args)
    {
        if (!OperatingSystem.IsWindows()) return;

        foreach(var srcFile in args)
        {
            Bitmap? src = null;
            Bitmap? dst = null;
            using (var fs = new FileStream(srcFile, FileMode.Open))
            {
                src = new Bitmap(fs);
            }

            if (src is not null)
            {
                dst = ToGray(src);

                if (dst is not null)
                {
                    var dir = Path.GetDirectoryName(srcFile);
                    var file = Path.GetFileNameWithoutExtension(srcFile);
                    var dstFile = Path.Join(dir, file+".png");

                    dst.Save(dstFile, System.Drawing.Imaging.ImageFormat.Png);
                }
            }
            src?.Dispose();
        }
    }
}

ビルド

dotnet build -c Release

使い方

ビルドで作成された実行ファイルをエクスプローラーの「送る」にショートカットを作成
試したところ複数のファイルを「送る」処理にも対応していました。

コメント