Introduction
In this article, a universal Image (C# class) enumerator will be introduced - one that can make your life much more easier. It's not based on SetPixel
/GetPixel
methods, so it's fast, yet still safe in the C# managed environment. This class can help you to read or write all the pixels, while you'll have a chance to modify the pixel color as you'll see fit. There's plenty of room for you to extend on this enumerator, and make yourself a - for example - quick convertor to a grayscale method. The possibilities are only limited by your imagination.
The version 2.0, is (kinda) synchronized with version 5.0 in an article: A Simple - Yet Quite Powerful Palette Quantizer. It contains many more options, speed optimizations, parallel processing, and much more. Check it out!
Disclaimer: Version 2.0 makes most of the interfaces, and API described in this article obsolete. If you're unable to handle version 2.0, you should use version 1.0. Or ask in the forum below. I've included version 2.0 sample only.
Background
I'm working on the image editor, and I needed some way to paste the images from clipboard to my application. But unfortunately my editor is about to handle several pixel formats (many actually), and the images came from clipboard in all different formats. So I needed a way to convert from many different formats to my many formats. So I decided to write universal pixel format converter. The easiest way to achieve that, I wondered, was to somehow enumerate all the pixels no matter the format. And then simply read one pixel in a source image and paste it into a target image. So I did exactly that. I saw many people on the internet (when I was searching for such a thing) were trying to simply parse an image, or parse it and do a slight modification per pixel. So I prepared few extensions methods, that will help you to quickly achieve your goal, without studying Microsoft pixel formats, and other distractions along the way.
Using the Code
The goodness comes in a form of the extensions methods, for System.Drawing.Image
class, so once referenced, they will appear as the standard methods for any Image instance. Also, there's one class Pixel
that does all the pixel reading/writing work. It contains a color value of any pixel format there is. I will describe all those methods down below.
The List
Here goes a list of the extension methods, some of them are just a bridge from Bitmap
class to be used in Image
class.
-
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)
The LockBits() and UnlockBits() Methods
These methods do the same thing as the Bitmap.LockBits()
and Bitmap.UnlockBits()
methods. Only they're provided on the image, and also they lock the data in a Image.PixelFormat
format. The LockBits()
method returns a locked image data, those must be unlocked by the UnlockBits()
method to be of any use. Even when you're just reading from an image, you've to unlock it. Those two methods are primarily for internal use, but are provided anyway. So hopefully, you won't need those methods at all.
The EnumerateImagePixels() Method
This method is the core method of this article. It allows you - depending on the access mode provided (ImageLockMode
) - read, write or both at the same time. Just supply an image, and process all its pixels via a structure Pixel
in a simple loop:
foreach (Pixel pixel in myImage.EnumerateImagePixels(ImageLockMode.ReadWrite))
{
}
It doesn't matter if the source image is a 4-bit indexed, or a 32-bit truecolor format. The pixel structure can handle them all. Once I accomplished that, it seemed like a good idea to go one step further, and add a method which will convert between two pixel formats, which should now be easy. Checkout the The Pixel structure section for more information. This brings us to our next method, the first fruit of pixel enumeration.
The ChangePixelFormats() Method
This method lets you to change the format of a given image, as long as the image is in a supported format (you can check that with IsSupported
method), and the target format is also supported. It basically enumerates the source image pixels for read, while the target image is enumerated for write. Then each pixel is read from the source and set on the target. In between, it's transformed as needed, or in case of deep-color formats, copied as is. You need to provide a IColorQuantizer
for the conversion from non-indexed format to an indexed one. It will automatically gather the information from the image, and performs the quantization. A palette will be created and set on the target image. The usage is easy as:
Image targetImage = sourceImage.ChangePixelFormat
(PixelFormat.Format8bppIndexed, myQuantizer);
The Rest of the Methods
Those are AddColorsToQuantizer()
, GetPalette()
and SetPalette()
. The first one lets you process the multiple images to have a common palette. It also uses the enumeration. It adds all the pixel colors to the quantizer to be latter used by all those images. The GetPalette()
method will afterwards retrieve the unified palette from the quantizer. The last method SetPalette()
sets this palette to any image you like. See the previous article (here) about palette quantization to get more detailed information about how that works.
The PixelFormat Extension Methods
While I was at it, I also added some extensions to the PixelFormat
enumeration. The GetBithDepth
method determines a bit count needed per the format's pixel. The GetColorCount
lets you know how much color is available for that pixel format. This method is only allowed for indexed formats. Whether a format is indexed or not can be determined by another method IsIndexed
, which returns True
for all the indexed formats, and False
for all the highcolor and truecolor formats. There's also the IsSupported
method which is used by my demo. It's basically a list check whether the format is supported by Microsoft because not all of the listed formats are actually supported (for example 16bppGrayscale
). If the HasAlpha
returns a True
then the pixel format contains also a transparency color component in it. The last method IsDeepColor
determines whether a format is non-standard. That is, if it uses more than 8-bit per color component. These are usually special modes, and should be handled with care.
The Pixel Structure
This class handles internally all those format differences, while on the outside is behaving almost like an old SetPixel
/GetPixel
combo. It contains three pairs of methods and three properties.
GetColor, SetColor and Color
The first two methods are used to read and write color value for the non-indexed formats. The Color
just basically wraps both methods.
GetIndex, SetIndex and Index
The same goes for these two methods and properties. Only these are used for indexed pixel formats. To get a color, you need to have to first retrieve it from your image's palette like this:
Color color = myImage.Palette.Entries[pixel.Index]
I can imagine a method, a wrapper for some of those extension methods, which will automatize the process further, but I always leave some space to figure it out for yourself. Keeping things at their basic state.
GetValue, SetValue and Value
Finally, these are used to handle 48-bit and 64-bit formats, as they unfortunately don't fit into a standard Color
structure.
A Simple Sample
(for version 1.0 only)
Just to show you the basic idea of how this stuff is used, I put together a sample program, which takes an image and converts it to a grayscale. That's all from me for now. I surely will be posting another article soon as many unusual things are popping up during this editor programming.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using Extensions;
using Extensions.Helpers;
namespace Sample
{
static class Program
{
static void Main()
{
Image image = Image.FromFile("Source.png");
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);
}
image.Save("Output.png", ImageFormat.Png);
}
}
}
A Not So Simple Sample
(for version 2.0 only)
I've included an example for version 2.0 as well, it tries to mimic the example in version 1.0, but code inside the archive actually contains the extended version with
"real" gray-scale calculation as pointed out by
nb2.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using ImagePixelEnumerator.Helpers;
namespace Sample
{
static class Program
{
static void Main()
{
Image image = Image.FromFile("Source.png");
Image targetImage;
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;
});
targetImage.Save("Output.png", ImageFormat.Png);
}
}
}
Points of Interest
- Microsoft actually (as they describe it "kind of") supports only first 13-bits per color component in the 48-bit and 64-bit pixel formats.
- You really should dispose the
IEnumerator<>
classes.
Related Articles
History
- 2013-06-20: Version 2.0 published
- 2013-06-20: The section A Not So Simple Sample added
- 2010-03-18: The initial article was posted