Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / WPF

WPF Image Pixel Color Picker Element

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
30 May 2009CPOL6 min read 84.2K   3.8K   28   4
WPF element to pick up an image (either bitmap or drawing) pixel color.

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:

C#
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:

  1. There are two dumb exception blocks in the code. What sort of exceptions are raised and how do we avoid them?
  2. Why is CroppedBitmap is used? Why not use the BitmapSource.CopyPixels method on the source bitmap?
  3. 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.

C#
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:

C#
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:

C#
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:

C#
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:

C#
byte[] pixels = new byte[4];
CroppedBitmap cb = new CroppedBitmap(…);
cb.CopyPixels(pixels, 4, 0);

instead of a more direct code like:

C#
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.

C#
/// <summary>
/// Picks the color at the position specified.
/// </summary>
/// <param name="x">The x coordinate in WPF pixels.</param>
/// <param name="y">The y coordinate in WPF pixels.</param>
/// <returns>The image pixel color at x,y position.</returns>
Color PickColor(double x, double y)
{
    …
    BitmapSource bitmapSource = Source as BitmapSource;
    if (bitmapSource != null)
    { // Get color from bitmap pixel.
        // Convert coopdinates from WPF pixels to Bitmap pixels
        // and restrict them by the Bitmap bounds.
        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;

        // Lee Brimelow approach (http://thewpfblog.com/?p=62).
        //byte[] pixels = new byte[4];
        //CroppedBitmap cb = new CroppedBitmap(bitmapSource, 
        //                   new Int32Rect((int)x, (int)y, 1, 1));
        //cb.CopyPixels(pixels, 4, 0);
        //return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]);

        // Alternative approach
        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]);
        }
        // TODO There are other PixelFormats which processing should be added if desired.
    }

    …
}

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.

C#
RenderTargetBitmap cachedTargetBitmap;
/// <summary>
/// Gets the target bitmap for the DrawingImage image Source.
/// </summary>
/// <value>The target bitmap.</value>
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);
                }

                // Scale the DrawingVisual.
                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.

C#
/// <summary>
/// Picks the color at the position specified.
/// </summary>
/// <param name="x">The x coordinate in WPF pixels.</param>
/// <param name="y">The y coordinate in WPF pixels.</param>
/// <returns>The image pixel color at x,y position.</returns>
Color PickColor(double x, double y)
{
    …

    DrawingImage drawingImage = Source as DrawingImage;
    if (drawingImage != null)
    { // Get color from drawing pixel.
        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;

        // TargetBitmap is always in PixelFormats.Pbgra32 format.
        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.

JPEG image16-color bitmap image

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionsetting picker position Pin
Muni Kiran Reddy Dwaram2-Jun-22 20:38
Muni Kiran Reddy Dwaram2-Jun-22 20:38 
QuestionUsing in MVVM Pin
Member 1163834318-Dec-15 8:39
Member 1163834318-Dec-15 8:39 
GeneralResize Window Problem Pin
torstenP6-Aug-10 0:52
torstenP6-Aug-10 0:52 
GeneralSorry been away, this is good have a 5 Pin
Sacha Barber16-Oct-09 18:32
Sacha Barber16-Oct-09 18:32 
Sorry been away, this is good have a 5

Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.