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

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

Например:

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

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

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

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

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


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


Ответы (3)


arrow_upward
11
arrow_downward

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

Вот один из способов добиться этого. Возьмите исходный образ 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

arrow_upward
3
arrow_downward

Решение Бена Зотто правильное, но есть способ сделать это без математических вычислений или локальной сложности, полагаясь на 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

arrow_upward
1
arrow_downward

Я попробовал код, предоставленный 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