C#でBitmapオブジェクトとbyte配列を変換する。

コンピュータ

C#で画像処理をしていると画像をPixel単位で加工したい状況に遭遇します。
いくつか方法はあるようですがC#でも比較的高速に動作するbyte配列に変換し加工する方法を試してみます。

using System.Drawing.Imaging;

namespace BitmapToBytes;

public partial class Form1 : Form
{
    // Bitmapオブジェクトをbyte配列に変換
    static public byte[] BitmapToBytes(Bitmap src)
    {
        var bmpData = src.LockBits(
            new Rectangle(0, 0, src.Width, src.Height),
            ImageLockMode.ReadOnly,
            src.PixelFormat);
        
        byte[] bytes = new byte[Math.Abs(bmpData.Stride) * src.Height];

        System.Runtime.InteropServices.Marshal.Copy(
            bmpData.Scan0,
            bytes, 0, bytes.Length);
        
        src.UnlockBits(bmpData);

        return bytes;
    }
    // byte配列をにBitmapオブジェクト変換
    static public Bitmap BytesToBitmap(byte[] bytes, int width, int height, PixelFormat format)
    {
        var dst = new Bitmap(width, height, format);

        var bmpData = dst.LockBits(
            new Rectangle(0, 0, dst.Width, dst.Height),
            ImageLockMode.ReadWrite,
            dst.PixelFormat);

        System.Runtime.InteropServices.Marshal.Copy(
            bytes,
            0, bmpData.Scan0,
            bytes.Length);
        dst.UnlockBits(bmpData);

        return dst;
    }
    public Form1()
    {
        InitializeComponent();

        var bmp = new Bitmap(@"H:\202211181317.PNG");

        byte[] bytes = BitmapToBytes(bmp);

        int channel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
        int stride = bytes.Length / bmp.Height;
        // int stride = (bmp.Width * (8 * channel) + 31) / 32 * 4;

        // 平均値フィルタ 3x3
        double[,] kernel = new double[,] {
            {1.0/9.0, 1.0/9.0, 1.0/9.0,},
            {1.0/9.0, 1.0/9.0, 1.0/9.0,},
            {1.0/9.0, 1.0/9.0, 1.0/9.0,},
        };
        for (int c = 0; c < channel; c++)
        {
            for (int y = 1; y < (bmp.Height-1); y++)
            {
                for (int x = 1; x < (bmp.Width-1); x++)
                {
                    double avg = 0.0;
                    avg += bytes[(y-1) * stride + (x-1) * channel + c] * kernel[0,0];
                    avg += bytes[(y-1) * stride + (x+0) * channel + c] * kernel[0,1];
                    avg += bytes[(y-1) * stride + (x+1) * channel + c] * kernel[0,2];
                    avg += bytes[(y+0) * stride + (x-1) * channel + c] * kernel[1,0];
                    avg += bytes[(y+0) * stride + (x+0) * channel + c] * kernel[1,1];
                    avg += bytes[(y+0) * stride + (x+1) * channel + c] * kernel[1,2];
                    avg += bytes[(y+1) * stride + (x-1) * channel + c] * kernel[2,0];
                    avg += bytes[(y+1) * stride + (x+0) * channel + c] * kernel[2,1];
                    avg += bytes[(y+1) * stride + (x+1) * channel + c] * kernel[2,2];
                    bytes[y * stride + x * channel + c] = (byte)avg;
                }
            }
        }

        Bitmap bmp2 = BytesToBitmap(bytes, bmp.Width, bmp.Height, bmp.PixelFormat);

        PictureBox picbox = new()
        {
            SizeMode = PictureBoxSizeMode.AutoSize,
            Image = bmp2,
        };
        Panel panel = new()
        {
            AutoScroll = true,
            Dock = DockStyle.Fill,
        };
        panel.Controls.Add(picbox);
        Controls.Add(panel);
    }
}

BitmapToBytes()でBitmapオブジェクトからbyte配列を生成します。
生成したbyte配列は画像のPixelに対応しますので、byte配列のデータを変更すると画像に反映します。
今回は動作確認のために、畳み込み演算のようなもので平均化フィルタを作成してみました。
加工後、ByteToBitmap()でbyte配列からBitmapオブジェクトを生成しています。

平均化フィルタにより画像がボケた感じになっています。
あと、画像の縁の1ピクセル分がフィルタの対象になっていません。元のPixelで補完するとか黒か白など任意の色で埋めるなり処理が必要になります。

雑記

追記:20230612

今回の記事では、オブジェクトや配列など抽象的な概念を操作するのではなく、メモリを操作している感じになります。この類の操作は実行速度が重視されるのでネイティブな実行ファイルを出力できる言語の方が(C言語とか)が向いているとは思うのですが、C#の手軽さを知ってしまうと全てをC#で書きたい衝動に駆られます。

C#を使うとしてもOpenCVSharpなどのグラフィックライブラリがありますので、こちらを使ったほうが良いかもしれません。

コメント