Retrieving a pixel alpha value for a UIImage
Asked Answered
W

4

37

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);
Windshield answered 25/6, 2009 at 9:18 Comment(4)
Hey teabot - I realize this question is already answered, but I was doing something similar to this a few days ago. Instead of drawing the entire image and then indexing into the byte array, you should only draw 1px of the source image into a 1px by 1px bitmap context. You can use the rect parameter in CGContextDrawImage to put the right pixel in, and it's like 1000 times faster :-)Pelagianism
Thanks for the comment @Ben Gotow - I only draw the image once and then keep using the same byte array. @Nikolai Ruhe also suggested the single pixel draw but said that my array approach would be faster if I didn't need to draw the image more than once, but needed to lookup the alpha repeatedly.Windshield
Just a quick update (years later) after using this and another question on here for a similar problem: The rect parameter on CGContextDrawImage is used to control "The location and dimensions in user space of the bounding box in which to draw the image." So, if you make that rect 1x1 in size, it will scale the whole image down to 1x1 before drawing it. In order to get the right pixel you need to utilize CGContextTranslateCTM as in Nikolai's answer, and leave the rect's size equal to the source image to prevent scaling.Laceylach
Hi, I'm trying to use the code you have posted but not able to get the data type of "rawData", "bytePerPixel" variables. Can you please tell me the answer?Vasili
C
10

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);
Carmarthenshire answered 25/6, 2009 at 9:57 Comment(3)
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?Windshield
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.Carmarthenshire
I'm trying this out as well (and I am doing lots of lookups - one for each tap). It works beautifully on the Simulator but not on an iPhone 4. The coordinates look good (lower-left is 0,0) but the hit testing is a jumbled mess. See #7506748Ferdie
L
37

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.

EDIT December 8 2011:

A commenter points out that under certain circumstances the image may be flipped. I've been thinking about this, and I'm a little sorry that I didn't write the code using the UIImage directly, like this (I think the reason is that at the time I didn't understand about UIGraphicsPushContext):

// assume im is a UIImage, point is the CGPoint to test
unsigned char pixel[1] = {0};
CGContextRef context = CGBitmapContextCreate(pixel, 
                                             1, 1, 8, 1, NULL,
                                             kCGImageAlphaOnly);
UIGraphicsPushContext(context);
[im drawAtPoint:CGPointMake(-point.x, -point.y)];
UIGraphicsPopContext();
CGContextRelease(context);
CGFloat alpha = pixel[0]/255.0;
BOOL transparent = alpha < 0.01;

I think that would have solved the flipping issue.

Lal answered 21/9, 2010 at 18:31 Comment(5)
For some reason I had to use the following line for the CGContextDrawImage(...: CGContextDrawImage(context, CGRectMake(-point.x, -(image.size.height-point.y), CGImageGetWidth(cgim), CGImageGetHeight(cgim)), cgim);Morvin
@MattLeff - That makes perfect sense if your image is flipped, which can easily happen due to the impedance mismatch between Core Graphics (where y origin is at bottom) and UIKit (where y origin is at top).Lal
Couldn't this also be used to extract all the other values to create a complete UIColor?Thunderpeal
what about Swift solution?Kaitlinkaitlyn
@BartłomiejSemańczyk Do you mean https://mcmap.net/q/130731/-cgcontext-init-null-color-space-no-longer-allowed ?Lal
C
10

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);
Carmarthenshire answered 25/6, 2009 at 9:57 Comment(3)
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?Windshield
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.Carmarthenshire
I'm trying this out as well (and I am doing lots of lookups - one for each tap). It works beautifully on the Simulator but not on an iPhone 4. The coordinates look good (lower-left is 0,0) but the hit testing is a jumbled mess. See #7506748Ferdie
P
3

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 (unpremultiply) by dividing the color components by the alpha.

Puppetry answered 25/6, 2009 at 9:58 Comment(0)
I
3

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!

Ideality answered 12/7, 2009 at 23:46 Comment(3)
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.Goldy
That also means that your rawData that you malloced may be to small to hold the bitmap and there may be a buffer overrun.Goldy
I wonder if this is why I'm having trouble getting this to work on an iPhone, but it works fine on the simulator? #7506748Ferdie

© 2022 - 2024 — McMap. All rights reserved.