当我的位图使用不同的索引 PixelFormat 时,为什么需要将 PixelFormat.Format8bppIndexed 传递给 Bitmap.LockBits?

问题描述 投票:0回答:1

我发现这个答案展示了如何使用

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???

LockBits

方法的
文档告诉我第三个参数是

一个

PixelFormat
枚举,指定此 Bitmap 的数据格式

所以我应该使用

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

c# bitmap gdi+
1个回答
0
投票

简短的回答是,

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

© www.soinside.com 2019 - 2024. All rights reserved.