BitmapLocker
class is intended for fast read/write of pixels in a Bitmap
image file.
But, Color GetPixel(int x, int y)
and void SetPixel(int x, int y, Color c)
cannot handle 1-bit and 4-bit images.
public class BitmapLocker : IDisposable
{
//private properties
Bitmap _bitmap = null;
BitmapData _bitmapData = null;
private byte[] _imageData = null;
//public properties
public bool IsLocked { get; set; }
public IntPtr IntegerPointer { get; private set; }
public int Width
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.Width;
}
}
public int Height
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.Height;
}
}
public int Stride
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.Stride;
}
}
public int ColorDepth
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return Bitmap.GetPixelFormatSize(_bitmapData.PixelFormat);
}
}
public int Channels
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return ColorDepth / 8;
}
}
public int PaddingOffset
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.Stride - (_bitmapData.Width * Channels);
}
}
public PixelFormat ImagePixelFormat
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.PixelFormat;
}
}
//public bool IsGrayscale
//{
// get
// {
// if (IsLocked == false) throw new InvalidOperationException("not locked");
// return Grayscale.IsGrayscale(_bitmap);
// }
//}
//Constructor
public BitmapLocker(Bitmap source)
{
IsLocked = false;
IntegerPointer = IntPtr.Zero;
this._bitmap = source;
}
/// Lock bitmap
public void Lock()
{
if (IsLocked == false)
{
try
{
// Lock bitmap (so that no movement of data by .NET framework) and return bitmap data
_bitmapData = _bitmap.LockBits(
new Rectangle(0, 0, _bitmap.Width, _bitmap.Height),
ImageLockMode.ReadWrite,
_bitmap.PixelFormat);
// Create byte array to copy pixel values
int noOfBytesNeededForStorage = Math.Abs(_bitmapData.Stride) * _bitmapData.Height;
_imageData = new byte[noOfBytesNeededForStorage];
IntegerPointer = _bitmapData.Scan0;
// Copy data from IntegerPointer to _imageData
Marshal.Copy(IntegerPointer, _imageData, 0, _imageData.Length);
IsLocked = true;
}
catch (Exception)
{
throw;
}
}
else
{
throw new Exception("Bitmap is already locked.");
}
}
/// Unlock bitmap
public void Unlock()
{
if (IsLocked == true)
{
try
{
// Copy data from _imageData to IntegerPointer
Marshal.Copy(_imageData, 0, IntegerPointer, _imageData.Length);
// Unlock bitmap data
_bitmap.UnlockBits(_bitmapData);
IsLocked = false;
}
catch (Exception)
{
throw;
}
}
else
{
throw new Exception("Bitmap is not locked.");
}
}
public Color GetPixel(int x, int y)
{
Color clr = Color.Empty;
// Get color components count
int cCount = ColorDepth / 8;
// Get start index of the specified pixel
int i = (Stride > 0 ? y : y - Height + 1) * Stride + x * cCount;
int dataLength = _imageData.Length - cCount;
if (i > dataLength)
{
throw new IndexOutOfRangeException();
}
if (ColorDepth == 32) // For 32 bpp get Red, Green, Blue and Alpha
{
byte b = _imageData[i];
byte g = _imageData[i + 1];
byte r = _imageData[i + 2];
byte a = _imageData[i + 3]; // a
clr = Color.FromArgb(a, r, g, b);
}
if (ColorDepth == 24) // For 24 bpp get Red, Green and Blue
{
byte b = _imageData[i];
byte g = _imageData[i + 1];
byte r = _imageData[i + 2];
clr = Color.FromArgb(r, g, b);
}
if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
// For 8 bpp get color value (Red, Green and Blue values are the same)
{
byte c = _imageData[i];
clr = Color.FromArgb(c, c, c);
}
return clr;
}
public void SetPixel(int x, int y, Color color)
{
if (!IsLocked) throw new Exception();
// Get color components count
int cCount = ColorDepth / 8;
// Get start index of the specified pixel
int i = (Stride > 0 ? y : y - Height + 1) * Stride + x * cCount;
try
{
if (ColorDepth == 32) // For 32 bpp set Red, Green, Blue and Alpha
{
_imageData[i] = color.B;
_imageData[i + 1] = color.G;
_imageData[i + 2] = color.R;
_imageData[i + 3] = color.A;
}
if (ColorDepth == 24) // For 24 bpp set Red, Green and Blue
{
_imageData[i] = color.B;
_imageData[i + 1] = color.G;
_imageData[i + 2] = color.R;
}
if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
// For 8 bpp set color value (Red, Green and Blue values are the same)
{
_imageData[i] = color.B;
}
}
catch (Exception ex)
{
throw new Exception("(" + x + ", " + y + "), " + _imageData.Length + ", " + ex.Message + ", i=" + i);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// free managed resources
_bitmap = null;
_bitmapData = null;
_imageData = null;
IntegerPointer = IntPtr.Zero;
}
}
}
For instance, the following code displays a fully black output:
public class MainClass
{
public static void Main(string [] args)
{
Bitmap source = (Bitmap)Bitmap.FromFile(@"1_bit__parrot__monochrome.png");
BitmapLocker locker = new BitmapLocker(source);
locker.Lock();
Bitmap dest = new Bitmap(source.Width, source.Height, locker.ImagePixelFormat);
BitmapLocker locker2 = new BitmapLocker(dest);
locker2.Lock();
for (int h = 0; h < locker.Height; h++)
{
for (int w = 0; w < locker.Width; w++)
{
locker2.SetPixel(w,h,locker.GetPixel(w,h));
}
}
locker2.Unlock();
locker.Unlock();
dest.Palette = source.Palette; // copy color palette too!
PictureDisplayForm f = new PictureDisplayForm(source, dest);
f.ShowDialog();
}
}
How can I correct this code so that it can handle 1-bit and 4-bit images?
.
.
Sample Input
For pixel formats smaller than 8 bits, more than one pixel is packed into a single byte. Therefore you can not have a catch-all statement like this for 8, 4 and 1-bit formats:
if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
{
byte c = _imageData[i];
clr = Color.FromArgb(c, c, c);
}
Instead, based on the pixel format, when retrieving the pixel data the bit position in the byte has to be calculated and appropriate bits extracted from the byte -- this would be either "high" or "low" bits in the case of 4-bit images or the single bit in case of 1-bit images. Conversely, when setting the pixel data only certain bits in the byte (based on the pixel format) need to be changed.
Suppose we have a 4-bit format image. The image data may look something like this:
bit index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ byte index: 0 1 2 pixel index: 0 1 2 3 4 5
This format packs two pixels per byte. Therefore, when retrieving pixel data, we first compute the bit index for the pixel:
int biti = (Stride > 0 ? y : y - Height + 1) * Stride * 8 + x * ColorDepth;
Stride
is the number of bytes in a single row, so just multiply that by height * 8 (for 8 bits in a byte) and add the width * ColorDepth
(for number of bits per pixel).
Then we need to figure out whether we want to retrieve the first four bits in the byte or the last four bits. For that, we simply compute bitindex mod 8
. Obviously if the pixel starts with the byte, this will be 0 (for example, 8 mod 8 = 0
), otherwise it will be 4. Based on that, if we want the first four bits, we shift the byte by four. C# zeroes out the first four bits:
+-----------------+ |+---+---+---+---+|---+---+---+---+ +---+---+---+---+---+---+---+---+ || 0 | 0 | 1 | 1 || 1 | 1 | 0 | 0 | => | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | |+---+---+---+---+|---+---+---+---+ +---+---+---+---+---+---+---+---+ +-----------------+ ===============>>
On the other hand, if we want the last four bits, we AND
the image data byte with a byte that has the first four bits zeroed out:
+---+---+---+---+---+---+---+---+ | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | +---+---+---+---+---+---+---+---+ AND +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | +---+---+---+---+---+---+---+---+ = +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | +---+---+---+---+---+---+---+---+
In code, all this looks something like:
byte c = 0;
if (biti % 8 == 0)
{
c = (byte)(_imageData[i] >> 4);
}
else
{
c = (byte)(_imageData[i] & 0xF);
}
For 1-bit, monochrome images, we want to get at the single bit. For that, we AND
the image data byte with a byte that has all the other bits zeroed out (the "mask"). For example, if we want to get at the bit at index 5, we would do this:
+---+---+---+---+---+---+---+---+ | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | +---+---+---+---+---+---+---+---+ AND +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | +---+---+---+---+---+---+---+---+ = +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | +---+---+---+---+---+---+---+---+
If the result is zero, then we know that the bit is zero, otherwise the bit is "set". In code:
byte mask = (byte)(1 << bbi);
byte c = (byte)((_imageData[i] & mask) != 0 ? 1 : 0);
Once we have retrieved the pixel data, let's retrieve the actual color since the GetPixel
function returns a Color
object. For 8-, 4- and 1-bit images the pixel data actually represents an index into a color palette. A color palette looks something like this:
============= +-----+-----+-----++-----+-----+-----++-----+-----+-----+ | R | G | B || R | G | B || R | G | B | Color +-----+-----+-----++-----+-----+-----++-----+-----+-----+ | 000 | 016 | 005 || 020 | 120 | 053 || 117 | 002 | 209 | ============= +-----+-----+-----++-----+-----+-----++-----+-----+-----+ | || || | Index | 0 || 1 || 2 | | || || | ============= +-----------------++-----------------++-----------------+
We have access to the color palette, so to retrieve the color:
clr = Palette.Entries[c];
Where c
is the retrieved pixel data.
Something similar is done for setting pixel data. There is plenty of information on bit manipulation in C#, such as here, here and here.
Putting it all together, keeping with your existing code:
public class BitmapLocker : IDisposable
{
//private properties
Bitmap _bitmap = null;
BitmapData _bitmapData = null;
private byte[] _imageData = null;
//public properties
public bool IsLocked { get; set; }
public IntPtr IntegerPointer { get; private set; }
public int Width
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.Width;
}
}
public int Height
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.Height;
}
}
public int Stride
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.Stride;
}
}
public int ColorDepth
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return Bitmap.GetPixelFormatSize(_bitmapData.PixelFormat);
}
}
public int Channels
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return ColorDepth / 8;
}
}
public int PaddingOffset
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.Stride - (_bitmapData.Width * Channels);
}
}
public PixelFormat ImagePixelFormat
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmapData.PixelFormat;
}
}
public ColorPalette Palette
{
get
{
if (IsLocked == false) throw new InvalidOperationException("not locked");
return _bitmap.Palette;
}
}
//Constructor
public BitmapLocker(Bitmap source)
{
IsLocked = false;
IntegerPointer = IntPtr.Zero;
this._bitmap = source;
}
/// Lock bitmap
public void Lock()
{
if (IsLocked == false)
{
try
{
// Lock bitmap (so that no movement of data by .NET framework) and return bitmap data
_bitmapData = _bitmap.LockBits(
new Rectangle(0, 0, _bitmap.Width, _bitmap.Height),
ImageLockMode.ReadWrite,
_bitmap.PixelFormat);
// Create byte array to copy pixel values
int noOfBytesNeededForStorage = Math.Abs(_bitmapData.Stride) * _bitmapData.Height;
_imageData = new byte[noOfBytesNeededForStorage];
IntegerPointer = _bitmapData.Scan0;
// Copy data from IntegerPointer to _imageData
Marshal.Copy(IntegerPointer, _imageData, 0, _imageData.Length);
IsLocked = true;
}
catch (Exception)
{
throw;
}
}
else
{
throw new Exception("Bitmap is already locked.");
}
}
/// Unlock bitmap
public void Unlock()
{
if (IsLocked == true)
{
try
{
// Copy data from _imageData to IntegerPointer
Marshal.Copy(_imageData, 0, IntegerPointer, _imageData.Length);
// Unlock bitmap data
_bitmap.UnlockBits(_bitmapData);
IsLocked = false;
}
catch (Exception)
{
throw;
}
}
else
{
throw new Exception("Bitmap is not locked.");
}
}
public Color GetPixel(int x, int y)
{
Color clr = Color.Empty;
// Get the bit index of the specified pixel
int biti = (Stride > 0 ? y : y - Height + 1) * Stride * 8 + x * ColorDepth;
// Get the byte index
int i = biti / 8;
// Get color components count
int cCount = ColorDepth / 8;
int dataLength = _imageData.Length - cCount;
if (i > dataLength)
{
throw new IndexOutOfRangeException();
}
if (ColorDepth == 32) // For 32 bpp get Red, Green, Blue and Alpha
{
byte b = _imageData[i];
byte g = _imageData[i + 1];
byte r = _imageData[i + 2];
byte a = _imageData[i + 3]; // a
clr = Color.FromArgb(a, r, g, b);
}
if (ColorDepth == 24) // For 24 bpp get Red, Green and Blue
{
byte b = _imageData[i];
byte g = _imageData[i + 1];
byte r = _imageData[i + 2];
clr = Color.FromArgb(r, g, b);
}
if (ColorDepth == 8)
{
byte c = _imageData[i];
if(Palette.Entries.Length <= c)
throw new InvalidOperationException("no palette");
clr = Palette.Entries[c];
}
if (ColorDepth == 4)
{
byte c = 0;
if (biti % 8 == 0)
{
c = (byte)(_imageData[i] >> 4);
}
else
{
c = (byte)(_imageData[i] & 0xF);
}
if (Palette.Entries.Length <= c)
throw new InvalidOperationException("no palette");
clr = Palette.Entries[c];
}
if (ColorDepth == 1)
{
int bbi = biti % 8;
byte mask = (byte)(1 << bbi);
byte c = (byte)((_imageData[i] & mask) != 0 ? 1 : 0);
if (Palette.Entries.Length <= c)
throw new InvalidOperationException("no palette");
clr = Palette.Entries[c];
}
return clr;
}
public void SetPixel(int x, int y, Color color)
{
if (!IsLocked) throw new Exception();
// Get the bit index of the specified pixel
int biti = (Stride > 0 ? y : y - Height + 1) * Stride * 8 + x * ColorDepth;
// Get the byte index
int i = biti / 8;
// Get color components count
int cCount = ColorDepth / 8;
try
{
if (ColorDepth == 32) // For 32 bpp set Red, Green, Blue and Alpha
{
_imageData[i] = color.B;
_imageData[i + 1] = color.G;
_imageData[i + 2] = color.R;
_imageData[i + 3] = color.A;
}
if (ColorDepth == 24) // For 24 bpp set Red, Green and Blue
{
_imageData[i] = color.B;
_imageData[i + 1] = color.G;
_imageData[i + 2] = color.R;
}
if (ColorDepth == 8)
{
if (Palette.Entries.Length < 256)
throw new InvalidOperationException("no palette");
byte index = 0;
for (int j = 0; j < 256; j++)
{
if(Palette.Entries[j].R == color.R && Palette.Entries[j].G == color.G && Palette.Entries[j].B == color.B)
{
index = (byte)j;
break;
}
}
_imageData[i] = index;
}
if (ColorDepth == 4)
{
if (Palette.Entries.Length < 16)
throw new InvalidOperationException("no palette");
byte index = 0;
for (int j = 0; j < 16; j++)
{
if (Palette.Entries[j].R == color.R && Palette.Entries[j].G == color.G && Palette.Entries[j].B == color.B)
{
index = (byte)j;
break;
}
}
if (biti % 8 == 0)
{
_imageData[i] = (byte)((_imageData[i] & 0xF) | (index << 4));
}
else
{
_imageData[i] = (byte)((_imageData[i] & 0xF0) | index);
}
}
if (ColorDepth == 1)
{
if (Palette.Entries.Length < 2)
throw new InvalidOperationException("no palette");
byte index = 0;
for (int j = 0; j < 2; j++)
{
if (Palette.Entries[j].R == color.R && Palette.Entries[j].G == color.G && Palette.Entries[j].B == color.B)
{
index = (byte)j;
break;
}
}
int bbi = biti % 8;
byte mask = (byte)(1 << bbi);
if (index != 0)
{
_imageData[i] |= mask;
}
else
{
_imageData[i] &= (byte)~mask;
}
}
}
catch (Exception ex)
{
throw new Exception("(" + x + ", " + y + "), " + _imageData.Length + ", " + ex.Message + ", i=" + i);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// free managed resources
_bitmap = null;
_bitmapData = null;
_imageData = null;
IntegerPointer = IntPtr.Zero;
}
}
}
Note: the for loops in SetPixel
to retrieve the index are not exactly efficient so if you're using that function a lot you may want to restructure the code so it takes in an index value instead of a color for indexed images.
Finally, in order to use this code, we must copy the palette before using the locker object for indexed images, so it would look something like this:
Bitmap source = (Bitmap)Bitmap.FromFile(@"testimage.png");
BitmapLocker locker = new BitmapLocker(source);
locker.Lock();
Bitmap dest = new Bitmap(source.Width, source.Height, locker.ImagePixelFormat);
if(source.Palette.Entries.Length > 0)
dest.Palette = source.Palette;
BitmapLocker locker2 = new BitmapLocker(dest);
locker2.Lock();
for (int h = 0; h < locker.Height; h++)
{
for (int w = 0; w < locker.Width; w++)
{
locker2.SetPixel(w, h, locker.GetPixel(w, h));
}
}
locker2.Unlock();
locker.Unlock();