web-dev-qa-db-ja.com

C#でのビットマップの高速作業

ビットマップの各ピクセルにアクセスし、それらを操作し、ビットマップに保存する必要があります。

Bitmap.GetPixel()およびBitmap.SetPixel()を使用すると、プログラムの実行が遅くなります。

Bitmapを_byte[]_にすばやく変換して戻すにはどうすればよいですか?

各ピクセルのRGBAデータを含むlength = (4 * width * height)付きの_byte[]_が必要です。

38
AndreyAkinshin

いくつかの異なる方法で行うことができます。 unsafeを使用してデータに直接アクセスするか、マーシャリングを使用してデータを前後にコピーできます。安全でないコードは高速ですが、マーシャリングには安全でないコードは必要ありません。 パフォーマンス比較 しばらく前にやりました。

ロックビットを使用した完全なサンプルを次に示します。

/*Note unsafe keyword*/
public unsafe Image ThresholdUA(float thresh)
{
    Bitmap b = new Bitmap(_image);//note this has several overloads, including a path to an image

    BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat);

    byte bitsPerPixel = GetBitsPerPixel(bData.PixelFormat);

    /*This time we convert the IntPtr to a ptr*/
    byte* scan0 = (byte*)bData.Scan0.ToPointer();

    for (int i = 0; i < bData.Height; ++i)
    {
        for (int j = 0; j < bData.Width; ++j)
        {
            byte* data = scan0 + i * bData.Stride + j * bitsPerPixel / 8;

            //data is a pointer to the first byte of the 3-byte color data
            //data[0] = blueComponent;
            //data[1] = greenComponent;
            //data[2] = redComponent;
        }
    }

    b.UnlockBits(bData);

    return b;
}

これも同じですが、マーシャリングがあります。

/*No unsafe keyword!*/
public Image ThresholdMA(float thresh)
{
    Bitmap b = new Bitmap(_image);

    BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat);

    /* GetBitsPerPixel just does a switch on the PixelFormat and returns the number */
    byte bitsPerPixel = GetBitsPerPixel(bData.PixelFormat);

    /*the size of the image in bytes */
    int size = bData.Stride * bData.Height;

    /*Allocate buffer for image*/
    byte[] data = new byte[size];

    /*This overload copies data of /size/ into /data/ from location specified (/Scan0/)*/
    System.Runtime.InteropServices.Marshal.Copy(bData.Scan0, data, 0, size);

    for (int i = 0; i < size; i += bitsPerPixel / 8 )
    {
        double magnitude = 1/3d*(data[i] +data[i + 1] +data[i + 2]);

        //data[i] is the first of 3 bytes of color

    }

    /* This override copies the data back into the location specified */
    System.Runtime.InteropServices.Marshal.Copy(data, 0, bData.Scan0, data.Length);

    b.UnlockBits(bData);

    return b;
}
76
davidtbernal

Bitmap.LockBitsメソッドを使用できます。また、並列タスク実行を使用する場合は、System.Threading.Tasks名前空間のParallelクラスを使用できます。以下のリンクには、サンプルと説明があります。

4
turgay

LockBits が必要です。その後、提供するBitmapDataオブジェクトから必要なバイトを抽出できます。

3
David Seiler

より高速ではるかに便利な別の方法があります。 Bitmapコンストラクターを見ると、最後のパラメーターとしてIntPtrをとるコンストラクターが見つかります。そのIntPtrはピクセルデータを保持するためのものです。では、どのように使用しますか?

Dim imageWidth As Integer = 1920
Dim imageHeight As Integer = 1080

Dim fmt As PixelFormat = PixelFormat.Format32bppRgb
Dim pixelFormatSize As Integer = Image.GetPixelFormatSize(fmt)

Dim stride As Integer = imageWidth * pixelFormatSize
Dim padding = 32 - (stride Mod 32)
If padding < 32 Then stride += padding

Dim pixels((stride \ 32) * imageHeight) As Integer
Dim handle As GCHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned)
Dim addr As IntPtr = Marshal.UnsafeAddrOfPinnedArrayElement(pixels, 0)

Dim bitmap As New Bitmap(imageWidth, imageHeight, stride \ 8, fmt, addr)

これで、単純な整数配列と同じメモリを参照するビットマップができました。整数配列に加えた変更は、ビットマップに直接影響します。簡単な輝度変換でこれを試してみましょう。

Public Sub Brightness(ByRef pixels() As Integer, ByVal scale As Single)
    Dim r, g, b As Integer
    Dim mult As Integer = CInt(1024.0f * scale)
    Dim pixel As Integer

    For i As Integer = 0 To pixels.Length - 1
        pixel = pixels(i)
        r = pixel And 255
        g = (pixel >> 8) And 255
        b = (pixel >> 16) And 255

        'brightness calculation
        'shift right by 10 <=> divide by 1024
        r = (r * mult) >> 10
        g = (g * mult) >> 10
        b = (b * mult) >> 10

        'clamp to between 0 and 255
        If r < 0 Then r = 0
        If g < 0 Then g = 0
        If b < 0 Then b = 0
        r = (r And 255)
        g = (g And 255)
        b = (b And 255)

        pixels(i) = r Or (g << 8) Or (b << 16) Or &HFF000000
    Next
End Sub

ループ内で浮動小数点演算を行わないようにするために、ちょっとしたトリックを使用したことに気づくかもしれません。これにより、パフォーマンスがかなり向上します。そして、完了したら、もちろん少しクリーンアップする必要があります...

addr = IntPtr.Zero
If handle.IsAllocated Then
    handle.Free()
    handle = Nothing
End If
bitmap.Dispose()
bitmap = Nothing
pixels = Nothing

ここではアルファコンポーネントを無視しましたが、これも自由に使用できます。このようにして、多くのビットマップ編集ツールをまとめました。 Bitmap.LockBits()よりもはるかに高速で信頼性が高く、何よりも、ビットマップの編集を開始するためにゼロメモリコピーが必要です。

2
Daniklad

@notJimの答えに基づいて(そして http://www.bobpowell.net/lockingbits.htm の助けを借りて)、次のように開発しました。 xおよびy座標によってピクセルにジャンプできる配列の配列。もちろん、x座標はピクセルあたりのバイト数で補正する必要がありますが、これは簡単な拡張です。

Dim bitmapData As Imaging.BitmapData = myBitmap.LockBits(New Rectangle(0, 0, myBitmap.Width, myBitmap.Height), Imaging.ImageLockMode.ReadOnly, myBitmap.PixelFormat)

Dim size As Integer = Math.Abs(bitmapData.Stride) * bitmapData.Height
Dim data(size - 1) As Byte

Marshal.Copy(bitmapData.Scan0, data, 0, size)

Dim pixelArray(myBitmap.Height)() As Byte

'we have to load all the opacity pixels into an array for later scanning by column
'the data comes in rows
For y = myBitmap.Height - 1 To 0 Step -1
    Dim rowArray(bitmapData.Stride) As Byte
    Array.Copy(data, y * bitmapData.Stride, rowArray, 0, bitmapData.Stride)
    'For x = myBitmap.Width - 1 To 0 Step -1
    '   Dim i = (y * bitmapData.Stride) + (x * 4)
    '   Dim B = data(i)
    '   Dim G = data(i + 1)
    '   Dim R = data(i + 2)
    '   Dim A = data(i + 3)
    'Next
    pixelArray(y) = rowArray
Next
2
cjbarth

このC#ソリューションを試してください。

テスト用のwinformsアプリを作成します。

ボタンとPictureBox、クリックイベントとフォーム終了イベントを追加します。

フォームに次のコードを使用します。

public partial class Form1 : Form
{
    uint[] _Pixels { get; set; }

    Bitmap _Bitmap { get; set; }

    GCHandle _Handle { get; set; }

    IntPtr _Addr { get; set; }


    public Form1()
    {
        InitializeComponent();

        int imageWidth = 100; //1920;

        int imageHeight = 100; // 1080;

        PixelFormat fmt = PixelFormat.Format32bppRgb;

        int pixelFormatSize = Image.GetPixelFormatSize(fmt);

        int stride = imageWidth * pixelFormatSize;

        int padding = 32 - (stride % 32);

        if (padding < 32)
        {
            stride += padding;
        }

        _Pixels = new uint[(stride / 32) * imageHeight + 1];

         _Handle = GCHandle.Alloc(_Pixels, GCHandleType.Pinned);

        _Addr = Marshal.UnsafeAddrOfPinnedArrayElement(_Pixels, 0);

        _Bitmap = new Bitmap(imageWidth, imageHeight, stride / 8, fmt, _Addr);

        pictureBox1.Image = _Bitmap;

    }

    private void button1_Click(object sender, EventArgs e)
    {
        for (int i = 0; i < _Pixels.Length; i++)
        {
            _Pixels[i] = ((uint)(255 | (255 << 8) | (255 << 16) | 0xff000000));

        }

    }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        _Addr = IntPtr.Zero;

        if (_Handle.IsAllocated)
        {
            _Handle.Free();

        }

        _Bitmap.Dispose();

        _Bitmap = null;

        _Pixels = null;

    }

}

これで、配列を編集すると、ビットマップが自動的に更新されます。

これらの変更を確認するには、ピクチャボックスでrefreshメソッドを呼び出す必要があります。

0