如何处理 1 位和 4 位图像?
How can I work with 1-bit and 4-bit images?
BitmapLocker
class 用于 Bitmap
图像文件中的快速 read/write 像素。
但是,Color GetPixel(int x, int y)
和 void SetPixel(int x, int y, Color c)
无法处理 1 位和 4 位图像。
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;
}
}
}
例如,以下代码显示全黑输出:
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();
}
}
如何更正此代码,使其可以处理 1 位和 4 位图像?
.
.
示例输入
1 位单色和 4 位彩色
对于小于 8 位的像素格式,一个以上的像素被打包到一个字节中。因此,对于 8、4 和 1 位格式,您不能使用这样的包罗万象的语句:
if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
{
byte c = _imageData[i];
clr = Color.FromArgb(c, c, c);
}
相反,根据像素格式,在检索像素数据时,必须计算字节中的位位置并从字节中提取适当的位——这可能是 "high" 或 "low" 位在 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
图像数据字节中有一个字节将所有其他位清零("mask")。例如,如果我们想获取索引 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 |
+---+---+---+---+---+---+---+---+
如果结果为零,则我们知道该位为零,否则该位为"set"。在代码中:
byte mask = (byte)(1 << bbi);
byte c = (byte)((_imageData[i] & mask) != 0 ? 1 : 0);
检索到像素数据后,让我们检索自 GetPixel
函数 returns 一个 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# 中位操作的信息很多,例如 here, here and here.
将它们放在一起,与您现有的代码保持一致:
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;
}
}
}
注意:SetPixel
中用于检索索引的 for 循环效率不高,因此如果您经常使用该函数,您可能需要重组代码,以便它接受索引图像的索引值而不是颜色。
最后,为了使用这段代码,我们必须在使用索引图像的储物柜对象之前复制调色板,所以它看起来像这样:
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();
1 位和 4 位内容使用起来相当烦人。出于这个原因,我使用的任何索引数据都只是转换为更方便的每像素 1 字节 (8bpp) 进行处理,使用 ConvertTo8Bit
和 ConvertFrom8Bit
函数。
它们与 GetImageData
函数协同工作以从图像中获取字节,以及 BuildImage
函数以从字节中构建新的 Bitmap
。
一般来说,关于图像要记住的一件重要事情是,以像素为单位的线条宽度不一定等于位数乘以宽度。首先,因为对于 1bpp 或 4bpp,您无论如何都可以有多余的部分来到达下一个完整字节,其次,because the .Net framework aligns image lines to multiples of 4 bytes。出于这个原因,当将图像作为字节处理时,始终保持一个 stride
值非常重要,该值包含以字节为单位的实际数据宽度。
另一件要记住的重要事情是您需要一个用于索引图像的调色板;它们的像素不是颜色,而是对调色板的引用。如果没有调色板,它们将无法显示任何内容,如果您忽略调色板,它们最终可能会使用默认颜色 Windows 为每种像素格式提供默认颜色,这通常根本不是图像所需要的。
最后,如果您在 8 位数组中编辑 1 位或 4 位数据,则必须确保永远不要将超过原始像素格式允许的最大值的数据放入数组中。所以在 4bpp 数据中,你永远不应该有值高于 0x0F 的字节,而在 1bpp 中,你的字节中应该只有值 0 和 1。
GetImageData
和 BuildImage
函数:
/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <returns>The raw bytes of the image.</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride)
{
if (sourceImage == null)
throw new ArgumentNullException("sourceImage", "Source image is null!");
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
stride = sourceData.Stride;
Byte[] data = new Byte[stride * sourceImage.Height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
sourceImage.UnlockBits(sourceData);
return data;
}
/// <summary>
/// Creates a bitmap based on data, width, height, stride and pixel format.
/// </summary>
/// <param name="sourceData">Byte array of raw source data</param>
/// <param name="width">Width of the image</param>
/// <param name="height">Height of the image</param>
/// <param name="stride">Scanline length inside the data. If this is negative, the image is built from the bottom up (BMP format).</param>
/// <param name="pixelFormat">Pixel format</param>
/// <param name="palette">Color palette</param>
/// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
/// <returns>The new image</returns>
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
{
Bitmap newImage = new Bitmap(width, height, pixelFormat);
BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
// Compensate for possible negative stride on BMP format.
Boolean isFlipped = targetData.Stride < 0;
Int32 targetStride = Math.Abs(targetData.Stride);
Int64 scan0 = targetData.Scan0.ToInt64();
for (Int32 y = 0; y < height; y++)
Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
newImage.UnlockBits(targetData);
// Fix negative stride on BMP format.
if (isFlipped)
newImage.RotateFlip(RotateFlipType.Rotate180FlipX);
// For indexed images, set the palette.
if ((pixelFormat & PixelFormat.Indexed) != 0 && palette != null)
{
ColorPalette pal = newImage.Palette;
for (Int32 i = 0; i < pal.Entries.Length; i++)
{
if (i < palette.Length)
pal.Entries[i] = palette[i];
else if (defaultColor.HasValue)
pal.Entries[i] = defaultColor.Value;
else
break;
}
newImage.Palette = pal;
}
return newImage;
}
ConvertTo8Bit
和 ConvertFrom8Bit
函数:
/// <summary>
/// Converts given raw image data for a paletted image to 8-bit, so we have a simple one-byte-per-pixel format to work with.
/// </summary>
/// <param name="fileData">The file data.</param>
/// <param name="width">Width of the image.</param>
/// <param name="height">Height of the image.</param>
/// <param name="start">Start offset of the image data in the fileData parameter.</param>
/// <param name="bitsLength">Amount of bits used by one pixel.</param>
/// <param name="bigEndian">True if the bits in the original image data are stored as big-endian.</param>
/// <param name="stride">Stride used in the original image data. Will be adjusted to the new stride value.</param>
/// <returns>The image data in a 1-byte-per-pixel format, with a stride exactly the same as the width.</returns>
public static Byte[] ConvertTo8Bit(Byte[] fileData, Int32 width, Int32 height, Int32 start, Int32 bitsLength, Boolean bigEndian, ref Int32 stride)
{
if (bitsLength != 1 && bitsLength != 2 && bitsLength != 4 && bitsLength != 8)
throw new ArgumentOutOfRangeException("Cannot handle image data with " + bitsLength + "bits per pixel.");
// Full array
Byte[] data8bit = new Byte[width * height];
// Amount of pixels that end up on the same byte
Int32 parts = 8 / bitsLength;
// Amount of bytes to write per line
Int32 newStride = width;
// Bit mask for reducing read and shifted data to actual bits length
Int32 bitmask = (1 << bitsLength) - 1;
Int32 size = stride * height;
// File check, and getting actual data.
if (start + size > fileData.Length)
throw new IndexOutOfRangeException("Data exceeds array bounds!");
// Actual conversion process.
for (Int32 y = 0; y < height; y++)
{
for (Int32 x = 0; x < width; x++)
{
// This will hit the same byte multiple times
Int32 indexXbit = start + y * stride + x / parts;
// This will always get a new index
Int32 index8bit = y * newStride + x;
// Amount of bits to shift the data to get to the current pixel data
Int32 shift = (x % parts) * bitsLength;
// Reversed for big-endian
if (bigEndian)
shift = 8 - shift - bitsLength;
// Get data and store it.
data8bit[index8bit] = (Byte)((fileData[indexXbit] >> shift) & bitmask);
}
}
stride = newStride;
return data8bit;
}
/// <summary>
/// Converts given raw image data for a paletted 8-bit image to lower amount of bits per pixel.
/// </summary>
/// <param name="data8bit">The eight bit per pixel image data</param>
/// <param name="width">The width of the image</param>
/// <param name="height">The height of the image</param>
/// <param name="bitsLength">The new amount of bits per pixel</param>
/// <param name="bigEndian">True if the bits in the new image data are to be stored as big-endian.</param>
/// <param name="stride">Stride used in the original image data. Will be adjusted to the new stride value.</param>
/// <returns>The image data converted to the requested amount of bits per pixel.</returns>
public static Byte[] ConvertFrom8Bit(Byte[] data8bit, Int32 width, Int32 height, Int32 bitsLength, Boolean bigEndian, ref Int32 stride)
{
Int32 parts = 8 / bitsLength;
// Amount of bytes to write per line
Int32 newStride = ((bitsLength * width) + 7) / 8;
// Bit mask for reducing original data to actual bits maximum.
// Should not be needed if data is correct, but eh.
Int32 bitmask = (1 << bitsLength) - 1;
Byte[] dataXbit = new Byte[newStride * height];
// Actual conversion process.
for (Int32 y = 0; y < height; y++)
{
for (Int32 x = 0; x < width; x++)
{
// This will hit the same byte multiple times
Int32 indexXbit = y * newStride + x / parts;
// This will always get a new index
Int32 index8bit = y * stride + x;
// Amount of bits to shift the data to get to the current pixel data
Int32 shift = (x % parts) * bitsLength;
// Reversed for big-endian
if (bigEndian)
shift = 8 - shift - bitsLength;
// Get data, reduce to bit rate, shift it and store it.
dataXbit[indexXbit] |= (Byte)((data8bit[index8bit] & bitmask) << shift);
}
}
stride = newStride;
return dataXbit;
}
请注意,bigEndian
参数指的是位块的顺序。通常,4bpp 中的字节 12 34
只会为您提供像素 1 2 3 4
,在这种情况下适用大端规则(值的数学上最大的部分作为第一个像素处理)。 1bpp也是如此;值 37 通常会给出像素 0 0 1 1 0 1 1 1
。但是在我开发的旧 DOS 游戏的一些自定义文件格式中,情况并非如此(4bpp 12 34
会给出像素 2 1 4 3
),因此函数具有该参数的原因。
start
参数同样存在,因为我使用它的数据是从自定义文件格式中读取的。通常这应该总是 0.
BitmapLocker
class 用于 Bitmap
图像文件中的快速 read/write 像素。
但是,Color GetPixel(int x, int y)
和 void SetPixel(int x, int y, Color c)
无法处理 1 位和 4 位图像。
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;
}
}
}
例如,以下代码显示全黑输出:
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();
}
}
如何更正此代码,使其可以处理 1 位和 4 位图像?
.
.
示例输入
1 位单色和 4 位彩色
对于小于 8 位的像素格式,一个以上的像素被打包到一个字节中。因此,对于 8、4 和 1 位格式,您不能使用这样的包罗万象的语句:
if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
{
byte c = _imageData[i];
clr = Color.FromArgb(c, c, c);
}
相反,根据像素格式,在检索像素数据时,必须计算字节中的位位置并从字节中提取适当的位——这可能是 "high" 或 "low" 位在 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
图像数据字节中有一个字节将所有其他位清零("mask")。例如,如果我们想获取索引 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 | +---+---+---+---+---+---+---+---+
如果结果为零,则我们知道该位为零,否则该位为"set"。在代码中:
byte mask = (byte)(1 << bbi);
byte c = (byte)((_imageData[i] & mask) != 0 ? 1 : 0);
检索到像素数据后,让我们检索自 GetPixel
函数 returns 一个 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# 中位操作的信息很多,例如 here, here and here.
将它们放在一起,与您现有的代码保持一致:
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;
}
}
}
注意:SetPixel
中用于检索索引的 for 循环效率不高,因此如果您经常使用该函数,您可能需要重组代码,以便它接受索引图像的索引值而不是颜色。
最后,为了使用这段代码,我们必须在使用索引图像的储物柜对象之前复制调色板,所以它看起来像这样:
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();
1 位和 4 位内容使用起来相当烦人。出于这个原因,我使用的任何索引数据都只是转换为更方便的每像素 1 字节 (8bpp) 进行处理,使用 ConvertTo8Bit
和 ConvertFrom8Bit
函数。
它们与 GetImageData
函数协同工作以从图像中获取字节,以及 BuildImage
函数以从字节中构建新的 Bitmap
。
一般来说,关于图像要记住的一件重要事情是,以像素为单位的线条宽度不一定等于位数乘以宽度。首先,因为对于 1bpp 或 4bpp,您无论如何都可以有多余的部分来到达下一个完整字节,其次,because the .Net framework aligns image lines to multiples of 4 bytes。出于这个原因,当将图像作为字节处理时,始终保持一个 stride
值非常重要,该值包含以字节为单位的实际数据宽度。
另一件要记住的重要事情是您需要一个用于索引图像的调色板;它们的像素不是颜色,而是对调色板的引用。如果没有调色板,它们将无法显示任何内容,如果您忽略调色板,它们最终可能会使用默认颜色 Windows 为每种像素格式提供默认颜色,这通常根本不是图像所需要的。
最后,如果您在 8 位数组中编辑 1 位或 4 位数据,则必须确保永远不要将超过原始像素格式允许的最大值的数据放入数组中。所以在 4bpp 数据中,你永远不应该有值高于 0x0F 的字节,而在 1bpp 中,你的字节中应该只有值 0 和 1。
GetImageData
和 BuildImage
函数:
/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <returns>The raw bytes of the image.</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride)
{
if (sourceImage == null)
throw new ArgumentNullException("sourceImage", "Source image is null!");
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
stride = sourceData.Stride;
Byte[] data = new Byte[stride * sourceImage.Height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
sourceImage.UnlockBits(sourceData);
return data;
}
/// <summary>
/// Creates a bitmap based on data, width, height, stride and pixel format.
/// </summary>
/// <param name="sourceData">Byte array of raw source data</param>
/// <param name="width">Width of the image</param>
/// <param name="height">Height of the image</param>
/// <param name="stride">Scanline length inside the data. If this is negative, the image is built from the bottom up (BMP format).</param>
/// <param name="pixelFormat">Pixel format</param>
/// <param name="palette">Color palette</param>
/// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
/// <returns>The new image</returns>
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
{
Bitmap newImage = new Bitmap(width, height, pixelFormat);
BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
// Compensate for possible negative stride on BMP format.
Boolean isFlipped = targetData.Stride < 0;
Int32 targetStride = Math.Abs(targetData.Stride);
Int64 scan0 = targetData.Scan0.ToInt64();
for (Int32 y = 0; y < height; y++)
Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
newImage.UnlockBits(targetData);
// Fix negative stride on BMP format.
if (isFlipped)
newImage.RotateFlip(RotateFlipType.Rotate180FlipX);
// For indexed images, set the palette.
if ((pixelFormat & PixelFormat.Indexed) != 0 && palette != null)
{
ColorPalette pal = newImage.Palette;
for (Int32 i = 0; i < pal.Entries.Length; i++)
{
if (i < palette.Length)
pal.Entries[i] = palette[i];
else if (defaultColor.HasValue)
pal.Entries[i] = defaultColor.Value;
else
break;
}
newImage.Palette = pal;
}
return newImage;
}
ConvertTo8Bit
和 ConvertFrom8Bit
函数:
/// <summary>
/// Converts given raw image data for a paletted image to 8-bit, so we have a simple one-byte-per-pixel format to work with.
/// </summary>
/// <param name="fileData">The file data.</param>
/// <param name="width">Width of the image.</param>
/// <param name="height">Height of the image.</param>
/// <param name="start">Start offset of the image data in the fileData parameter.</param>
/// <param name="bitsLength">Amount of bits used by one pixel.</param>
/// <param name="bigEndian">True if the bits in the original image data are stored as big-endian.</param>
/// <param name="stride">Stride used in the original image data. Will be adjusted to the new stride value.</param>
/// <returns>The image data in a 1-byte-per-pixel format, with a stride exactly the same as the width.</returns>
public static Byte[] ConvertTo8Bit(Byte[] fileData, Int32 width, Int32 height, Int32 start, Int32 bitsLength, Boolean bigEndian, ref Int32 stride)
{
if (bitsLength != 1 && bitsLength != 2 && bitsLength != 4 && bitsLength != 8)
throw new ArgumentOutOfRangeException("Cannot handle image data with " + bitsLength + "bits per pixel.");
// Full array
Byte[] data8bit = new Byte[width * height];
// Amount of pixels that end up on the same byte
Int32 parts = 8 / bitsLength;
// Amount of bytes to write per line
Int32 newStride = width;
// Bit mask for reducing read and shifted data to actual bits length
Int32 bitmask = (1 << bitsLength) - 1;
Int32 size = stride * height;
// File check, and getting actual data.
if (start + size > fileData.Length)
throw new IndexOutOfRangeException("Data exceeds array bounds!");
// Actual conversion process.
for (Int32 y = 0; y < height; y++)
{
for (Int32 x = 0; x < width; x++)
{
// This will hit the same byte multiple times
Int32 indexXbit = start + y * stride + x / parts;
// This will always get a new index
Int32 index8bit = y * newStride + x;
// Amount of bits to shift the data to get to the current pixel data
Int32 shift = (x % parts) * bitsLength;
// Reversed for big-endian
if (bigEndian)
shift = 8 - shift - bitsLength;
// Get data and store it.
data8bit[index8bit] = (Byte)((fileData[indexXbit] >> shift) & bitmask);
}
}
stride = newStride;
return data8bit;
}
/// <summary>
/// Converts given raw image data for a paletted 8-bit image to lower amount of bits per pixel.
/// </summary>
/// <param name="data8bit">The eight bit per pixel image data</param>
/// <param name="width">The width of the image</param>
/// <param name="height">The height of the image</param>
/// <param name="bitsLength">The new amount of bits per pixel</param>
/// <param name="bigEndian">True if the bits in the new image data are to be stored as big-endian.</param>
/// <param name="stride">Stride used in the original image data. Will be adjusted to the new stride value.</param>
/// <returns>The image data converted to the requested amount of bits per pixel.</returns>
public static Byte[] ConvertFrom8Bit(Byte[] data8bit, Int32 width, Int32 height, Int32 bitsLength, Boolean bigEndian, ref Int32 stride)
{
Int32 parts = 8 / bitsLength;
// Amount of bytes to write per line
Int32 newStride = ((bitsLength * width) + 7) / 8;
// Bit mask for reducing original data to actual bits maximum.
// Should not be needed if data is correct, but eh.
Int32 bitmask = (1 << bitsLength) - 1;
Byte[] dataXbit = new Byte[newStride * height];
// Actual conversion process.
for (Int32 y = 0; y < height; y++)
{
for (Int32 x = 0; x < width; x++)
{
// This will hit the same byte multiple times
Int32 indexXbit = y * newStride + x / parts;
// This will always get a new index
Int32 index8bit = y * stride + x;
// Amount of bits to shift the data to get to the current pixel data
Int32 shift = (x % parts) * bitsLength;
// Reversed for big-endian
if (bigEndian)
shift = 8 - shift - bitsLength;
// Get data, reduce to bit rate, shift it and store it.
dataXbit[indexXbit] |= (Byte)((data8bit[index8bit] & bitmask) << shift);
}
}
stride = newStride;
return dataXbit;
}
请注意,bigEndian
参数指的是位块的顺序。通常,4bpp 中的字节 12 34
只会为您提供像素 1 2 3 4
,在这种情况下适用大端规则(值的数学上最大的部分作为第一个像素处理)。 1bpp也是如此;值 37 通常会给出像素 0 0 1 1 0 1 1 1
。但是在我开发的旧 DOS 游戏的一些自定义文件格式中,情况并非如此(4bpp 12 34
会给出像素 2 1 4 3
),因此函数具有该参数的原因。
start
参数同样存在,因为我使用它的数据是从自定义文件格式中读取的。通常这应该总是 0.