Как получить данные пикселей из UIImage (Cocoa Touch) или CGImage (Core Graphics)?

200

У меня есть UIImage (Touch Cocoa). Исходя из этого, я счастлив получить CGImage или что-нибудь еще, что вы хотели бы, что доступно. Я хотел бы написать эту функцию:

- (int)getRGBAFromImage:(UIImage *)image atX:(int)xx andY:(int)yy {
  // [...]
  // What do I want to read about to help
  // me fill in this bit, here?
  // [...]

  int result = (red << 24) | (green << 16) | (blue << 8) | alpha;
  return result;
}
Olie
источник

Ответы:

249

К вашему сведению, я объединил ответ Керемка с моей первоначальной схемой, убрал опечатки, обобщил их, чтобы получить массив цветов, и получил все, что нужно скомпилировать. Вот результат:

+ (NSArray*)getRGBAsFromImage:(UIImage*)image atX:(int)x andY:(int)y count:(int)count
{
    NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];

    // First get the image into your data buffer
    CGImageRef imageRef = [image CGImage];
    NSUInteger width = CGImageGetWidth(imageRef);
    NSUInteger height = CGImageGetHeight(imageRef);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    unsigned char *rawData = (unsigned char*) calloc(height * width * 4, sizeof(unsigned char));
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * width;
    NSUInteger bitsPerComponent = 8;
    CGContextRef context = CGBitmapContextCreate(rawData, width, height,
                    bitsPerComponent, bytesPerRow, colorSpace,
                    kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);

    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    CGContextRelease(context);

    // Now your rawData contains the image data in the RGBA8888 pixel format.
    NSUInteger byteIndex = (bytesPerRow * y) + x * bytesPerPixel;
    for (int i = 0 ; i < count ; ++i)
    {
        CGFloat alpha = ((CGFloat) rawData[byteIndex + 3] ) / 255.0f;
        CGFloat red   = ((CGFloat) rawData[byteIndex]     ) / alpha;
        CGFloat green = ((CGFloat) rawData[byteIndex + 1] ) / alpha;
        CGFloat blue  = ((CGFloat) rawData[byteIndex + 2] ) / alpha;
        byteIndex += bytesPerPixel;

        UIColor *acolor = [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
        [result addObject:acolor];
    }

  free(rawData);

  return result;
}
Olie
источник
4
Не забудьте перемножить цвета. Кроме того, эти умножения на 1 ничего не делают.
Питер Хоси
2
Даже если это правильно. Когда count является большим числом (например, длина строки изображения или весь размер изображения!), Я не думаю, что производительность этого метода будет хорошей, поскольку слишком много объектов UIColor действительно тяжело. В реальной жизни, когда мне нужен пиксель, тогда UIColor в порядке, (NSArray of UIColors - это слишком много для меня). И когда вам понадобится куча цветов, я не буду использовать UIColors, так как я, вероятно, буду использовать OpenGL и т. Д. По моему мнению, этот метод не имеет реального использования, но очень хорош для образовательных целей.
nacho4d
4
Слишком громоздко малок большое пространство только для чтения одного пикселя.
Рагнариус
46
ИСПОЛЬЗУЙТЕ CALLOC ВМЕСТО MALLOC !!!! Я использовал этот код, чтобы проверить, были ли определенные пиксели прозрачными, и я получал поддельную информацию, где пиксели должны были быть прозрачными, потому что память не была очищена первой. Используя calloc (width * height, 4) вместо malloc, добились цели. Остальной код отличный, СПАСИБО!
DonnaLea
5
Это круто. Спасибо. Примечание по использованию: x и y - это координаты в изображении, с которых нужно начать получать RGBA, а «count» - это количество пикселей, которые нужно получить, двигаясь слева направо, затем строка за строкой. Пример использования: Чтобы получить RGBA для всего изображения: getRGBAsFromImage:image atX:0 andY:0 count:image.size.width*image.size.height Чтобы получить RGBA для последнего ряда изображения: getRGBAsFromImage:image atX:0 andY:image.size.height-1 count:image.size.width
Harris
49

Один из способов сделать это - нарисовать изображение в растровом контексте, который поддерживается данным буфером для данного цветового пространства (в данном случае это RGB): (обратите внимание, что это скопирует данные изображения в этот буфер, поэтому вы делаете хотите кэшировать его вместо выполнения этой операции каждый раз, когда вам нужно получить значения пикселей)

Смотрите ниже как образец:

// First get the image into your data buffer
CGImageRef image = [myUIImage CGImage];
NSUInteger width = CGImageGetWidth(image);
NSUInteger height = CGImageGetHeight(image);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
unsigned char *rawData = malloc(height * width * 4);
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel * width;
NSUInteger bitsPerComponent = 8;
CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);

CGContextDrawImage(context, CGRectMake(0, 0, width, height));
CGContextRelease(context);

// Now your rawData contains the image data in the RGBA8888 pixel format.
int byteIndex = (bytesPerRow * yy) + xx * bytesPerPixel;
red = rawData[byteIndex];
green = rawData[byteIndex + 1];
blue = rawData[byteIndex + 2];
alpha = rawData[byteIndex + 3];
keremk
источник
Я сделал нечто подобное в моем приложении. Чтобы извлечь произвольный пиксель, вы просто рисуете в растровое изображение 1x1 с известным растровым форматом, соответствующим образом корректируя происхождение CGBitmapContext.
Марк Бесси
5
Это утечка памяти. Выпуск rawData: бесплатно (rawData);
Адам Уэйт
5
CGContextDrawImageтребует три аргумента и выше приведены только два ... Я думаю, что это должно бытьCGContextDrawImage(context, CGRectMake(0, 0, width, height), image)
user1135469
25

Apple, технический Q & QA1509 показывает следующий простой подход:

CFDataRef CopyImagePixels(CGImageRef inImage)
{
    return CGDataProviderCopyData(CGImageGetDataProvider(inImage));
}

Используйте, CFDataGetBytePtrчтобы получить действительные байты (и различные CGImageGet*методы, чтобы понять, как их интерпретировать).

Yakovlev
источник
10
Это не очень хороший подход, потому что формат пикселя имеет тенденцию сильно варьироваться в зависимости от изображения. Есть несколько вещей, которые могут измениться, в том числе: 1. ориентация изображения 2. формат альфа-компонента и 3. порядок байтов RGB. Я лично потратил некоторое время, пытаясь расшифровать документы Apple, как это сделать, но я не уверен, что оно того стоит. Если вы хотите сделать это быстро, просто решение Керемка.
Тайлер
1
Этот метод всегда дает мне формат BGR, даже если я сохраняю изображение (из моего приложения в
цветовом
1
@Soumyaljit Это скопирует данные в том формате, в котором они изначально хранились. В большинстве случаев это будет BGRA, но также может быть и YUV.
Кэмерон Лоуэлл Палмер
18

Не могу поверить, что здесь нет ни одного правильного ответа . Нет необходимости выделять указатели, а неумноженные значения по-прежнему необходимо нормализовать. Чтобы перейти к погоне, вот правильная версия для Swift 4. UIImageПросто для использования .cgImage.

extension CGImage {
    func colors(at: [CGPoint]) -> [UIColor]? {
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bytesPerPixel = 4
        let bytesPerRow = bytesPerPixel * width
        let bitsPerComponent = 8
        let bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue

        guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
            let ptr = context.data?.assumingMemoryBound(to: UInt8.self) else {
            return nil
        }

        context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))

        return at.map { p in
            let i = bytesPerRow * Int(p.y) + bytesPerPixel * Int(p.x)

            let a = CGFloat(ptr[i + 3]) / 255.0
            let r = (CGFloat(ptr[i]) / a) / 255.0
            let g = (CGFloat(ptr[i + 1]) / a) / 255.0
            let b = (CGFloat(ptr[i + 2]) / a) / 255.0

            return UIColor(red: r, green: g, blue: b, alpha: a)
        }
    }
}

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

Эрик Айгнер
источник
Как я могу использовать это расширение для моего изображения? let imageObj = UIImage (названный: "greencolorImg.png")
Сагар Сукоде
let imageObj = UIImage(named:"greencolorImg.png"); imageObj.cgImage?.colors(at: [pt1, pt2])
Эрик Эйгнер,
имейте в виду, что pt1и pt2являются CGPointпеременными.
AnBisw
спас мой день :)
Дари
1
@ErikAigner будет ли это работать, если я хочу получить цвет каждого пикселя изображения? какова стоимость исполнения?
Амит Дхас
9

Вот SO поток, где @Matt рендерит только нужный пиксель в контекст 1x1, смещая изображение так, чтобы нужный пиксель выравнивался с одним пикселем в контексте.

Эдди
источник
9
NSString * path = [[NSBundle mainBundle] pathForResource:@"filename" ofType:@"jpg"];
UIImage * img = [[UIImage alloc]initWithContentsOfFile:path];
CGImageRef image = [img CGImage];
CFDataRef data = CGDataProviderCopyData(CGImageGetDataProvider(image));
const unsigned char * buffer =  CFDataGetBytePtr(data);
Нидал Фахури
источник
Я опубликовал это, и это сработало для меня, но возвращаясь из буфера обратно в UIImage, я, похоже, пока не могу понять, есть идеи?
Нидал Фахури
8

UIImage - это обертка, байты - CGImage или CIImage.

Согласно справочнику Apple по UIImage, объект является неизменным, и у вас нет доступа к резервным байтам. Хотя верно, что вы можете получить доступ к данным CGImage, если вы заполнили UIImageих CGImage(явным или неявным образом), они вернутся, NULLесли они UIImageподдерживаются CIImageи наоборот.

Объекты изображения не обеспечивают прямой доступ к данным, лежащим в их основе. Однако вы можете получить данные изображения в других форматах для использования в вашем приложении. В частности, вы можете использовать свойства cgImage и ciImage для получения версий изображения, которые совместимы с Core Graphics и Core Image, соответственно. Вы также можете использовать функции UIImagePNGRepresentation ( :) и UIImageJPEGRepresentation ( : _ :) для создания объекта NSData, содержащего данные изображения в формате PNG или JPEG.

Общие приемы, чтобы обойти эту проблему

Как указано, ваши варианты

  • UIImagePNGRпредставление или JPEG
  • Определите, есть ли на изображении данные поддержки CGImage или CIImage, и получите их там

Ни один из этих приемов не очень полезен, если вы хотите выводить данные, которые не являются данными ARGB, PNG или JPEG, а данные еще не поддержаны CIImage.

Моя рекомендация, попробуйте CIImage

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

- render: toBitmap: rowBytes: bounds: format: colorSpace:

В качестве дополнительного бонуса вы можете начать делать приятные манипуляции с изображением, цепляя фильтры на изображение. Это решает множество проблем, когда изображение переворачивается вверх ногами или его необходимо повернуть / масштабировать и т. Д.

Кэмерон Лоуэлл Палмер
источник
6

Основываясь на ответе Оли и Алгала, вот обновленный ответ для Swift 3

public func getRGBAs(fromImage image: UIImage, x: Int, y: Int, count: Int) -> [UIColor] {

var result = [UIColor]()

// First get the image into your data buffer
guard let cgImage = image.cgImage else {
    print("CGContext creation failed")
    return []
}

let width = cgImage.width
let height = cgImage.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let rawdata = calloc(height*width*4, MemoryLayout<CUnsignedChar>.size)
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width
let bitsPerComponent = 8
let bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue

guard let context = CGContext(data: rawdata, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
    print("CGContext creation failed")
    return result
}

context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))

// Now your rawData contains the image data in the RGBA8888 pixel format.
var byteIndex = bytesPerRow * y + bytesPerPixel * x

for _ in 0..<count {
    let alpha = CGFloat(rawdata!.load(fromByteOffset: byteIndex + 3, as: UInt8.self)) / 255.0
    let red = CGFloat(rawdata!.load(fromByteOffset: byteIndex, as: UInt8.self)) / alpha
    let green = CGFloat(rawdata!.load(fromByteOffset: byteIndex + 1, as: UInt8.self)) / alpha
    let blue = CGFloat(rawdata!.load(fromByteOffset: byteIndex + 2, as: UInt8.self)) / alpha
    byteIndex += bytesPerPixel

    let aColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)
    result.append(aColor)
}

free(rawdata)

return result
}

swillsea
источник
3

Версия Swift 5

Ответы, приведенные здесь, являются либо устаревшими, либо неправильными, поскольку они не учитывают следующее:

  1. Размер пикселя изображения может отличаться от размера его точки, который возвращается image.size.width/ image.size.height.
  2. Могут быть различные макеты, используемые пиксельными компонентами изображения, такими как BGRA, ABGR, ARGB и т. Д., Или они могут вообще не иметь альфа-компонента, например BGR и RGB. Например, UIView.drawHierarchy(in:afterScreenUpdates:)метод может создавать изображения BGRA.
  3. Компоненты цвета могут быть предварительно умножены на альфа для всех пикселей изображения и должны быть разделены на альфа, чтобы восстановить исходный цвет.
  4. Для используемой оптимизации памяти CGImageразмер строки пикселей в байтах может быть больше, чем простое умножение ширины пикселя на 4.

Приведенный ниже код должен предоставить универсальное решение Swift 5 для получения UIColorпикселя для всех таких особых случаев. Код оптимизирован для удобства использования и ясности, а не для производительности.

public extension UIImage {

    var pixelWidth: Int {
        return cgImage?.width ?? 0
    }

    var pixelHeight: Int {
        return cgImage?.height ?? 0
    }

    func pixelColor(x: Int, y: Int) -> UIColor {
        assert(
            0..<pixelWidth ~= x && 0..<pixelHeight ~= y,
            "Pixel coordinates are out of bounds")

        guard
            let cgImage = cgImage,
            let data = cgImage.dataProvider?.data,
            let dataPtr = CFDataGetBytePtr(data),
            let colorSpaceModel = cgImage.colorSpace?.model,
            let componentLayout = cgImage.bitmapInfo.componentLayout
        else {
            assertionFailure("Could not get a pixel of an image")
            return .clear
        }

        assert(
            colorSpaceModel == .rgb,
            "The only supported color space model is RGB")
        assert(
            cgImage.bitsPerPixel == 32 || cgImage.bitsPerPixel == 24,
            "A pixel is expected to be either 4 or 3 bytes in size")

        let bytesPerRow = cgImage.bytesPerRow
        let bytesPerPixel = cgImage.bitsPerPixel/8
        let pixelOffset = y*bytesPerRow + x*bytesPerPixel

        if componentLayout.count == 4 {
            let components = (
                dataPtr[pixelOffset + 0],
                dataPtr[pixelOffset + 1],
                dataPtr[pixelOffset + 2],
                dataPtr[pixelOffset + 3]
            )

            var alpha: UInt8 = 0
            var red: UInt8 = 0
            var green: UInt8 = 0
            var blue: UInt8 = 0

            switch componentLayout {
            case .bgra:
                alpha = components.3
                red = components.2
                green = components.1
                blue = components.0
            case .abgr:
                alpha = components.0
                red = components.3
                green = components.2
                blue = components.1
            case .argb:
                alpha = components.0
                red = components.1
                green = components.2
                blue = components.3
            case .rgba:
                alpha = components.3
                red = components.0
                green = components.1
                blue = components.2
            default:
                return .clear
            }

            // If chroma components are premultiplied by alpha and the alpha is `0`,
            // keep the chroma components to their current values.
            if cgImage.bitmapInfo.chromaIsPremultipliedByAlpha && alpha != 0 {
                let invUnitAlpha = 255/CGFloat(alpha)
                red = UInt8((CGFloat(red)*invUnitAlpha).rounded())
                green = UInt8((CGFloat(green)*invUnitAlpha).rounded())
                blue = UInt8((CGFloat(blue)*invUnitAlpha).rounded())
            }

            return .init(red: red, green: green, blue: blue, alpha: alpha)

        } else if componentLayout.count == 3 {
            let components = (
                dataPtr[pixelOffset + 0],
                dataPtr[pixelOffset + 1],
                dataPtr[pixelOffset + 2]
            )

            var red: UInt8 = 0
            var green: UInt8 = 0
            var blue: UInt8 = 0

            switch componentLayout {
            case .bgr:
                red = components.2
                green = components.1
                blue = components.0
            case .rgb:
                red = components.0
                green = components.1
                blue = components.2
            default:
                return .clear
            }

            return .init(red: red, green: green, blue: blue, alpha: UInt8(255))

        } else {
            assertionFailure("Unsupported number of pixel components")
            return .clear
        }
    }

}

public extension UIColor {

    convenience init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
        self.init(
            red: CGFloat(red)/255,
            green: CGFloat(green)/255,
            blue: CGFloat(blue)/255,
            alpha: CGFloat(alpha)/255)
    }

}

public extension CGBitmapInfo {

    enum ComponentLayout {

        case bgra
        case abgr
        case argb
        case rgba
        case bgr
        case rgb

        var count: Int {
            switch self {
            case .bgr, .rgb: return 3
            default: return 4
            }
        }

    }

    var componentLayout: ComponentLayout? {
        guard let alphaInfo = CGImageAlphaInfo(rawValue: rawValue & Self.alphaInfoMask.rawValue) else { return nil }
        let isLittleEndian = contains(.byteOrder32Little)

        if alphaInfo == .none {
            return isLittleEndian ? .bgr : .rgb
        }
        let alphaIsFirst = alphaInfo == .premultipliedFirst || alphaInfo == .first || alphaInfo == .noneSkipFirst

        if isLittleEndian {
            return alphaIsFirst ? .bgra : .abgr
        } else {
            return alphaIsFirst ? .argb : .rgba
        }
    }

    var chromaIsPremultipliedByAlpha: Bool {
        let alphaInfo = CGImageAlphaInfo(rawValue: rawValue & Self.alphaInfoMask.rawValue)
        return alphaInfo == .premultipliedFirst || alphaInfo == .premultipliedLast
    }

}
Десмонд Хьюм
источник
Как насчет изменения ориентации? Вам не нужно проверять UIImage.Orientation, а?
завершение
Кроме того, вы componentLayoutпропускаете случай, когда есть только альфа .alphaOnly.
завершение
@endavid "Альфа только" не поддерживается (это очень редкий случай). Ориентация изображения выходит за рамки этого кода, но есть много ресурсов о том, как нормализовать ориентацию a UIImage, что вы и сделали бы, прежде чем начать читать цвета его пикселей.
Десмонд Хьюм
2

Основываясь на разных ответах, но в основном на этом , это работает для того, что мне нужно:

UIImage *image1 = ...; // The image from where you want a pixel data
int pixelX = ...; // The X coordinate of the pixel you want to retrieve
int pixelY = ...; // The Y coordinate of the pixel you want to retrieve

uint32_t pixel1; // Where the pixel data is to be stored
CGContextRef context1 = CGBitmapContextCreate(&pixel1, 1, 1, 8, 4, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaNoneSkipFirst);
CGContextDrawImage(context1, CGRectMake(-pixelX, -pixelY, CGImageGetWidth(image1.CGImage), CGImageGetHeight(image1.CGImage)), image1.CGImage);
CGContextRelease(context1);

В результате этой строки у вас будет пиксель в формате AARRGGBB с альфа-каналом, всегда установленным в FF в 4-байтовом целом числе без знака pixel1.

cprcrack
источник
1

Для доступа к необработанным значениям RGB UIImage в Swift 5 используйте базовый CGImage и его dataProvider:

import UIKit

let image = UIImage(named: "example.png")!

guard let cgImage = image.cgImage,
    let data = cgImage.dataProvider?.data,
    let bytes = CFDataGetBytePtr(data) else {
    fatalError("Couldn't access image data")
}
assert(cgImage.colorSpace?.model == .rgb)

let bytesPerPixel = cgImage.bitsPerPixel / cgImage.bitsPerComponent
for y in 0 ..< cgImage.height {
    for x in 0 ..< cgImage.width {
        let offset = (y * cgImage.bytesPerRow) + (x * bytesPerPixel)
        let components = (r: bytes[offset], g: bytes[offset + 1], b: bytes[offset + 2])
        print("[x:\(x), y:\(y)] \(components)")
    }
    print("---")
}

https://www.ralfebert.de/ios/examples/image-processing/uiimage-raw-pixels/

Ральф Эберт
источник