18

How can I extract the alpha channel of a UIImage or CGImageRef and convert it into a mask that I can use with CGImageMaskCreate?

For example:

example image

Essentially, given any image, I don't care about the colors inside the image. All I want is to create a grayscale image that represents the alpha channel. This image can then be used to mask other images.

An example behavior of this is in the UIBarButtonItem when you supply it an icon image. According to the Apple docs it states:

The images displayed on the bar are derived from this image. If this image is too large to fit on the bar, it is scaled to fit. Typically, the size of a toolbar and navigation bar image is 20 x 20 points. The alpha values in the source image are used to create the images—opaque values are ignored.

The UIBarButtonItem takes any image and looks only at the alpha, not the colors of the image.

3 Answers 3

12

To color icons the way the bar button items do, you don't want the traditional mask, you want the inverse of a mask-- one where the opaque pixels in the original image take on your final coloring, rather than the other way around.

Here's one way to accomplish this. Take your original RBGA image, and process it by:

  • Drawing it into an alpha-only one-channel bitmap image
  • Invert the alpha values of each pixel to get the opposite behavior as noted above
  • Turn this inverted-alpha image into an actual mask
  • Use it.

E.g.

#define ROUND_UP(N, S) ((((N) + (S) - 1) / (S)) * (S))

// Original RGBA image
CGImageRef originalMaskImage = [[UIImage imageNamed:@"masktest.png"] CGImage];
float width = CGImageGetWidth(originalMaskImage);
float height = CGImageGetHeight(originalMaskImage);

// Make a bitmap context that's only 1 alpha channel
// WARNING: the bytes per row probably needs to be a multiple of 4 
int strideLength = ROUND_UP(width * 1, 4);
unsigned char * alphaData = calloc(strideLength * height, sizeof(unsigned char));
CGContextRef alphaOnlyContext = CGBitmapContextCreate(alphaData,
                                                      width, 
                                                      height,
                                                      8, 
                                                      strideLength, 
                                                      NULL, 
                                                      kCGImageAlphaOnly);

// Draw the RGBA image into the alpha-only context.
CGContextDrawImage(alphaOnlyContext, CGRectMake(0, 0, width, height), originalMaskImage);

// Walk the pixels and invert the alpha value. This lets you colorize the opaque shapes in the original image.
// If you want to do a traditional mask (where the opaque values block) just get rid of these loops.
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        unsigned char val = alphaData[y*strideLength + x];
        val = 255 - val;
        alphaData[y*strideLength + x] = val;
    }
}

CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
CGContextRelease(alphaOnlyContext);
free(alphaData);

// Make a mask
CGImageRef finalMaskImage = CGImageMaskCreate(CGImageGetWidth(alphaMaskImage),
                                              CGImageGetHeight(alphaMaskImage),
                                              CGImageGetBitsPerComponent(alphaMaskImage),
                                              CGImageGetBitsPerPixel(alphaMaskImage),
                                              CGImageGetBytesPerRow(alphaMaskImage),
                                              CGImageGetDataProvider(alphaMaskImage), NULL, false);
CGImageRelease(alphaMaskImage);

Now you can use finalMaskImage as the mask in CGContextClipToMask etc, or etc.

3
  • Just following up, make sure to clear the context when drawing or you get really weird ghosting effects: Nov 15, 2011 at 2:11
  • Hi, I stumbled on the same question as @Nate Weiner, and I've tried to use your code, but it doesn't work. First of all, compiler complains about the implicit conversation between kCGImageInfo and kCGBitmapInfo. Mixing enum types is bad practice. And then CGBitmapContextCreate returns 0x0. I suppose it is caused by passing NULL to the colorspace parameter. Please, can you explain how make to work the code from your answer?
    – kelin
    Nov 5, 2014 at 13:17
  • From docs "Note that the only legal case when `space' can be NULL is when alpha is specified as kCGImageAlphaOnly."
    – Marek H
    Dec 13, 2021 at 15:10
5

The solution by Ben Zotto is correct, but there is a way to do this with no math or local complexity by relying on CGImage to do the work for us.

The following solution uses Swift (v3) to create a mask from an image by inverting the alpha channel of an existing image. Transparent pixels in the source image will become opaque, and partially transparent pixels will be inverted to be proportionally more or less transparent.

The only requirement for this solution is a CGImage base image. One can be obtained from UIImage.cgImage for a most UIImages. If you're rendering the base image yourself in a CGContext, use CGContext.makeImage() to generate a new CGImage.

The code

let image: CGImage = // your image

// Create a "Decode Array" which flips the alpha channel in
// an image in ARGB format (premultiplied first). Adjust the
// decode array as needed based on the pixel format of your
// image data.
// The tuples in the decode array specify how to clamp the
// pixel color channel values when the image data is decoded.
//
// Tuple(0,1) means the value should be clamped to the range
// 0 and 1. For example, a red value of 0.5888 (~150 out of
// 255) would not be changed at all because 0 < 0.5888 < 1.
// Tuple(1,0) flips the value, so the red value of 0.5888
// would become 1-0.5888=0.4112. We use this method to flip
// the alpha channel values.

let decode = [ CGFloat(1), CGFloat(0),  // alpha (flipped)
               CGFloat(0), CGFloat(1),  // red   (no change)
               CGFloat(0), CGFloat(1),  // green (no change)
               CGFloat(0), CGFloat(1) ] // blue  (no change)

// Create the mask `CGImage` by reusing the existing image data
// but applying a custom decode array.
let mask =  CGImage(width:              image.width,
                    height:             image.height,
                    bitsPerComponent:   image.bitsPerComponent,
                    bitsPerPixel:       image.bitsPerPixel,
                    bytesPerRow:        image.bytesPerRow,
                    space:              image.colorSpace!,
                    bitmapInfo:         image.bitmapInfo,
                    provider:           image.dataProvider!,
                    decode:             decode,
                    shouldInterpolate:  image.shouldInterpolate,
                    intent:             image.renderingIntent)

That's it! The mask CGImage is now ready to used with context.clip(to: rect, mask: mask!).

Demo

Here is my base image with "Mask Image" in opaque red on a transparent background: image that will become an image mask with red text on a transparent background

To demonstrate what happens when running it through the above algorithm, here is an example which simply renders the resulting image over a green background.

override func draw(_ rect: CGRect) {
    // Create decode array, flipping alpha channel
    let decode = [ CGFloat(1), CGFloat(0),
                   CGFloat(0), CGFloat(1),
                   CGFloat(0), CGFloat(1),
                   CGFloat(0), CGFloat(1) ]

    // Create the mask `CGImage` by reusing the existing image data
    // but applying a custom decode array.
    let mask =  CGImage(width:              image.width,
                        height:             image.height,
                        bitsPerComponent:   image.bitsPerComponent,
                        bitsPerPixel:       image.bitsPerPixel,
                        bytesPerRow:        image.bytesPerRow,
                        space:              image.colorSpace!,
                        bitmapInfo:         image.bitmapInfo,
                        provider:           image.dataProvider!,
                        decode:             decode,
                        shouldInterpolate:  image.shouldInterpolate,
                        intent:             image.renderingIntent)

    let context = UIGraphicsGetCurrentContext()!

    // paint solid green background to highlight the transparent areas
    context.setFillColor(UIColor.green.cgColor)
    context.fill(rect)

    // render the mask image directly. The black areas will be masked.
    context.draw(mask!, in: rect)
}

mask image rendered directly, showing the background through the clipping area

Now we can use that image to mask any rendered content. Here's an example where we render a masked gradient on top of the green from the previous example.

override func draw(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()!

    // paint solid green background to highlight the transparent areas
    context.setFillColor(UIColor.green.cgColor)
    context.fill(rect)

    let mask: CGImage = // mask generation elided. See previous example.

    // Clip to the mask image
    context.clip(to: rect, mask: mask!)

    // Create a simple linear gradient
    let colors = [ UIColor.red.cgColor, UIColor.blue.cgColor, UIColor.orange.cgColor ]
    let gradient = CGGradient(colorsSpace: context.colorSpace, colors: colors as CFArray, locations: nil)

    // Draw the linear gradient around the clipping area
    context.drawLinearGradient(gradient!,
                               start: CGPoint.zero,
                               end: CGPoint(x: rect.size.width, y: rect.size.height),
                               options: CGGradientDrawingOptions())
}

final image showing a gradient with the original image text masked out to green

(Note: You could also also swap the CGImage code to use Accelerate Framework's vImage, possibly benefiting from the vector processing optimizations in that library. I haven't tried it.)

2
  • CoreGraphics (CGImage) calls vImage for a bunch of stuff. I would run instruments on the code and check to see if vImage is already doing the work. If it isn't and the CG code doesn't look too amazing, file a Radar. It is better if CG calls vImage because then everyone benefits. Oct 14, 2016 at 1:31
  • Can you post your answer, not in Swift, but in Objective-C? Sep 27, 2023 at 13:49
1

I tried the code provided by quixoto but it didn't work for me so I changed it a little bit.

The problem was that drawing only the alpha channel wasn't working for me, so I did that manually by first obtaining the data of the original image and working on the alpha channel.

#define ROUND_UP(N, S) ((((N) + (S) - 1) / (S)) * (S))
#import <stdlib.h>

- (CGImageRef) createMaskWithImageAlpha: (CGContextRef) originalImageContext {

    UInt8 *data = (UInt8 *)CGBitmapContextGetData(originalImageContext);

    float width = CGBitmapContextGetBytesPerRow(originalImageContext) / 4;
    float height = CGBitmapContextGetHeight(originalImageContext);

    // Make a bitmap context that's only 1 alpha channel
    // WARNING: the bytes per row probably needs to be a multiple of 4 
    int strideLength = ROUND_UP(width * 1, 4);
    unsigned char * alphaData = (unsigned char * )calloc(strideLength * height, 1);
    CGContextRef alphaOnlyContext = CGBitmapContextCreate(alphaData,
                                                          width, 
                                                          height,
                                                          8, 
                                                          strideLength, 
                                                      NULL, 
                                                          kCGImageAlphaOnly);

    // Draw the RGBA image into the alpha-only context.
    //CGContextDrawImage(alphaOnlyContext, CGRectMake(0, 0, width, height), originalMaskImage);

    // Walk the pixels and invert the alpha value. This lets you colorize the opaque shapes in the original image.
    // If you want to do a traditional mask (where the opaque values block) just get rid of these loops.


    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            //unsigned char val = alphaData[y*strideLength + x];
            unsigned char val = data[y*(int)width*4 + x*4 + 3];
            val = 255 - val;
            alphaData[y*strideLength + x] = val;
        }
    }


    CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
    CGContextRelease(alphaOnlyContext);
    free(alphaData);

    // Make a mask
    CGImageRef finalMaskImage = CGImageMaskCreate(CGImageGetWidth(alphaMaskImage),
                                                  CGImageGetHeight(alphaMaskImage),
                                                  CGImageGetBitsPerComponent(alphaMaskImage),
                                                  CGImageGetBitsPerPixel(alphaMaskImage),
                                                  CGImageGetBytesPerRow(alphaMaskImage),
                                                  CGImageGetDataProvider(alphaMaskImage),     NULL, false);
    CGImageRelease(alphaMaskImage);

    return finalMaskImage;
}

You can call that function like this

    CGImageRef originalImage = [image CGImage];
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef bitmapContext = CGBitmapContextCreate(NULL,
                                                   CGImageGetWidth(originalImage),
                                                   CGImageGetHeight(originalImage),
                                                   8,
                                                   CGImageGetWidth(originalImage)*4,
                                                   colorSpace,
                                                   kCGImageAlphaPremultipliedLast);

    CGContextDrawImage(bitmapContext, CGRectMake(0, 0, CGBitmapContextGetWidth(bitmapContext), CGBitmapContextGetHeight(bitmapContext)), originalImage);    
    CGImageRef finalMaskImage = [self createMaskWithImageAlpha:bitmapContext];
    //YOUR CODE HERE
    CGContextRelease(bitmapContext);
    CGImageRelease(finalMaskImage);
3
  • Slight mistake there man. When walking the alpha data of the image, you've updated the data address you read each pixel from to the correct value, but you're still writing to the incorrect value, leading to a totally jumbled output. I also suggest storing that value in a temporary local attribute to a) prevent this kind of mistake happening, and b) save having to perform the calculation twice.
    – Ash
    Jan 19, 2014 at 14:01
  • float width = CGBitmapContextGetWidth(originalImageContext); int strideLength = (int) CGBitmapContextGetBytesPerRow(originalImageContext);
    – geowar
    Apr 26, 2014 at 18:27
  • int strideLength = ROUND_UP(width * 1, 4); // was right
    – geowar
    Apr 26, 2014 at 18:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.