Bitmap の各ピクセルにアクセスして操作し、Bitmap に保存する必要があります。
とを使用するBitmap.GetPixel()
とBitmap.SetPixel()
、プログラムの実行が遅くなります。
Bitmap
にすばやく変換するにはどうすればよいbyte[]
ですか?
各ピクセルの RGBA データを含むbyte[]
withが必要です。length = (4 * width * height)
いくつかの異なる方法でそれを行うことができます。を使用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;
}
Bitmap.LockBits メソッドを使用できます。また、タスクの並列実行を使用する場合は、System.Threading.Tasks 名前空間の Parallel クラスを使用できます。次のリンクには、いくつかのサンプルと説明があります。
LockBitsが必要です。その後、BitmapData オブジェクトから必要なバイトを抽出できます。
はるかに高速で便利な別の方法があります。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() よりもはるかに高速で信頼性が高く、何よりも、ビットマップの編集を開始するためにメモリ コピーが不要です。
@notJim の回答に基づいて (およびhttps://web.archive.org/web/20141229164101/http://bobpowell.net/lockingbits.aspxの助けを借りて)、私は次のことを開発しました。最終的には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
この C# ソリューションを試してください。
テスト用の winforms アプリを作成します。
Button と 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;
}
}
これで、配列を編集すると、ビットマップが自動的に更新されます。
これらの変更を表示するには、picturebox で refresh メソッドを呼び出す必要があります。