Как преобразовать альфа-канал UIImage / CGImageRef в маску?

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

Например:

пример изображения

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

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

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

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


person Nate Weiner    schedule 14.11.2011    source источник


Ответы (3)


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

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

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

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

Теперь вы можете использовать finalMaskImage в качестве маски в CGContextClipToMask и т. Д. И т. Д.

person Ben Zotto    schedule 14.11.2011
comment
Просто следите за тем, чтобы очистить контекст при рисовании, иначе вы получите действительно странные побочные эффекты: - person Nate Weiner; 15.11.2011
comment
Привет, я наткнулся на тот же вопрос, что и @Nate Weiner, и я пытался использовать ваш код, но он не работает. Прежде всего, компилятор жалуется на неявный диалог между kCGImageInfo и kCGBitmapInfo. Смешивание типов перечислений - плохая практика. Затем CGBitmapContextCreate возвращает 0x0. Я полагаю, это вызвано передачей NULL параметру цветового пространства. Объясните, пожалуйста, как заставить работать код из вашего ответа? - person kelin; 05.11.2014

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

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

Единственное требование для этого решения - 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 на использование vImage Accelerate Framework, возможно, пользуясь оптимизацией векторной обработки в этой библиотеке. Я не пробовал.)

person Adam Kaplan    schedule 09.10.2016
comment
CoreGraphics (CGImage) вызывает vImage для множества вещей. Я бы запускал инструменты в коде и проверял, выполняет ли уже vImage свою работу. Если это не так и CG-код выглядит не слишком хорошо, отправьте радар. Лучше, если CG вызовет vImage, потому что тогда от этого выиграют все. - person Ian Ollmann; 14.10.2016

Я попробовал код, предоставленный 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);
person Jorge Garcia    schedule 29.08.2012
comment
Небольшая ошибка там человек. При просмотре альфа-данных изображения вы обновили адрес данных, с которого считываете каждый пиксель, до правильного значения, но вы по-прежнему записываете неправильное значение, что приводит к полностью беспорядочным выводам. Я также предлагаю сохранить это значение во временном локальном атрибуте, чтобы: а) предотвратить возникновение такого рода ошибок и б) избежать необходимости выполнять вычисления дважды. - person Ash; 19.01.2014
comment
ширина с плавающей запятой = CGBitmapContextGetWidth (originalImageContext); int strideLength = (int) CGBitmapContextGetBytesPerRow (originalImageContext); - person geowar; 26.04.2014
comment
int strideLength = ROUND_UP (ширина * 1, 4); // был прав - person geowar; 26.04.2014