Как преобразовать альфа-канал UIImage / CGImageRef в маску? - PullRequest
13 голосов
/ 14 ноября 2011

Как я могу извлечь альфа-канал из UIImage или CGImageRef и преобразовать его в маску, которую можно использовать с CGImageMaskCreate?

Например:

example image

По сути, учитывая любое изображение, мне нет дела до цветов внутри изображения. Все, что я хочу, это создать изображение в градациях серого, представляющее альфа-канал. Это изображение затем можно использовать для маскировки других изображений.

Пример поведения этого в UIBarButtonItem, когда вы предоставляете ему изображение значка. Согласно документам Apple, говорится:

Изображения, отображаемые на панели, получены из этого изображения. Если это изображение слишком велико, чтобы поместиться на панели, оно масштабируется, чтобы соответствовать. Как правило, размер панели инструментов и изображения панели навигации составляет 20 х 20 точек. Альфа-значения в исходном изображении используются для создания изображений - непрозрачные значения игнорируются.

UIBarButtonItem берет любое изображение и смотрит только на альфа, а не на цвета изображения.

Ответы [ 3 ]

11 голосов
/ 15 ноября 2011

Чтобы раскрасить значки так, как это делают элементы панели кнопок, вам не нужна традиционная маска, вам нужна инверсия маски - та, в которой непрозрачные пиксели в исходном изображении приобретают окончательную окраску, а ненаоборот

Вот один из способов сделать это.Возьмите исходное изображение RBGA и обработайте его:

  • Рисование его в одноканальное растровое изображение только для альфа-канала
  • Инвертируйте альфа-значения каждого пикселя, чтобы получить противоположное поведениекак отмечено выше
  • Превратите это перевернутое альфа-изображение в настоящую маску
  • Используйте его.

Например

#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 и т. д. или т. п.

3 голосов
/ 10 октября 2016

Решение Бена Зотто верное, но есть способ сделать это без математических или локальных сложностей, полагаясь на CGImage, чтобы сделать работу за нас.

Следующее решение использует Swift (v3) для создания маски из изображения путем инвертирования альфа-канала существующего изображения. Прозрачные пиксели в исходном изображении станут непрозрачными, а частично прозрачные пиксели будут инвертированы, чтобы быть пропорционально более или менее прозрачными.

Единственное требование для этого решения - базовое изображение CGImage. Можно получить из UIImage.cgImage в течение большинства UIImage с. Если вы визуализируете базовое изображение в 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!).

Демо

Вот мое базовое изображение с "Mask Image" в непрозрачном красном на прозрачном фоне: image that will become an image mask with red text on a transparent 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

Теперь мы можем использовать это изображение для маскировки любого отображаемого содержимого. Вот пример, в котором мы визуализируем замаскированный градиент поверх зеленого цвета из предыдущего примера.

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

(Примечание. Вы также можете поменять код CGImage на использование vImage в Accelerate Framework, возможно, выиграв от оптимизации векторной обработки в этой библиотеке. Я не пробовал.)

1 голос
/ 29 августа 2012

Я попробовал код, предоставленный quixoto , но он мне не помог, поэтому я немного его изменил.

Проблема заключалась в том, что рисование только альфа-канала не работало для меня, поэтому я сделал это вручную, сначала получив данные исходного изображения и работая над альфа-каналом.

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