【问题标题】:Why must "stride" in the System.Drawing.Bitmap constructor be a multiple of 4?为什么 System.Drawing.Bitmap 构造函数中的“步幅”必须是 4 的倍数?
【发布时间】:2011-01-12 06:12:02
【问题描述】:

我正在编写一个应用程序,该应用程序需要我采用专有位图格式(MVTec Halcon HImage)并将其转换为 C# 中的 System.Drawing.Bitmap。

提供给我的唯一专有功能可以帮助我完成此操作,包括写入文件,但使用“获取指针”功能除外。

这个函数很棒,它给了我一个指向像素数据的指针,图像的宽度、高度和类型。

我的问题是,当我使用构造函数创建 System.Drawing.Bitmap 时:

new System.Drawing.Bitmap(width, height, stride, format, scan)

我需要指定一个“步幅”,它是 4 的倍数。 这可能是一个问题,因为我不确定我的函数将使用什么大小的位图。 假设我最终得到一个 111x111 像素的位图,除了在我的图像中添加虚假列或减去 3 列之外,我无法运行此函数。

有什么办法可以绕过这个限制吗?

【问题讨论】:

    标签: c# constructor bitmap


    【解决方案1】:

    更简单的方法是使用(width, height, pixelformat) 构造函数制作图像。然后它会自己处理步幅。

    然后,您可以使用LockBits 将您的图像数据逐行复制到其中,而无需自己操心Stride 的东西;你可以从 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;
    }
    

    【讨论】:

      【解决方案2】:

      正确代码:

      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);
      }
      

      【讨论】:

        【解决方案3】:

        这可以追溯到早期的 CPU 设计。处理位图位的最快方法是一次读取 32 位,从扫描线的开头开始。当扫描线的第一个字节在 32 位地址边界上对齐时效果最佳。换句话说,地址是 4 的倍数。在早期的 CPU 上,第一个字节未对齐将花费额外的 CPU 周期来从 RAM 中读取两个 32 位字并打乱字节以创建 32 位值。确保每条扫描线从一个对齐的地址开始(如果步幅是 4 的倍数,则自动执行)可以避免这种情况。

        这在现代 CPU 上不再是一个真正的问题,现在与缓存线边界对齐更为重要。尽管如此,由于 appcompat 的原因,4 的倍数要求仍然存在。

        顺便说一句,你可以很容易地从格式和宽度计算步幅:

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

        【讨论】:

        • 这是正确答案;加一整天。这里有更多关于计算stridestackoverflow.com/questions/1983781/…
        • 所以,我很可能应该尝试更改我的图像格式。现在我正在使用将每个像素存储为单个字节的东西。话虽如此,我想我不能使用那个 Bitmap 构造函数,因为没有图像类型只需要一个字节并且不依赖于不稳定的颜色图。
        • 没错,8bpp 需要调色板。 GDI+ 不能很好地支持它们,它会给你带来无穷无尽的麻烦。您可以通过加载现有的 8bpp 图像来启动它。如果大小合适,您可以直接窃取其调色板条目或 LockBits。或者在 MemoryStream 中合成一个。
        • 感谢大家的意见。我最终制作了自己的调色板,现在我将强制我的图像宽度为 4 的倍数。
        • @HansPassant 嗨!我有兴趣了解为什么扫描线的第一个字节必须在 32 位地址边界上对齐,我不确定我能否从答案中得到很好的理解。您能否提供更详细的资源?谢谢!
        【解决方案4】:

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

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

        【讨论】:

          【解决方案5】:

          正如 Jake 之前所说,您通过查找每个像素的字节数(16 位为 2,32 位为 4)然后将其乘以宽度来计算步幅。所以如果你有一个 111 的宽度和一个 32 位的图像,你将有 444,它是 4 的倍数。

          但是,假设您有一个 24 位图像。 24 位等于 3 个字节,因此对于 111 像素宽度,您的步幅为 333。显然,这不是 4 的倍数。因此,您需要四舍五入到 336(下一个最高的 4 倍数)。即使您有一些额外的空间,这些未使用的空间也不足以真正对大多数应用程序产生很大影响。

          不幸的是,没有办法绕过这个限制(除非你总是使用 32 位或 64 位想象,它们总是 4 的倍数。

          【讨论】:

            【解决方案6】:

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

            Sizeof(int32) = 4
            

            但是不用担心,当图像从内存保存到文件时,它将尽可能使用最有效的内存使用。在内部,它使用每像素 24 位(8 位红色、8 位绿色和 8 位蓝色)并保留最后 8 位冗余。

            【讨论】:

              猜你喜欢
              • 2020-11-15
              • 1970-01-01
              • 2019-10-06
              • 2018-04-27
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-12-12
              • 1970-01-01
              相关资源
              最近更新 更多