What Color is My Pixel? Image based color picker on iPhone

Color PickerMy Postcards app has a design screen where you can change colors of things, so I needed a color picker. A very easy way to make a color picker on just about any platform is to make some kind of color pallet image file, display it for the user to click on, and do a call like: getPixelColorAtScreenLocation(x,y)

This was available back in 1983 on my ZX-Spectrum, but alas Cocoa Touch doesn’t have it. (If I missed something I hope a reader will set me straight.) What are some alternatives then?

  • Ditch the color pallet and use sliders to mix colors. Not very user friendly for the everyday user.
  • Manually build a lookup table for position to color. Hard work and what if I want to use a different image?
  • Parse the image file myself and find the color at x,y using some third party library. Maybe…
  • Bend core graphics to my will and have it ‘parse’ the image file for me and make the uncompressed pixels available.

Googling around turned up a way to make the last technique work, which is good because we’re using existing APIs, and it will work for any image:

  1. Make an off screen bitmap image context that is uncompressed and has a known byte to pixel mapping.
  2. Re-draw the color picker image into that context.
  3. Turn x,y touch co-ordinates into a position in the context’s bitmap buffer.
  4. Turn the pixel bytes at the offset position into a UIColor.

I’ve wrapped this up in ColorPickerImageView, a subclass of UIImageView.  When you tap it, it uses this approach to get the pixel color you tapped.  You can also register a simple delegate with it to get a callback with the picked color. You put your own image in it, so you can make it a little color pallet in a corner or make it fill the screen. As it’s derived from UIImageView, you use it just like UIImageView in code or in Interface Builder. The  pixel grabbing source code is below, and the whole class is included in: ColorPicker code to download and copy.

- (UIColor*) getPixelColorAtLocation:(CGPoint)point {
	UIColor* color = nil;
	CGImageRef inImage = self.image.CGImage;
	// Create off screen bitmap context to draw the image into. Format ARGB is 4 bytes for each pixel: Alpa, Red, Green, Blue
	CGContextRef cgctx = [self createARGBBitmapContextFromImage:inImage];
	if (cgctx == NULL) { return nil; /* error */ }

    size_t w = CGImageGetWidth(inImage);
	size_t h = CGImageGetHeight(inImage);
	CGRect rect = {{0,0},{w,h}}; 

	// Draw the image to the bitmap context. Once we draw, the memory
	// allocated for the context for rendering will then contain the
	// raw image data in the specified color space.
	CGContextDrawImage(cgctx, rect, inImage); 

	// Now we can get a pointer to the image data associated with the bitmap
	// context.
	unsigned char* data = CGBitmapContextGetData (cgctx);
	if (data != NULL) {
		//offset locates the pixel in the data from x,y.
		//4 for 4 bytes of data per pixel, w is width of one row of data.
		int offset = 4*((w*round(point.y))+round(point.x));
		int alpha =  data[offset];
		int red = data[offset+1];
		int green = data[offset+2];
		int blue = data[offset+3];
		NSLog(@"offset: %i colors: RGB A %i %i %i  %i",offset,red,green,blue,alpha);
		color = [UIColor colorWithRed:(red/255.0f) green:(green/255.0f) blue:(blue/255.0f) alpha:(alpha/255.0f)];
	}

	// When finished, release the context
	CGContextRelease(cgctx);
	// Free image data memory for the context
	if (data) { free(data); }

	return color;
}

- (CGContextRef) createARGBBitmapContextFromImage:(CGImageRef) inImage {

	CGContextRef    context = NULL;
	CGColorSpaceRef colorSpace;
	void *          bitmapData;
	int             bitmapByteCount;
	int             bitmapBytesPerRow;

	// Get image width, height. We'll use the entire image.
	size_t pixelsWide = CGImageGetWidth(inImage);
	size_t pixelsHigh = CGImageGetHeight(inImage);

	// Declare the number of bytes per row. Each pixel in the bitmap in this
	// example is represented by 4 bytes; 8 bits each of red, green, blue, and
	// alpha.
	bitmapBytesPerRow   = (pixelsWide * 4);
	bitmapByteCount     = (bitmapBytesPerRow * pixelsHigh);

	// Use the generic RGB color space.
	colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
	if (colorSpace == NULL)
	{
		fprintf(stderr, "Error allocating color space\n");
		return NULL;
	}

	// Allocate memory for image data. This is the destination in memory
	// where any drawing to the bitmap context will be rendered.
	bitmapData = malloc( bitmapByteCount );
	if (bitmapData == NULL)
	{
		fprintf (stderr, "Memory not allocated!");
		CGColorSpaceRelease( colorSpace );
		return NULL;
	}

	// Create the bitmap context. We want pre-multiplied ARGB, 8-bits
	// per component. Regardless of what the source image format is
	// (CMYK, Grayscale, and so on) it will be converted over to the format
	// specified here by CGBitmapContextCreate.
	context = CGBitmapContextCreate (bitmapData,
									 pixelsWide,
									 pixelsHigh,
									 8,      // bits per component
									 bitmapBytesPerRow,
									 colorSpace,
									 kCGImageAlphaPremultipliedFirst);
	if (context == NULL)
	{
		free (bitmapData);
		fprintf (stderr, "Context not created!");
	}

	// Make sure and release colorspace before returning
	CGColorSpaceRelease( colorSpace );

	return context;
}

Comments

  1. Mital Pritmani says:

    hello,

    this was a great tutial. Thanks for code also.

    It worked and was very helpful for me.

  2. Mobin says:

    Really Awesome dude… It really helps me…

  3. Marina says:

    This code needs a minor adjustment to properly work with high resolution images on iPhone 4.
    In function – (UIColor*) getPixelColorAtLocation:(CGPoint)point you need to add the following to lines:

    CGFloat x = 1.0;
    if ([self.image respondsToSelector:@selector(scale)]) x = self.image.scale;

    and then change this line:
    int offset = 4*((w*round(point.y))+round(point.x));
    to this:
    int offset = 4*((w*round(point.y))+round(point.x)*x);

    Then everything works just fine on iPhone 4, and should still work fine if they come up with other resolution devices.

    • markj says:

      Hey Marina – thanks for posting this update :-), and nice job remembering to include the check for the new .scale property.

    • Andrea says:

      Hi Marina!
      There’s a little error in your code:
      this line is wrong
      int offset = 4*((w*round(point.y))+round(point.x)*x);
      beacuse it should be like this:
      int offset = 4*((w*round(point.y))+round(point.x))*x;

      thank you for this update!!

  4. thoag says:

    If the static analyzer results are causing you wonder, anxiety or grief, they can be eliminated by renaming

    - (CGContextRef) createARGBBitmapContextFromImage:(CGImageRef) inImage

    to

    - (CGContextRef) newARGBBitmapContextFromImage:(CGImageRef) inImage

    The crux of the warning is that method naming rules say that when a method is returning an object (in this case a CGContextRef) the method name should begin with the word ‘new’ or ‘copy’. Accordingly, Clang sees the createARGBBitmapContextFromImage, recognizes that it is returning an object that was created but not released and flags it with a warning.

    Nonetheless, as the getPixelColorAtLocation:(CGPoint)point method *does* release the context created, there shouldn’t be any leaking.

    In lieu of renaming the method, you can just accept it as is and ignore the Clang warning.

    Great piece of code. Thanks for sharing!!

  5. kiran says:

    Hi
    Nice application.
    I am using your application trying it with different images.

    when i clicked on image it generates a different pixel value.

    what to do?

  6. Ben says:

    This is awesome. A thousand times thank you!

  7. Liam says:

    Great article Mark, I cant wait to try this out

  8. Smitesh says:

    Thnx a million saved my day man!!! also for sharing code :)

  9. aya says:

    Thank you so much!
    let me used it by my application.

  10. Mani says:

    very useful artcile.. Thanks..

  11. Niraj says:

    Gracefully done! Much appreciated!

    Thanks,
    Niraj

  12. Daniel Brooks says:

    I’m trying to use this with a picture i’m taking with the iphone’s camera and for some reason I always get the wrong color (not even close). It seems like the color is coming from a different position on the screen. Any ideas?

  13. Jimmy says:

    Great post!!!

    But I want to use it on cocos2d. I’ve tried but the function always return (0,0,0,0) value.

    Can i use it on cocos2d? Please help me.

  14. bhoomi kathiriya says:

    Hello
    I have tried to use it with different types of images.but i get wrong colors many times.It seems that colors are coming from other positions and not the position tapped

  15. siva says:

    Hi Thank you so much…nice application..

  16. Rahul Anand says:

    Hello Sir,

    Please Help Me….I am facing some problem in my app that i want to change the color of a pixel in a image from black to red. Can you please guide me that how can i do it thru code.

    Thank & Regards,
    Rahul

  17. Serge says:

    I got a “kCGColorSpaceGenericRGB’ is unavailable” error. Worked with CGColorSpaceCreateDeviceRGB() though. Thanks a lot, really what I was looking for and nice of an example.

  18. fgerikeke says:

    Hy!

    On ios5 drops this failure : ‘kCGColorSpaceGenericRGB’ is unavailable .

    Whats wrong? please help me!

    Thank you!

  19. Joe says:

    Any way to make this work with the non-default contentMode. If I set it to UIViewContentModeScaleAspectFit it breaks.

  20. Fatemeh says:

    So thanks, its very useful sample code.

Trackbacks

Speak Your Mind

*