如何从2D数组创建并写入图像?

3

我在C#中有一个Color[,]数组(二维数组,其中元素类型为Drawing.Color),如何将其保存为PNG图像文件?

只使用构建中提供的 .Net 官方包,不使用任何其他库或 Nuget 包。


你必须使用位图(Bitmap)并调用 Bitmap.Save。Bitmap 对象本身是一个三维数组(X、Y、RGB),因此你完全可以通过使用 Bitmap 而不是 Color 数组来避免使用二维颜色数组。Bitmap 对象包含设置/获取特定坐标处像素颜色的函数。 - Prime
要画出东西,你需要颜色和形状。仅有颜色是无法画出图形的。 - Krivitskiy Grigoriy
1
OP和点赞者可能需要复习一下[提问]。 - user585968
这是WinForms吗?WPF? - user585968
2个回答

4

简单方法

首先创建一个指定尺寸的空白Bitmap实例:

Bitmap bmp = new Bitmap(100, 100);

遍历颜色数组并绘制像素:

for (int i = 0; i < 100; i++)
{
  for (int j = 0; j < 100; j++)
  {
    bmp.SetPixel(i, j, colors[i,j]);
  }
}

最后,将位图保存到文件中:

bmp.Save("myfile.png", ImageFormat.Png);

更快的方法

Bitmap.SetPixel 方法速度较慢。要快速访问位图像素,更好的方法是直接写入一个32位值的数组(假设你正在制作32位PNG),并将该数组作为Bitmap类的支持。

一种实现方式是创建该数组,并获取一个GCHandle以防止其被垃圾回收。Bitmap类提供了一个构造函数,允许你从一个数组指针、一个像素格式和一个步幅(组成单个像素数据行的字节数)创建一个实例:

public Bitmap (int width, int height, int stride,
  System.Drawing.Imaging.PixelFormat format, IntPtr scan0);

这是创建后备数组、句柄和Bitmap实例的方法:
Int32[] bits = new Int32[width * height];
GCHandle handle = GCHandle.Alloc(bits, GCHandleType.Pinned);
Bitmap bmp = new Bitmap(width, height, width * 4, 
  PixelFormat.Format32bppPArgb, handle.AddrOfPinnedObject());

请注意:
  • 备用数组具有32位条目,因为我们使用32位像素格式
  • Bitmap步幅为width*4,这是单行像素占用的字节数(每个像素4个字节)
有了这个,你现在可以直接将像素值写入备用数组中,并且它们会反映在Bitmap中。这比使用Bitmap.SetPixel快得多。以下是代码示例,假设您已经把所有东西都封装在一个知道位图宽度和高度的类中:
public void SetPixelValue(int x, int y, int color)
{
  // Out of bounds?
  if (x < 0 || x >= Width || y < 0 || y >= Height) return;

  int index = x + (y * Width);
  Bits[index] = color;
}

请注意,color 是一个 int 值,而不是 Color 值。如果您有一组 Color 值的数组,则必须先将每个值转换为 int,例如:
public void SetPixelColor(int x, int y, Color color)
{
  SetPixelValue(x, y, color.ToArgb());
}

这种转换需要时间,因此最好一路使用int值进行操作。如果您确定不会使用越界坐标,则可以通过放弃x/y边界检查来进一步加快速度:

public void SetPixelValueUnchecked(int x, int y, int color)
{
  // No out of bounds checking.
  int index = x + (y * Width);
  Bits[index] = color;
}

这里需要注意一点,如果您以这种方式包装Bitmap,仍然可以直接访问Bitmap实例来使用Graphics绘制线条、矩形、圆等东西,但是没有通过固定数组获得速度增益。如果您希望这些原语也能更快地绘制,则必须提供自己的线条/圆形实现。请注意,根据我的经验,自己的Bresenham线路由很难超越GDI内置的线路,因此可能不值得。

更快的方法

如果您能够一次性设置多个像素,则速度可能会更快。如果您有一个具有相同颜色值的水平像素序列,则可以应用此方法。我发现使用Buffer.BlockCopy设置数组中的序列是最快的方法(参见这里进行讨论)。这是一个实现示例:

/// <summary>
/// Set a sequential stretch of integers in the bitmap to a specified value.
/// This is done using a Buffer.BlockCopy that duplicates its size on each
/// pass for speed.
/// </summary>
/// <param name="value">Fill value</param>
/// <param name="startIndex">Fill start index</param>
/// <param name="count">Number of ints to fill</param>
private void FillUsingBlockCopy(Int32 value, int startIndex, int count)
{
  int numBytesInItem = 4;

  int block = 32, index = startIndex;
  int endIndex = startIndex + Math.Min(block, count);

  while (index < endIndex)          // Fill the initial block
    Bits[index++] = value;

  endIndex = startIndex + count;
  for (; index < endIndex; index += block, block *= 2)
  {
    int actualBlockSize = Math.Min(block, endIndex - index);
    Buffer.BlockCopy(Bits, startIndex * numBytesInItem, Bits, index * numBytesInItem, actualBlockSize * numBytesInItem);
  }
}

这将特别有用,当您需要快速清除位图,并使用水平线(例如在三角形光栅化之后)填充矩形或三角形时。请参考:triangle rasterization

有没有更快的方式,比如不需要迭代所有行、列,按行/数组设置? - Danial
@Daniel 我已更新我的回答,提出了加速设置 Bitmap 中像素的一些观察。 - Alexander van Oostenrijk
很好的答案,但你快速给出的例子假设是一个一维数组。问题明确涉及到二维数组。 - john k

3
// Color 2D Array
var imgColors = new Color[128, 128];

// Get Image Width And Height Form Color Array
int imageH = imgColors.GetLength(0);
int imageW = imgColors.GetLength(1);

// Create Image Instance
Bitmap img = new Bitmap(imageW, imageH);

// Fill Colors on Our Image
for (int x = 0; x < img.Width; ++x)
{
    for (int y = 0; y < img.Height; ++y)
    {
        img.SetPixel(x, y, imgColors[x, y]);
    }
}

// Just Save it
img.Save("image.png", ImageFormat.Png);

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