图像逐像素枚举、像素格式转换及更多
一组方便的扩展方法,可帮助您快速修改图像。
介绍
本文将介绍一个通用的图像(C# 类)枚举器,它可以让您的生活变得更加轻松。它不基于 SetPixel
/GetPixel
方法,因此速度快,但在 C# 托管环境中仍然安全。此类别可以帮助您读取或写入所有像素,同时您将有机会根据需要修改像素颜色。您有足够的空间来扩展此枚举器,并为自己制作一个——例如——快速转换为灰度的方法。可能性仅受您的想象力限制。
版本 2.0 与文章《一个简单而强大的调色板量化器》中的版本 5.0(有点)同步。它包含更多选项、速度优化、并行处理等等。快去看看吧!
免责声明:版本 2.0 使本文描述的大部分接口和 API 过时。如果您无法处理版本 2.0,则应使用版本 1.0。或者在下面的论坛中提问。我只包含了版本 2.0 示例。
背景
我正在开发图像编辑器,我需要某种方式将图像从剪贴板粘贴到我的应用程序中。但不幸的是,我的编辑器将处理多种像素格式(实际上是很多),并且从剪贴板来的图像都是各种不同的格式。所以我需要一种方法来将许多不同的格式转换为我的许多格式。于是我决定编写一个通用的像素格式转换器。我琢磨着,实现这一点的最简单方法是,无论格式如何,以某种方式枚举所有像素。然后简单地从源图像中读取一个像素并将其粘贴到目标图像中。所以我正是这样做的。我在网上(当我搜索这种东西时)看到许多人试图简单地解析图像,或者解析它并对每个像素进行细微修改。因此,我准备了一些扩展方法,它们将帮助您快速实现目标,而无需研究 Microsoft 像素格式和其他沿途的干扰。
Using the Code
好处在于 System.Drawing.Image
类的扩展方法形式,因此一旦引用,它们将作为任何 Image 实例的标准方法出现。此外,还有一个 Pixel
类,它完成所有像素读取/写入工作。它包含任何像素格式的颜色值。我将在下面描述所有这些方法。
列表
以下是扩展方法的列表,其中一些只是从 Bitmap
类到 Image
类使用的桥梁。
-
BitmapData LockBits(this Image image, ImageLockMode lockMode)
-
void UnlockBits(this Image image, BitmapData data)
-
IEnumerable>Pixel< EnumerateImagePixels(this Image image, ImageLockMode accessMode)
-
Image ChangePixelFormat(this Image image, PixelFormat targetFormat, IColorQuantizer quantizer)
-
void AddColorsToQuantizer(this Image image, IColorQuantizer quantizer)
-
List<Color> GetPalette(this Image image)
-
void SetPalette(this Image image, List<Color> palette)
LockBits() 和 UnlockBits() 方法
这些方法与 Bitmap.LockBits()
和 Bitmap.UnlockBits()
方法的功能相同。只是它们是在图像上提供的,并且它们还以 Image.PixelFormat
格式锁定数据。LockBits()
方法返回锁定的图像数据,必须通过 UnlockBits()
方法解锁才能使用。即使您只是从图像中读取,也必须解锁它。这两个方法主要用于内部使用,但仍然提供。所以希望您根本不需要这些方法。
EnumerateImagePixels() 方法
此方法是本文的核心方法。它允许您——根据提供的访问模式(ImageLockMode
)——读取、写入或同时进行读写。只需提供一个图像,并通过一个结构 Pixel
在一个简单的循环中处理其所有像素。
foreach (Pixel pixel in myImage.EnumerateImagePixels(ImageLockMode.ReadWrite))
{
// modify a pixel here
}
无论源图像是 4 位索引格式还是 32 位真彩色格式,像素结构都可以处理它们。一旦我完成了这项工作,似乎可以更进一步,添加一个方法来在两种像素格式之间进行转换,现在应该很容易了。有关更多信息,请查看像素结构部分。这带来了我们的下一个方法,也是像素枚举的第一个成果。
ChangePixelFormats() 方法
此方法允许您更改给定图像的格式,只要图像处于支持的格式(您可以使用 IsSupported
方法检查),并且目标格式也受支持。它基本上枚举源图像像素以进行读取,而目标图像则枚举以进行写入。然后从源读取每个像素并将其设置到目标。在此过程中,它会根据需要进行转换,或者在深色格式的情况下,按原样复制。您需要提供一个 IColorQuantizer
用于从非索引格式到索引格式的转换。它将自动从图像中收集信息,并执行量化。将创建一个调色板并将其设置到目标图像上。用法很简单,如下所示
Image targetImage = sourceImage.ChangePixelFormat
(PixelFormat.Format8bppIndexed, myQuantizer);
其余方法
这些是 AddColorsToQuantizer()
、GetPalette()
和 SetPalette()
。第一个方法允许您处理多个图像以拥有一个公共调色板。它也使用枚举。它将所有像素颜色添加到量化器中,以便所有这些图像以后使用。GetPalette()
方法将随后从量化器中检索统一的调色板。最后一个方法 SetPalette()
将此调色板设置到您喜欢的任何图像。请参阅上一篇关于调色板量化的文章(此处),以获取有关其工作原理的更详细信息。
PixelFormat 扩展方法
在此过程中,我还为 PixelFormat
枚举添加了一些扩展。GetBithDepth
方法确定每种格式像素所需的位计数。GetColorCount
让您知道该像素格式有多少种颜色可用。此方法仅适用于索引格式。格式是否为索引格式可以通过另一个方法 IsIndexed
确定,该方法对所有索引格式返回 True
,对所有高色和真彩色格式返回 False
。还有 IsSupported
方法,我的演示使用了它。它基本上是检查格式是否受 Microsoft 支持的列表,因为并非所有列出的格式都实际受支持(例如 16bppGrayscale
)。如果 HasAlpha
返回 True
,则像素格式中还包含透明颜色分量。最后一个方法 IsDeepColor
确定格式是否为非标准格式。也就是说,如果它每个颜色分量使用超过 8 位。这些通常是特殊模式,应谨慎处理。
像素结构
这个类在内部处理所有这些格式差异,而在外部则几乎像旧的 SetPixel
/GetPixel
组合一样运行。它包含三对方法和三个属性。
GetColor、SetColor 和 Color
前两种方法用于读取和写入非索引格式的颜色值。Color
基本上只是封装了这两种方法。
GetIndex、SetIndex 和 Index
这些方法和属性也是如此。只是它们用于索引像素格式。要获取颜色,您需要首先从图像的调色板中检索它,如下所示
Color color = myImage.Palette.Entries[pixel.Index]
我可以想象一个方法,一个包装器,用于其中一些扩展方法,它将进一步自动化该过程,但我总是留出一些空间让您自己去解决。将事情保持在基本状态。
GetValue、SetValue 和 Value
最后,这些用于处理 48 位和 64 位格式,因为它们不幸地不适合标准的 Color
结构。
一个简单示例
(仅适用于版本 1.0)
为了向您展示此功能的基使用理念,我编写了一个示例程序,它获取一张图像并将其转换为灰度。目前我只介绍这些。在编写此编辑器时,我肯定会很快发布另一篇文章,因为会出现许多不寻常的事情。
using System;
using System.Drawing;
using System.Drawing.Imaging;
using Extensions;
using Extensions.Helpers;
namespace Sample
{
// this sample only works for non-indexed format images,
// but can be easily extended to support them
static class Program
{
static void Main()
{
// loads the source image
Image image = Image.FromFile("Source.png");
// converts it to a grayscale
foreach (Pixel pixel in image.EnumerateImagePixels(ImageLockMode.ReadWrite))
{
Color color = pixel.GetColor();
Int32 gray = (color.R + color.G + color.B)/3;
Color outputColor = Color.FromArgb(color.A, gray, gray, gray);
pixel.SetColor(outputColor);
}
// saves the converted image
image.Save("Output.png", ImageFormat.Png);
}
}
}
一个不那么简单的示例 
(仅适用于版本 2.0)
我还包含了版本 2.0 的示例,它试图模仿版本 1.0 中的示例,但存档中的代码实际上包含带有“真实”灰度计算的扩展版本,正如 nb2 所指出的。using System;
using System.Drawing;
using System.Drawing.Imaging;
using ImagePixelEnumerator.Helpers;
namespace Sample
{
static class Program
{
static void Main()
{
// loads the source image
Image image = Image.FromFile("Source.png");
// prepares our target image
Image targetImage;
// this sample only works for non-indexed format images,
// actually the code inside the archive contains such 2-pass version
ImageBuffer.TransformImagePerPixel(image, PixelFormat.Format32bppArgb, null, out targetImage, null, 4, (passIndex, sourcePixel, targetPixel) =>
{
Color color = sourcePixel.GetColor();
Int32 gray = (color.R + color.G + color.B)/3;
Color grayColor = Color.FromArgb(color.A, gray, gray, gray);
targetPixel.SetColor(grayColor, null);
return true;
});
// saves the converted image
targetImage.Save("Output.png", ImageFormat.Png);
}
}
}
关注点
- Microsoft 实际上(正如他们所描述的“有点”)只支持 48 位和 64 位像素格式中每个颜色分量的最初 13 位。
- 您确实应该释放
IEnumerator<>
类。
相关文章
历史
- 2013-06-20:版本 2.0 发布
- 2013-06-20:添加了“一个不那么简单的示例”部分
- 2010-03-18:发布了初始文章