我发现这个答案展示了如何使用
PixelFormat.Format8bppIndexed
创建位图,并希望对其进行调整以覆盖其他索引格式(Format1bppIndexed
和Format4bppIndexed
)并使用我提供的调色板而不是灰度调色板。这是生成的代码(为了简洁起见,我省略了一堆验证代码):
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
/// <summary>
/// Static class to help with creating a Bitmap using one of the indexed pixel formats.
/// </summary>
[SupportedOSPlatform("windows")]
public static class IndexedBitmapHelper
{
/// <summary>
/// Sets the palette and pixels of an indexed Bitmap.
/// </summary>
/// <param name="destinationBitmap">The Bitmap to populate.</param>
/// <param name="colourTable">
/// An array of the possible colours that the Bitmap's pixels can be set to.
/// </param>
/// <param name="colourIndexes">
/// Each entry in this array represents one pixel and is an index into the <paramref name="colourTable"/>.
/// </param>
public static void Populate(
Bitmap destinationBitmap,
Color[] colourTable,
byte[] colourIndexes)
{
SetPalette(destinationBitmap, colourTable);
SetPixels(destinationBitmap, colourIndexes);
}
private static void SetPalette(Bitmap destinationBitmap, Color[] colourTable)
{
var numberOfColours = colourTable.Length;
// The Palette property is of type ColorPalette, which doesn't have a public constructor
// because that would allow people to change the number of colours in a Bitmap's palette.
// So instead of creating a new ColorPalette, we need to take a copy of the one created
// by the Bitmap constructor.
var copyOfPalette = destinationBitmap.Palette;
for (var i = 0; i < numberOfColours; i++)
{
copyOfPalette.Entries[i] = colourTable[i];
}
destinationBitmap.Palette = copyOfPalette;
}
private static void SetPixels(Bitmap destinationBitmap, byte[] colourIndexes)
{
var width = destinationBitmap.Width;
var height = destinationBitmap.Height;
var data = destinationBitmap.LockBits(
new Rectangle(0, 0, width, height),
ImageLockMode.WriteOnly,
PixelFormat.Format8bppIndexed); // <--- why???
var dataOffset = 0;
var scanPtr = data.Scan0.ToInt64();
for (var y = 0; y < height; ++y)
{
// Copy one row of pixels from the colour indexes to the destination bitmap
Marshal.Copy(colourIndexes, dataOffset, new IntPtr(scanPtr), width);
dataOffset += width;
scanPtr += data.Stride;
}
destinationBitmap.UnlockBits(data);
}
}
使用示例,创建 1bpp(两种颜色)图像并将其显示在 Windows 窗体上的
PictureBox
中:
var width = 10;
var height = 12;
var colourTable = new Color[] { Color.Red, Color.Green };
var colourIndexes = new byte[]
{
0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 0, 0, 0, 0, 1,
1, 0, 1, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 0, 0, 1, 1, 0, 1, 0,
1, 0, 1, 1, 1, 0, 0, 0, 1, 0,
1, 0, 0, 0, 0, 1, 1, 1, 1, 0,
1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
};
using var destinationBitmap = new Bitmap(width, height, PixelFormat.Format1bppIndexed);
IndexedBitmapHelper.Populate(destinationBitmap, colourTable, colourIndexes);
pictureBox1.Image = destinationBitmap;
这工作得很好,但是我对
SetPixels
方法中的这一行感到困惑:
var data = destinationBitmap.LockBits(
new Rectangle(0, 0, width, height),
ImageLockMode.WriteOnly,
PixelFormat.Format8bppIndexed); // <--- why???
方法的文档告诉我第三个参数是
一个
枚举,指定此 Bitmap 的数据格式PixelFormat
所以我应该使用
destinationBitmap.PixelFormat
,即我正在创建的PixelFormat
的Bitmap
,作为这个参数的值,对吧?显然不是,因为当我这样做并创建 1bpp 或 4bpp 位图时,所有像素似乎都位于错误的位置。对于上面的 1bpp 示例,这会生成一个如下所示的位图(R = 红色像素,G = 绿色像素)
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRRRR
RRRRRRRGRR
RRRRRRRGRR
RRRRRRRGRR
RRRRRRRGRR
RRRRRRRGRR
RRRRRRRGRR
而不是预期的位图
RRRRRGGGGG
RGGGGRRRRG
RGGGGRRRRG
RGGGGRRRRG
RGGGGRRRRG
RGGGGRRRRG
GRGRRGGRGR
GRGRRGGRGR
GRGRRGGRGR
GRGGGRRRGR
GRRRRGGGGR
GGGGGRRRRR
但是,当我使用硬编码值
PixelFormat.Format8bppIndexed
时,会为所有三种格式正确创建位图。
我猜这与组成
Bitmap
对象的数据如何在内存中布局有关,但是任何人都可以向我解释为什么只有当我通过 PixelFormat.Format8bppIndexed
而不是实际的 时才能正确工作。我正在创建的
PixelFormat
的 Bitmap
?
简短的回答是,
format
方法的LockBits
参数与PixelFormat
的Bitmap
属性无关。相反,它指示用于表示填充位图的字节数组中的单个像素的位数。
因此,对于问题中的示例 1bpp 图像,使用
PixelFormat.Format8bppIndexed
作为此参数的值是正确的,因为源数组使用整个字节来表示每个像素,即使我们只需要一位来保存可能的值每个像素。大多数时候这样做很好,而且(无论如何在我看来),这比其他选择更容易,并且代码更具可读性。
但是,这并不是一种非常有效的内存使用方式,因为每八位中只有一位(如果创建 4bpp 位图则每八位中只有四位)包含有用信息。如果处理大型 1bpp 或 4bpp 图像,那么理论上我们可以使用一个数组,该数组每像素使用与位图相同的位数。例如,要创建带有颜色索引的 8x2 像素 1bpp 图像...
0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0
...我们可以使用这样的数组,并将
PixelFormat.Format1bbpIndexed
作为 format
方法的 LockBits
参数传递。
var colourIndexes = new byte[]
{
0b01010101,
0b10101010,
}
如何实现这是另一回事。
width * bits per pixel
不是 8 的倍数的位图可能很难正确处理。我们还不能忘记,Marshal.Copy
实际上是直接写入Bitmap
对象占用的内存区域,并且错误地执行此类操作可能会导致意外且完全令人困惑的错误,例如堆损坏异常0xC0000374 。事实上,我目前正在开发的实现似乎就存在这样的错误。如果我设法修复它,那么我会回来更新此答案/发布新答案。
同时,只要不担心内存消耗,无论
PixelFormat.Format8bppIndexed
的实际 PixelFormat
是多少,都可以使用 Bitmap
。