如何将 UIImage/CGImageRef 的 alpha 通道转换为 mask?

发布于 2024-12-15 18:20:09 字数 489 浏览 2 评论 0原文

如何提取 UIImage 或 CGImageRef 的 Alpha 通道并将其转换为可以与 CGImageMaskCreate 一起使用的蒙版?

例如:

example image

本质上,对于给定的任何图像,我不关心图像内部的颜色。我想要的只是创建一个代表 Alpha 通道的灰度图像。然后可以使用该图像来掩盖其他图像。

当您向 UIBarButtonItem 提供图标图像时,此行为的示例如下。根据苹果文档,它指出:

栏上显示的图像源自该图像。如果该图像太大而无法放在条上,则会缩放以适合该图像。通常,工具栏和导航栏图像的大小为 20 x 20 点。源图像中的 Alpha 值用于创建图像 - 不透明值将被忽略。

UIBarButtonItem 接受任何图像并且仅查看 Alpha,而不查看图像的颜色。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

策马西风 2024-12-22 18:20:09

要按照栏按钮项目的方式为图标着色,您不需要传统的蒙版,而是需要蒙版的反面 - 原始图像中的不透明像素呈现最终的颜色,而不是相反。

这是实现此目的的一种方法。获取原始 RBGA 图像,并通过以下方式处理它:

  • 将其绘制为仅包含 alpha 的单通道位图图像
  • 反转每个像素的 alpha 值以获得与上述相反的行为
  • 将此反转的 alpha 图像转换为实际的蒙版
  • 使用它。

例如

#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);

,现在您可以使用 finalMaskImage 作为 CGContextClipToMask 等中的遮罩。

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.

奶气 2024-12-22 18:20:09

Ben Zotto 的解决方案是正确的,但有一种方法可以通过依靠 CGImage 为我们完成这项工作,而无需数学或局部复杂性。

以下解决方案使用 Swift (v3) 通过反转现有图像的 alpha 通道来从图像创建蒙版。源图像中的透明像素将变得不透明,部分透明的像素将反转为按比例或多或少透明。

此解决方案的唯一要求是 CGImage 基础图像。大多数UIImage 都可以从UIImage.cgImage 中获取。如果您自己在 CGContext 中渲染基础图像,请使用 CGContext.makeImage() 生成新的 CGImage

代码

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)

就是这样! mask CGImage 现在可以与 context.clip(to: rect, mask: mask!) 一起使用。

演示

这是我的基本图像,在透明背景上带有不透明红色的“蒙版图像”:
将要显示的图像成为透明背景上带有红色文本的图像蒙版

为了演示通过上述算法运行时会发生什么,这里有一个示例,它只是在绿色背景上渲染结果图像。

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)
}

渲染的蒙版图像直接通过剪切区域显示背景

现在我们可以使用该图像来掩盖任何渲染的内容。这是一个示例,我们在上一个示例的绿色顶部渲染蒙版渐变。

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())
}

最终图像显示原始图像文本遮盖为绿色的渐变

(注意:您还可以交换 CGImage 代码以使用 Accelerate Framework 的 vImage,可能会受益从矢量处理我还没有尝试过该库的优化。)

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.)

汐鸠 2024-12-22 18:20:09

我尝试了 quixoto 提供的代码,但它对我不起作用,所以我对其进行了一些更改。

问题是只绘制 alpha 通道对我来说不起作用,所以我首先获取原始图像的数据并处理 alpha 通道来手动完成此操作。

#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;
}

你可以像这样调用该函数

    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);

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);
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文