Для форматов пикселей меньше 8 бит более одного пикселя упаковывается в один байт. Поэтому вы не можете использовать такой универсальный оператор для 8, 4 и 1-битных форматов:
if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
{
byte c = _imageData[i];
clr = Color.FromArgb(c, c, c);
}
Вместо этого, основываясь на формате пикселя, при извлечении данных пикселя должна быть рассчитана позиция бита в байте и извлечены соответствующие биты из байта - это будет либо «старший», либо «низкий» бит в случае 4-битных изображений или одного бита в случае 1-битных изображений. И наоборот, при установке данных пикселей необходимо изменять только определенные биты в байте (на основе формата пикселей).
Предположим, у нас есть изображение в 4-битном формате. Данные изображения могут выглядеть примерно так:
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
Этот формат упаковывает два пикселя на байт. Поэтому при извлечении данных пикселя мы сначала вычисляем битовый индекс для пикселя:
int biti = (Stride > 0 ? y : y - Height + 1) * Stride * 8 + x * ColorDepth;
Stride
- это число байтов в одной строке, поэтому просто умножьте это на высоту * 8 (для 8 бит в байте) и добавьте ширину * ColorDepth
(для количества бит на пиксель).
Затем нам нужно выяснить, хотим ли мы получить первые четыре бита в байте или последние четыре бита. Для этого мы просто вычисляем bitindex mod 8
. Очевидно, что если пиксель начинается с байта, это будет 0 (например, 8 mod 8 = 0
), в противном случае это будет 4. На основании этого, если нам нужны первые четыре бита, мы сдвигаем байт на четыре. C # обнуляет первые четыре бита:
+-----------------+
|+---+---+---+---+|---+---+---+---+ +---+---+---+---+---+---+---+---+
|| 0 | 0 | 1 | 1 || 1 | 1 | 0 | 0 | => | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
|+---+---+---+---+|---+---+---+---+ +---+---+---+---+---+---+---+---+
+-----------------+
===============>>
С другой стороны, если нам нужны последние четыре бита, мы AND
байт данных изображения с байтом, в котором первые четыре бита обнулены:
+---+---+---+---+---+---+---+---+
| 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 |
+---+---+---+---+---+---+---+---+
В коде все это выглядит примерно так:
byte c = 0;
if (biti % 8 == 0)
{
c = (byte)(_imageData[i] >> 4);
}
else
{
c = (byte)(_imageData[i] & 0xF);
}
Для 1-битных монохромных изображений мы хотим получить один бит. Для этого мы AND
байт данных изображения с байтом, в котором все остальные биты обнулены («маска»). Например, если мы хотим получить бит 5 с индексом, мы сделаем это:
+---+---+---+---+---+---+---+---+
| 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 |
+---+---+---+---+---+---+---+---+
Если результат равен нулю, то мы знаем, что бит равен нулю, в противном случае бит "установлен". В коде:
byte mask = (byte)(1 << bbi);
byte c = (byte)((_imageData[i] & mask) != 0 ? 1 : 0);
Как только мы получим данные пикселей, давайте восстановим фактический цвет, так как функция GetPixel
возвращает объект Color
. Для 8-, 4- и 1-битных изображений данные пикселей фактически представляют собой индекс в цветовую палитру. Цветовая палитра выглядит примерно так:
============= +-----+-----+-----++-----+-----+-----++-----+-----+-----+
| R | G | B || R | G | B || R | G | B |
Color +-----+-----+-----++-----+-----+-----++-----+-----+-----+
| 000 | 016 | 005 || 020 | 120 | 053 || 117 | 002 | 209 |
============= +-----+-----+-----++-----+-----+-----++-----+-----+-----+
| || || |
Index | 0 || 1 || 2 |
| || || |
============= +-----------------++-----------------++-----------------+
У нас есть доступ к цветовой палитре, поэтому для получения цвета:
clr = Palette.Entries[c];
Где c
- полученные данные пикселей.
Нечто подобное сделано для настройки данных пикселей. Существует много информации о битовых манипуляциях в C #, таких как здесь , здесь и здесь .
Собираем все это вместе с существующим кодом:
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;
}
}
}
Примечание: циклы for в SetPixel
для извлечения индекса не совсем эффективны, поэтому, если вы часто используете эту функцию, вы можете захотеть реструктурировать код, чтобы он принимал значение индекса вместо цвет для проиндексированных изображений.
Наконец, чтобы использовать этот код, мы должны скопировать палитру перед использованием объекта locker для индексированных изображений, чтобы он выглядел примерно так:
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();