为什么在System.Drawing.Bitmap构造函数中的“stride”参数必须是4的倍数?

29
我正在编写一个应用程序,需要将专有的位图格式(MVTec Halcon HImage)转换为C#中的System.Drawing.Bitmap。
给我提供帮助完成此操作的唯一专有函数要求我写入文件,除了使用“获取指针”函数之外。
这个函数很好用,它给了我一个指向像素数据、宽度、高度和图像类型的指针。
我的问题是,在使用构造函数创建System.Drawing.Bitmap时:
new System.Drawing.Bitmap(width, height, stride, format, scan)

我需要指定一个是4的倍数的“步幅”。 这可能是个问题,因为我不确定我的函数将处理多大的位图。 假设最终得到的位图是111x111像素,我没有其他办法运行此函数,除非向我的图像添加虚假列或减去3列。

有没有办法可以规避这个限制?

6个回答

72

这涉及到早期CPU设计。最快处理位图的方法是每次读取32位,从扫描行的开头开始。当扫描行的第一个字节在32位地址边界上对齐时,这种方法效果最好。换句话说,是4的倍数地址。在早期的CPU中,若第一个字节未对齐会额外消耗CPU周期来读取两个32位单词并调整字节以创建32位值。确保每行扫描从对齐地址开始(如果步幅为4的倍数,则自动对齐)避免了这种情况。

现代CPU上这已经不再是一个真正的问题了,现在对齐到缓存线边界更加重要。尽管如此,步幅需要是4的倍数仍然存在于应用程序兼容性的考虑中。

顺便说一下,您可以使用以下公式轻松计算步幅:

        int bitsPerPixel = ((int)format & 0xff00) >> 8;
        int bytesPerPixel = (bitsPerPixel + 7) / 8;
        int stride = 4 * ((width * bytesPerPixel + 3) / 4);

2
这是正确的答案,加一整天。这里有更多关于计算“步幅”的信息:https://dev59.com/SUvSa4cB1Zd3GeqPbRG6#1983886。 - jason
所以,我最好尝试更改我的图像格式。 现在我正在使用将每个像素存储为单个字节的东西。话虽如此,我想我不能使用那个位图构造函数,因为没有一种图像类型仅使用单个字节并且不依赖于奇怪的颜色映射。 - Gorchestopher H
没错,8bpp需要调色板。GDI+对它们的支持不太好,会给你带来无尽的麻烦。你可以通过加载现有的8bpp图像来开始使用它。如果它具有正确的大小,你可以窃取它的调色板条目或直接锁定它的位图。或者在MemoryStream中合成一个。 - Hans Passant
谢谢大家的建议。最终我自己制作了调色板,现在我只是强制我的图像宽度为4的倍数。 - Gorchestopher H
@HansPassant 你好!我对了解为什么扫描线的第一个字节必须对齐到32位地址边界很感兴趣,但我不确定我能从答案中理解得很好。你能否提供更详细的资源?谢谢! - kzidane
对于8bpp以下的格式,步幅计算实际上是错误的;在对其进行(x+7)/8操作之前,需要立即将bitsPerPixel乘以宽度。如果无法将“每像素字节数”表示为整数值,那么它就不是有意义的数据,这在4位或1位图像中是适用的。 - undefined

6
一种更简单的方法是只用 (width, height, pixelformat) 构造函数创建图像。然后它会自动处理步幅问题。
然后,您可以使用 LockBits 按行将图像数据复制到其中,而无需自己处理步幅;您可以从 BitmapData 对象中直接请求这些。对于每个扫描线的实际复制操作,只需通过跨度增加目标指针,将源指针增加为行数据宽度。
以下是一个示例,其中我使用字节数组获取了图像数据。如果那是完全紧凑的数据,则输入步幅通常只是图像宽度乘以每像素字节数量。如果它是 8 位调色板数据,则它就是宽度。
如果图像数据来自图像对象,则应以完全相同的方式通过从 BitmapData 对象中获取原始步幅来存储该提取过程的步幅。
/// <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</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 = stride < 0;
    stride = Math.Abs(stride);
    // Cache these to avoid unnecessary getter calls.
    Int32 targetStride = 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;
}

4
正如Jake之前所说,你可以通过找到每像素字节数(16位为2,32位为4)并将其乘以宽度来计算步幅。因此,如果你有111个像素宽度和一个32位图像,那么你会得到444,这是4的倍数。
然而,假设你有一个24位图像。24位等于3个字节,所以对于111个像素宽度,你的步幅将是333。显然,这不是4的倍数。因此,你需要向上舍入到336(下一个最高的4的倍数)。即使你有一些额外的未使用空间,但在大多数应用程序中,这些未使用的空间并不足以产生太大的影响。
不幸的是,除非你始终使用32位或64位图像(它们总是4的倍数),否则没有办法避免这个限制。

2

请记住,stridewidth是不同的。您可以有一张每行有111个(8位)像素的图像,但每行在内存中存储了112个字节。

这样做是为了有效利用内存,正如@Ian所说,它将数据存储在int32中。


1

正确的代码:

public static void GetStride(int width, PixelFormat format, ref int stride, ref int bytesPerPixel)
{
    //int bitsPerPixel = ((int)format & 0xff00) >> 8;
    int bitsPerPixel = System.Drawing.Image.GetPixelFormatSize(format);
    bytesPerPixel = (bitsPerPixel + 7) / 8;
    stride = 4 * ((width * bytesPerPixel + 3) / 4);
}

不过,以下8bpp以下的格式不适用。 - undefined

1

因为它使用int32来存储每个像素。

Sizeof(int32) = 4

不过别担心,当图像从内存保存到文件时,它会使用最高效的内存使用方式。内部使用每个像素24位(8位红色、8位绿色和8位蓝色),并将最后的8位作为冗余。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接