Como obter dados de pixel de uma UIImage (Cocoa Touch) ou CGImage (Core Graphics)?

200

Eu tenho uma UIImage (Cocoa Touch). A partir disso, fico feliz em obter uma CGImage ou qualquer outra coisa que você gostaria que estivesse disponível. Eu gostaria de escrever esta função:

- (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
fonte

Respostas:

249

Para sua informação, combinei a resposta de Keremk com meu esboço original, limpei os erros de digitação, generalizei-a para retornar uma variedade de cores e fiz com que tudo fosse compilado. Aqui está o resultado:

+ (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
fonte
4
Não se esqueça de multiplicar as cores. Além disso, essas multiplicações por 1 não fazem nada.
Peter Hosey
2
Mesmo que esteja correto. Quando count é um número grande (como o comprimento de uma linha da imagem ou o tamanho inteiro da imagem!), Não o desempenho desse método será bom, pois ter muitos objetos UIColor é realmente pesado. Na vida real, quando eu preciso de um pixel, o UIColor está ok (um NSArray da UIColors é demais para mim). E quando você precisar de muitas cores, não usarei UIColors, pois provavelmente usarei OpenGL, etc. Na minha opinião, este método não tem uso na vida real, mas é muito bom para fins educacionais.
N
4
Malloc muito pesado um espaço grande apenas para ler um pixel.
Ragnarius
46
USE CALLOC EM VEZ DE MALLOC !!!! Eu estava usando esse código para testar se certos pixels eram transparentes e estava recuperando informações falsas em que os pixels deveriam ser transparentes porque a memória não foi limpa primeiro. Usar calloc (largura * altura, 4) em vez de malloc fez o truque. O resto do código é ótimo, OBRIGADO!
DonnaLea
5
Isso é ótimo. Obrigado. Nota sobre o uso: x e y são as coordenadas na imagem para começar a obter RGBAs e 'count' é o número de pixels daquele ponto a ser obtido, indo da esquerda para a direita e depois linha por linha. Exemplo de uso: Para obter RGBAs para uma imagem inteira: getRGBAsFromImage:image atX:0 andY:0 count:image.size.width*image.size.height Para obter RGBAs para a última linha de uma imagem: getRGBAsFromImage:image atX:0 andY:image.size.height-1 count:image.size.width
Harris
49

Uma maneira de fazer isso é desenhar a imagem em um contexto de bitmap que é apoiado por um determinado buffer para um determinado espaço de cores (nesse caso, é RGB): (observe que isso copiará os dados da imagem para esse buffer, então você deve deseja armazená-lo em cache em vez de fazer essa operação sempre que precisar obter valores de pixel)

Veja abaixo como uma amostra:

// 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
fonte
Eu fiz algo semelhante no meu aplicativo. Para extrair um pixel arbitrário, basta desenhar em um bitmap 1x1 com um formato de bitmap conhecido, ajustando a origem do CGBitmapContext adequadamente.
22877 Mark Bessey
5
Isso vaza memória. Liberar rawData: free (rawData);
Adam Waite
5
CGContextDrawImagerequer três argumentos e acima apenas dois são dadas ... Eu acho que deveria serCGContextDrawImage(context, CGRectMake(0, 0, width, height), image)
user1135469
25

Perguntas e respostas técnicas da Apple O QA1509 mostra a seguinte abordagem simples:

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

Use CFDataGetBytePtrpara acessar os bytes reais (e vários CGImageGet*métodos para entender como interpretá-los).

yakovlev
fonte
10
Essa não é uma ótima abordagem, porque o formato de pixel tende a variar muito por imagem. Há várias coisas que podem mudar: 1. a orientação da imagem; 2. o formato do componente alfa e 3. a ordem de bytes do RGB. Pessoalmente, passei algum tempo tentando decodificar os documentos da Apple sobre como fazer isso, mas não sei se vale a pena. Se você quer que seja feito rapidamente, apenas a solução da keremk.
Tyler
1
Este método sempre me dá formato BGR mesmo se eu salvar a imagem (de dentro do meu aplicativo em RGBA de cores)
Soumyajit
1
@Soumyaljit Isso copiará os dados no formato em que os dados CGImageRef foram originalmente armazenados. Na maioria dos casos, isso será BGRA, mas também pode ser YUV.
Cameron Lowell Palmer
18

Não conseguia acreditar que não há uma única resposta correta aqui. Não há necessidade de alocar ponteiros, e os valores não multiplicados ainda precisam ser normalizados. Para ir direto ao ponto, aqui está a versão correta do Swift 4. Para UIImageapenas usar .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)
        }
    }
}

O motivo pelo qual você precisa desenhar / converter a imagem primeiro em um buffer é porque as imagens podem ter vários formatos diferentes. Esta etapa é necessária para convertê-lo em um formato consistente que você possa ler.

Erik Aigner
fonte
Como posso usar esta extensão para a minha imagem? deixe imageObj = UIImage (nomeado: "greencolorImg.png")
Sagar Sukode
let imageObj = UIImage(named:"greencolorImg.png"); imageObj.cgImage?.colors(at: [pt1, pt2])
Erik Aigner
tenha em mente que pt1e pt2são CGPointvariáveis.
AnBisw
salvou o meu dia :)
Dari
1
@ErikAigner isso funcionará se eu quiser obter a cor de cada pixel de uma imagem? qual é o custo de desempenho?
Ameet DHAS
9

Aqui está um tópico SO em que o @Matt renderiza apenas o pixel desejado em um contexto 1x1 deslocando a imagem para que o pixel desejado se alinhe com o pixel no contexto.

redemoinho
fonte
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);
Nidal Fakhouri
fonte
Eu postei isso e funcionou para mim, mas voltando do buffer para uma UIImage Ainda não consigo descobrir, alguma idéia?
Nidal Fakhouri
8

UIImage é um invólucro, os bytes são CGImage ou CIImage

De acordo com a Referência da Apple na UIImage, o objeto é imutável e você não tem acesso aos bytes de backup. Embora seja verdade que você possa acessar os dados da CGImage se os tiver preenchido UIImagecom um CGImage(explícita ou implicitamente), ele retornará NULLse UIImagefor apoiado por ae CIImagevice-versa.

Os objetos de imagem não fornecem acesso direto aos dados de imagem subjacentes. No entanto, você pode recuperar os dados da imagem em outros formatos para usar no seu aplicativo. Especificamente, você pode usar as propriedades cgImage e ciImage para recuperar versões da imagem compatíveis com Core Graphics e Core Image, respectivamente. Você também pode usar as funções UIImagePNGRepresentation ( :) e UIImageJPEGRepresentation ( : _ :) para gerar um objeto NSData que contém os dados da imagem no formato PNG ou JPEG.

Truques comuns para contornar esse problema

Conforme indicado, suas opções são

  • UIImagePNGRepresentation ou JPEG
  • Determine se a imagem possui dados de backup CGImage ou CIImage e obtenha-os lá

Nenhum desses truques é particularmente bom se você deseja uma saída que não seja dados ARGB, PNG ou JPEG e os dados ainda não sejam suportados pelo CIImage.

Minha recomendação, tente CIImage

Ao desenvolver seu projeto, pode fazer mais sentido evitar completamente o UIImage e escolher outra coisa. UIImage, como um invólucro de imagem Obj-C, geralmente é suportado por CGImage até o ponto em que tomamos como garantido. O CIImage tende a ser um formato de wrapper melhor, pois você pode usar um CIContext para obter o formato desejado sem precisar saber como ele foi criado. No seu caso, obter o bitmap seria uma questão de chamar

- render: toBitmap: rowBytes: limites: formato: colorSpace:

Como um bônus adicional, você pode começar a fazer boas manipulações na imagem, encadeando filtros na imagem. Isso resolve muitos problemas em que a imagem está de cabeça para baixo ou precisa ser girada / redimensionada etc.

Cameron Lowell Palmer
fonte
6

Com base na resposta de Olie e Algal, aqui está uma resposta atualizada para o 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
fonte
3

Versão Swift 5

As respostas fornecidas aqui estão desatualizadas ou incorretas porque não levam em consideração o seguinte:

  1. O tamanho do pixel da imagem pode diferir do tamanho do ponto retornado por image.size.width/image.size.height .
  2. Pode haver vários layouts usados ​​pelos componentes de pixel na imagem, como BGRA, ABGR, ARGB etc. ou pode não ter um componente alfa, como BGR e RGB. Por exemplo, o UIView.drawHierarchy(in:afterScreenUpdates:)método pode produzir imagens BGRA.
  3. Os componentes de cores podem ser pré-multiplicados pelo alfa para todos os pixels da imagem e precisam ser divididos pelo alfa para restaurar a cor original.
  4. Para a otimização de memória usada por CGImage, o tamanho de uma linha de pixels em bytes pode ser maior que a mera multiplicação da largura de pixels por 4.

O código abaixo é fornecer uma solução universal Swift 5 para obter o UIColorpixel de todos os casos especiais. O código é otimizado para usabilidade e clareza, não para desempenho.

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
    }

}
Desmond Hume
fonte
E as mudanças na orientação? Você não precisa verificar o UIImage.Orientation também?
endavid
Além disso, você componentLayoutestá perdendo o caso quando há apenas alfa .alphaOnly,.
endavid
@ endavid "Somente alfa" não é suportado (por ser um caso muito raro). A orientação da imagem está fora do escopo deste código, mas há muitos recursos sobre como normalizar a orientação de a UIImage, que é o que você faria antes de começar a ler suas cores de pixel.
Desmond Hume
2

Com base em respostas diferentes, mas principalmente nisso , funciona para o que eu preciso:

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

Como resultado dessas linhas, você terá um pixel no formato AARRGGBB com alfa sempre definido como FF no número inteiro não assinado de 4 bytes pixel1.

cprcrack
fonte
1

Para acessar os valores RGB brutos de uma UIImage no Swift 5, use a CGImage subjacente e seu 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/

Ralf Ebert
fonte