views:

2573

answers:

4

I am currently trying to obtain the alpha value of a pixel in a UIImageView. I have obtained the CGImage from [UIImageView image] and created a RGBA byte array from this. Alpha is premultiplied.

CGImageRef image = uiImage.CGImage;
NSUInteger width = CGImageGetWidth(image);
NSUInteger height = CGImageGetHeight(image);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
rawData = malloc(height * width * 4);
bytesPerPixel = 4;
bytesPerRow = bytesPerPixel * width;

NSUInteger bitsPerComponent = 8;
CGContextRef context = CGBitmapContextCreate(
    rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace,
    kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
);
CGColorSpaceRelease(colorSpace);

CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
CGContextRelease(context);

I then calculate the array index for the given alpha channel using the coordinates from the UIImageView.

int byteIndex = (bytesPerRow * uiViewPoint.y) + uiViewPoint.x * bytesPerPixel;
unsigned char alpha = rawData[byteIndex + 3];

However I don't get the values I expect. For a completely black transparent area of the image I get non-zero values for the alpha channel. Do I need to translate the co-ordinates between UIKit and Core Graphics - i.e: is the y-axis inverted? Or have I misunderstood premultiplied alpha values?

Update:

@Nikolai Ruhe's suggestion was key to this. I did not in fact need to translate between UIKit coordinates and Core Graphics coordinates. However, after setting the blend mode my alpha values were what I expected:

CGContextSetBlendMode(context, kCGBlendModeCopy);
+3  A: 

Yes, CGContexts have their y-axis pointing up while in UIKit it points down. See the docs.

Edit after reading code:

You also want to set the blend mode to replace before drawing the image since you want the image's alpha value, not the one which was in the context's buffer before:

CGContextSetBlendMode(context, kCGBlendModeCopy);

Edit after thinking:

You could do the lookup much more efficient by building the smallest possible CGBitmapContext (1x1 pixel? maybe 8x8? have a try) and translating the context to your desired position before drawing:

CGContextTranslateCTM(context, xOffset, yOffset);
Nikolai Ruhe
@Nikolai Ruhe - Good call - I should have read the docs.
teabot
The CGImageRef represents a static image. Once I have the byte array I throw the CGImage away and then repeatedly use the byte array for doing the alpha lookup. Would your optimization work in this scenario?
teabot
No, of course you'll need the image to draw it repeatedly. My approach could be more efficient for few lookups. If you're doing lots of lookups stick with your code.
Nikolai Ruhe
+1  A: 

Do I need to translate the co-ordinates between UIKit and Core Graphics - i.e: is the y-axis inverted?

It's possible. In CGImage, the pixel data is in English reading order: left-to-right, top-to-bottom. So, the first pixel in the array is the top-left; the second pixel is one from the left on the top row; etc.

Assuming you have that right, you should also make sure you're looking at the correct component within a pixel. Perhaps you're expecting RGBA but asking for ARGB, or vice versa. Or, maybe you have the byte order wrong (I don't know what the iPhone's endianness is).

Or have I misunderstood premultiplied alpha values?

It doesn't sound like it.

For those who don't know: Premultiplied means that the color components are premultiplied by the alpha; the alpha component is the same whether the color components are premultiplied by it or not. You can reverse this by dividing the color components by the alpha.

Peter Hosey
@Peter Hosey - Thanks for clarify how premultiplied alpha works - I was uncertain.
teabot
+2  A: 

I found this question/answer while researching how to do collision detection between sprites using the alpha value of the image data, rather than a rectangular bounding box. The context is an iPhone app... I am trying to do the above suggested 1 pixel draw and I am still having problems getting this to work, but I found an easier way of creating a CGContextRef using data from the image itself, and the helper functions here:

CGContextRef context = CGBitmapContextCreate(
     rawData, 
     CGImageGetWidth(cgiRef), 
     CGImageGetHeight(cgiRef), 
     CGImageGetBitsPerComponent(cgiRef), 
     CGImageGetBytesPerRow(cgiRef), 
     CGImageGetColorSpace(cgiRef),
     kCGImageAlphaPremultipliedLast  
 );

This bypasses all the ugly hardcoding in the sample above. The last value can be retrieved by calling CGImageGetBitmapInfo() but in my case, it return a value from the image that caused an error in the ContextCreate function. Only certain combinations are valid as documented here: http://developer.apple.com/qa/qa2001/qa1037.html

Hope this is helpful!

Owyn
This is helpful. Just keep in mind, that here, your BytesPerRow may not be width*bytesPerPixel. For optimization, it may be padded to 16 byte boundaries. As you traverse rawData, if you don't account for this, you'll end up using padding bytes as pixel data.
mahboudz
That also means that your rawData that you malloced may be to small to hold the bitmap and there may be a buffer overrun.
mahboudz
+1  A: 

If all you want is the alpha value of a single point, all you need is an alpha-only single-point buffer. I believe this should suffice:

// assume im is a UIImage, point is the CGPoint to test
CGImageRef cgim = im.CGImage;
unsigned char pixel[1] = {0};
CGContextRef context = CGBitmapContextCreate(pixel, 
                                         1, 1, 8, 1, NULL,
                                         kCGImageAlphaOnly);
CGContextDrawImage(context, CGRectMake(-point.x, 
                                   -point.y, 
                                   CGImageGetWidth(cgim), 
                                   CGImageGetHeight(cgim)), 
               cgim);
CGContextRelease(context);
CGFloat alpha = pixel[0]/255.0;
BOOL transparent = alpha < 0.01;

If the UIImage doesn't have to be recreated every time, this is very efficient.

matt
works like a charm for me!
Chris Devereux