Introduction
Sometimes a program or a component need to pick up a pixel color from a given image. Note, I'm talking not about a "color chooser" but a color picker, the sort of an eyedropper reusable code or a component.
If you search the web, you’ll find some discussions, but only a few color picker code examples. One very cool example I've found is the WPF Pixel Color Under Mouse in Lee Brimelow's blog post. The same code is used by Sacha Barber in his nice WPF: A Simple Color Picker With Preview article. But, my attempt to use this code revealed that it's devised probably to work under very restrictive conditions, and some enhancements are required to get it to work in more general cases.
I started with Lee Brimelow's code, and I reproduce it below to be able to discuss it later:
private byte[] pixels;
public Window1()
{
InitializeComponent();
wheel.MouseMove += new MouseEventHandler(wheel_MouseMove);
}
void wheel_MouseMove(object sender, MouseEventArgs e)
{
try
{
CroppedBitmap cb = new CroppedBitmap(wheel.Source as BitmapSource,
new Int32Rect((int)Mouse.GetPosition(this).X,
(int)Mouse.GetPosition(this).Y, 1, 1));
pixels = new byte[4];
try
{
cb.CopyPixels(pixels, 4, 0);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
Console.WriteLine(pixels[0] + ":" + pixels[1] +
":" + pixels[2] + ":" + pixels[3]);
rec.Fill = new SolidColorBrush(Color.FromRgb(pixels[2], pixels[1], pixels[0]));
}
catch (Exception exc)
{
}
}
Listing 1. Lee Brimelow's code
The "wheel
" is the image element, and "rec
" is the rectangle to show the selected color; both are defined in a sample XAML, which isn't provided. "wheel
" is initialized with some bitmap, so wheel.Source
is a BitmapSource
.
When starting to work with this code, I got the following questions:
- There are two dumb exception blocks in the code. What sort of exceptions are raised and how do we avoid them?
- Why is
CroppedBitmap
is used? Why not use the BitmapSource.CopyPixels
method on the source bitmap?
- What if we do not have a full color or true color image but, for example, an image with indexed colors?
Besides, I wanted to add the following features:
- Extend the code to work with drawings.
- Dress it into a WPF element.
Bitmap pixel color picking
Let's get the Listing 1 code line-by-line to see what it does.
CroppedBitmap cb = new CroppedBitmap(wheel.Source as BitmapSource,
new Int32Rect((int)Mouse.GetPosition(this).X,
(int)Mouse.GetPosition(this).Y, 1, 1));
First, note the "this
" argument of the Mouse.GetPosition(this)
method call. It gets the mouse position relative to Window1
, not to the "wheel" image. The code probably assumes that the image occupies the area at the top-left corner of the Window1
client rectangle. If, for example, the "wheel" has a non-zero top or left margins, this method will return the wrong mouse position for the "wheel", so we should use Mouse.GetPosition(wheel)
instead. Let's fix it, and rewrite the line of code above in a more verbose way:
BitmapSource bitmapSource = wheel.Source as BitmapSource;
if (bitmapSource != null)
{
double x = Mouse.GetPosition(wheel).X;
double y = Mouse.GetPosition(wheel).Y;
CroppedBitmap cb = new CroppedBitmap(bitmapSource,
new Int32Rect((int)x, (int)y, 1, 1));
…
}
Testing shows that the ArgumentException
exception with the "Value is outside of the range" message happens when the size of the "wheel" image differs from the size of the source bitmap and the x,y point falls outside of the source bitmap dimensions. The trick is that the Int32Rect
x and y parameters must be expressed in the source bitmap coordinates, not in the WPF image coordinates.
The "wheel" image size may differ from the size of the source bitmap if we set the image Stretch
property to something other than Stretch.None
. We should convert x,y values from WPF pixel coordinates to bitmap pixel coordinates, like follows:
double x = Mouse.GetPosition(wheel).X;
x *= bitmapSource.PixelWidth / wheel.ActualWidth;
double y = Mouse.GetPosition(wheel).Y;
y *= bitmapSource.PixelHeight / wheel.ActualHeight;
CroppedBitmap cb = new CroppedBitmap(bitmapSource,
new Int32Rect((int)x, (int)y, 1, 1));
Is it OK now? Not completely: we still have the exception if the x,y point is the most right or the most bottom bitmap pixels because the Int32Rect
rectangle is still outside of the bitmap bounds in these cases. So, we have to restrict the x, y values to the bitmap bounds:
double x = Mouse.GetPosition(wheel).X;
x *= bitmapSource.PixelWidth / wheel.ActualWidth;
if ((int)x > bitmapSource.PixelWidth - 1)
x = bitmapSource.PixelWidth - 1;
else if (x < 0)
x = 0;
double y = Mouse.GetPosition(wheel).Y;
y *= bitmapSource.PixelHeight / wheel.ActualHeight;
if ((int)y > bitmapSource.PixelHeight - 1)
y = bitmapSource.PixelHeight - 1;
else if (y < 0)
y = 0;
CroppedBitmap cb = new CroppedBitmap(bitmapSource,
new Int32Rect((int)x, (int)y, 1, 1));
That's all on the exceptions, and we could remove the try/catch
blocks from the code now. Let's go on.
The second question is why are we using:
byte[] pixels = new byte[4];
CroppedBitmap cb = new CroppedBitmap(…);
cb.CopyPixels(pixels, 4, 0);
instead of a more direct code like:
byte[] pixels = new byte[4];
int stride = (bitmapSource.PixelWidth * bitmapSource.Format.BitsPerPixel + 7) / 8;
bitmapSource.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1), pixels, stride, 0);
to get the pixel value? The only reason I can imagine is the use of the fixed stride value of 4, but this value isn't correct in all cases. For example, it's OK for 24 or 32 bits per pixel bitmaps, but is wrong for indexed colors. To me, it's rather a way to get into a trap, so we won't use the CroppedBitmap
any more.
The third question is how to work with different bitmap formats? The bitmap pixel format defines the way we should use to retrieve a pixel value and interpret the value into a Color
. Instead of discussing that problem in words, I'll provide the bitmap pixel color picking code.
Color PickColor(double x, double y)
{
…
BitmapSource bitmapSource = Source as BitmapSource;
if (bitmapSource != null)
{
x *= bitmapSource.PixelWidth / ActualWidth;
if ((int)x > bitmapSource.PixelWidth - 1)
x = bitmapSource.PixelWidth - 1;
else if (x < 0)
x = 0;
y *= bitmapSource.PixelHeight / ActualHeight;
if ((int)y > bitmapSource.PixelHeight - 1)
y = bitmapSource.PixelHeight - 1;
else if (y < 0)
y = 0;
if (bitmapSource.Format == PixelFormats.Indexed4)
{
byte[] pixels = new byte[1];
int stride = (bitmapSource.PixelWidth *
bitmapSource.Format.BitsPerPixel + 3) / 4;
bitmapSource.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1),
pixels, stride, 0);
Debug.Assert(bitmapSource.Palette != null,
"bitmapSource.Palette != null");
Debug.Assert(bitmapSource.Palette.Colors.Count == 16,
"bitmapSource.Palette.Colors.Count == 16");
return bitmapSource.Palette.Colors[pixels[0] >> 4];
}
else if (bitmapSource.Format == PixelFormats.Indexed8)
{
byte[] pixels = new byte[1];
int stride = (bitmapSource.PixelWidth *
bitmapSource.Format.BitsPerPixel + 7) / 8;
bitmapSource.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1),
pixels, stride, 0);
Debug.Assert(bitmapSource.Palette != null,
"bitmapSource.Palette != null");
Debug.Assert(bitmapSource.Palette.Colors.Count == 256,
"bitmapSource.Palette.Colors.Count == 256");
return bitmapSource.Palette.Colors[pixels[0]];
}
else
{
byte[] pixels = new byte[4];
int stride = (bitmapSource.PixelWidth *
bitmapSource.Format.BitsPerPixel + 7) / 8;
bitmapSource.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1),
pixels, stride, 0);
return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]);
}
}
…
}
The PickColor
method belongs to the class derived from Image
, so properties like ActualWidth
are used without qualification, and refer to the parent Image
properties.
The code above provides specific processing for the Indexed4
and Indexed8
pixel formats and generic processing for all other formats. The latter is tested, and works for 24 and 32 color bitmaps.
This code isn't the complete solution: there are pixel formats which probably will be processed in a wrong way with the generic branch, so we'll be forced to add more format specific processing branches. Note. The potentially long if/else
selector on the pixel formats doesn’t seem to be nice. The alternative approach is to convert the original bitmap to another format, e.g., Pbgra32
or the like, with the FormatConvertedBitmap
or ColorConvertedBitmap
classes, cache this bitmap internally, and use it for the color picking. I leave that for the future.
Drawing pixel color picking
To get a pixel from the drawing, we should convert the drawing into a bitmap and then apply the approach described above to this bitmap. The TargetBitmap
property getter converts the drawing into the bitmap.
RenderTargetBitmap cachedTargetBitmap;
RenderTargetBitmap TargetBitmap
{
get
{
if (cachedTargetBitmap == null)
{
DrawingImage drawingImage = Source as DrawingImage;
if (drawingImage != null)
{
DrawingVisual drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawDrawing(drawingImage.Drawing);
}
Rect dvRect = drawingVisual.ContentBounds;
drawingVisual.Transform = new ScaleTransform(
ActualWidth / dvRect.Width
, ActualHeight / dvRect.Height);
cachedTargetBitmap = new RenderTargetBitmap((int)ActualWidth
, (int)ActualHeight, 96, 96, PixelFormats.Pbgra32);
cachedTargetBitmap.Render(drawingVisual);
}
}
return cachedTargetBitmap;
}
}
TargetBitmap
has the same size as the image, and has the Pbgra32
pixel format, so retrieving a pixel color from it is easier than more generic code from the previous section.
Color PickColor(double x, double y)
{
…
DrawingImage drawingImage = Source as DrawingImage;
if (drawingImage != null)
{
RenderTargetBitmap targetBitmap = TargetBitmap;
Debug.Assert(targetBitmap != null, "targetBitmap != null");
x *= targetBitmap.PixelWidth / ActualWidth;
if ((int)x > targetBitmap.PixelWidth - 1)
x = targetBitmap.PixelWidth - 1;
else if (x < 0)
x = 0;
y *= targetBitmap.PixelHeight / ActualHeight;
if ((int)y > targetBitmap.PixelHeight - 1)
y = targetBitmap.PixelHeight - 1;
else if (y < 0)
y = 0;
byte[] pixels = new byte[4];
int stride = (targetBitmap.PixelWidth *
targetBitmap.Format.BitsPerPixel + 7) / 8;
targetBitmap.CopyPixels(new Int32Rect((int)x, (int)y, 1, 1),
pixels, stride, 0);
return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]);
}
…
}
ImageColorPicker element
All the tricks above are encapsulated into the ImageColorPicker
class derived from the WPF Image
class and, as such, is the descendant of the FrameworkElement
class, not the Control
class. ImageColorPicker
defines two Dependency Properties:
SelectedColor
is read-only, and returns the currently selected color (transparent is the default).
Selector
gets or sets the Drawing
which is used as a marker of the currently selected pixel on the image surface. By default, that drawing is the circle with the 10 pixel diameter. The ImageColorPicker
doesn't restrict this drawing (except that it can't be set to null) and its size. It's the user's duty to choose it reasonably.
ImageColorPicker
overrides the OnRender
method to draw the Selector
drawing on top of the image. It overrides the OnRenderSizeChanged
and the OnPropertyChanged
methods to update the selector position and clear the cached bitmap (if any) when a new image is loaded. It also overrides the OnMouseXXX
methods to synchronize the selector position to the mouse pointer.
In other respects, ImageColorPicker
behaves like its parent Image
element.
Using the code
The code is the Visual Studio 2008 SP1 solution targeted at .NET Framework 3.5. It contains the ColorPickerControls class library project with just one class: ImageColorPicker
, and two sample applications.
The first sample, ImageColorPickerSample, shows two predefined images (see screenshot at the beginning of the article). The image on the left is the bitmap from the PNG file I borrowed from the code of WPF: A Simple Color Picker With Preview by Sacha Barber. The image on the right is the playing card face drawing I got from the "Windows Presentation Foundation Unleashed" book by Adam Nathan.
The second sample, ImageColorPickerSample2, is designed to load images from files.
The image on the left is the well known Forest.jpg Windows Vista wallpaper. The image on the right is the 16-color bitmap from the Visual Studio 2005 images collection.
I tested the ImageColorPicker
with different BMP, PNG, JPG, and GIF files. It would be nice if you run ImageColorPickerSample2 with these and other image types and notify me if something goes wrong.
History
- May, 31 2009 - First article post.