【问题标题】:How to convert UIImage/CGImageRef's alpha channel to mask?如何将 UIImage/CGImageRef 的 alpha 通道转换为遮罩?
【发布时间】:2011-11-14 18:27:21
【问题描述】:

如何提取 UIImage 或 CGImageRef 的 alpha 通道并将其转换为可用于 CGImageMaskCreate 的蒙版?

例如:

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

当您向 UIBarButtonItem 提供图标图像时,此行为的一个示例行为。根据 Apple 文档,它指出:

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

UIBarButtonItem 接受任何图像并且只查看 alpha,而不是图像的颜色。

【问题讨论】:

    标签: ios uikit core-graphics uibarbuttonitem


    【解决方案1】:

    要像条形按钮项那样为图标着色,您不需要传统的蒙版,而是需要蒙版的反转——原始图像中的不透明像素采用最终颜色,而不是其他方式。

    这是实现此目的的一种方法。获取您的原始 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 等中的掩码,等等。

    【讨论】:

    • 只是跟进,请确保在绘制时清除上下文,否则您会得到非常奇怪的重影效果:
    • 嗨,我偶然发现了与 @Nate Weiner 相同的问题,我尝试使用您的代码,但它不起作用。首先,编译器抱怨 kCGImageInfo 和 kCGBitmapInfo 之间的隐式对话。混合枚举类型是不好的做法。然后 CGBitmapContextCreate 返回 0x0。我想这是由将 NULL 传递给颜色空间参数引起的。请问,你能解释一下如何使你的答案中的代码工作吗?
    • 来自文档“请注意,`space' 可以为 NULL 的唯一合法情况是 alpha 指定为 kCGImageAlphaOnly。”
    【解决方案2】:

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

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

    此解决方案的唯一要求是CGImage 基本映像。一个可以从UIImage.cgImage 获得最多UIImages。如果您自己在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,这可能受益于该库中的矢量处理优化。我还没有尝试过。)

    【讨论】:

    • CoreGraphics (CGImage) 为一堆东西调用 vImage。我会在代码上运行工具并检查 vImage 是否已经在做这项工作。如果不是,并且 CG 代码看起来不太令人惊讶,请提交 Radar。如果 CG 调用 vImage 会更好,因为这样每个人都会受益。
    【解决方案3】:

    我尝试了 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);
    

    【讨论】:

    • 有点小错误。在遍历图像的 alpha 数据时,您已将读取每个像素的数据地址更新为正确的值,但您仍在写入不正确的值,导致输出完全混乱。我还建议将该值存储在临时本地属性中,以 a) 防止发生此类错误,以及 b) 不必执行两次计算。
    • 浮动宽度 = CGBitmapContextGetWidth(originalImageContext); int strideLength = (int) CGBitmapContextGetBytesPerRow(originalImageContext);
    • int strideLength = ROUND_UP(width * 1, 4); // 是对的
    猜你喜欢
    • 1970-01-01
    • 2010-12-19
    • 1970-01-01
    • 1970-01-01
    • 2014-12-07
    • 1970-01-01
    • 2020-10-18
    • 2011-04-08
    • 1970-01-01
    相关资源
    最近更新 更多