Come ottenere i dati pixel da un UIImage (Cocoa Touch) o CGImage (Core Graphics)?


200

Ho un UIImage (Cocoa Touch). Da ciò, sono felice di ricevere un CGImage o qualsiasi altra cosa tu voglia che sia disponibile. Vorrei scrivere questa funzione:

- (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;
}

Risposte:


249

Cordiali saluti, ho combinato la risposta di Keremk con il mio schema originale, ho ripulito gli errori di battitura, l'ho generalizzata per restituire una gamma di colori e ho fatto compilare il tutto. Ecco il risultato:

+ (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;
}

4
Non dimenticare di annullare la riproduzione dei colori. Inoltre, quelle moltiplicazioni per 1 non fanno nulla.
Peter Hosey,

2
Anche se è corretto. Quando count è un numero elevato (come la lunghezza di una riga dell'immagine o l'intera dimensione dell'immagine!), Le prestazioni di questo metodo non sono buone poiché avere troppi oggetti UIColor è davvero pesante. Nella vita reale quando ho bisogno di un pixel, UIColor è ok, (Un NSArray di UIColors è troppo per me). E quando avrai bisogno di un sacco di colori, non userò UIColors poiché probabilmente userò OpenGL, ecc. Secondo me questo metodo non ha un uso reale ma è ottimo per scopi educativi.
nacho4d,

4
Troppo ingombrante malloc un grande spazio solo per leggere un pixel.
Ragnarius,

46
USA CALLOC INSTEAD DI MALLOC !!!! Stavo usando questo codice per verificare se alcuni pixel erano trasparenti e stavo recuperando informazioni fasulle in cui i pixel dovevano essere trasparenti perché la memoria non era stata cancellata per prima. Usare calloc (larghezza * altezza, 4) invece di malloc ha fatto il trucco. Il resto del codice è fantastico, GRAZIE!
DonnaLea,

5
Questo è fantastico Grazie. Nota sull'uso: xey sono le coordinate all'interno dell'immagine da cui iniziare a ottenere RGBA e 'count' è il numero di pixel da quel punto da ottenere, andando da sinistra a destra, quindi riga per riga. Esempio di utilizzo: per ottenere RGBA per un'intera immagine: getRGBAsFromImage:image atX:0 andY:0 count:image.size.width*image.size.height per ottenere RGBA per l'ultima riga di un'immagine: getRGBAsFromImage:image atX:0 andY:image.size.height-1 count:image.size.width
Harris,

49

Un modo per farlo è quello di disegnare l'immagine in un contesto bitmap che è supportato da un determinato buffer per un determinato spazio colore (in questo caso è RGB): (nota che questo copierà i dati dell'immagine in quel buffer, quindi vuoi memorizzarlo nella cache invece di fare questa operazione ogni volta che devi ottenere i valori dei pixel)

Vedi sotto come esempio:

// 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];

Ho fatto qualcosa di simile nella mia app. Per estrarre un pixel arbitrario, basta disegnare in una bitmap 1x1 con un formato bitmap noto, regolando in modo appropriato l'origine di CGBitmapContext.
Mark Bessey,

5
Questo perde memoria. Rilascia rawData: gratuito (rawData);
Adam Waite,

5
CGContextDrawImagerichiede tre argomenti e sopra solo due sono dati ... Penso che dovrebbe essereCGContextDrawImage(context, CGRectMake(0, 0, width, height), image)
user1135469

25

Le domande e risposte tecniche di Apple QA1509 mostrano il seguente approccio semplice:

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

Utilizzare CFDataGetBytePtrper arrivare ai byte effettivi (e vari CGImageGet*metodi per capire come interpretarli).


10
Questo non è un ottimo approccio perché il formato dei pixel tende a variare molto per immagine. Ci sono diverse cose che possono cambiare, tra cui 1. l'orientamento dell'immagine 2. il formato del componente alfa e 3. l'ordine dei byte di RGB. Personalmente ho trascorso del tempo a provare a decodificare i documenti di Apple su come farlo, ma non sono sicuro che ne valga la pena. Se lo vuoi fare velocemente, basta la soluzione di keremk.
Tyler,

1
Questo metodo mi dà sempre il formato BGR anche se salvo l'immagine (dall'interno della mia app nello
spazio

1
@Soumyaljit Copia i dati nel formato in cui erano originariamente memorizzati i dati CGImageRef. Nella maggior parte dei casi questo sarà BGRA, ma potrebbe anche essere YUV.
Cameron Lowell Palmer l'

18

Non riuscivo a credere che non ci sia un'unica risposta corretta qui. Non è necessario allocare i puntatori e i valori non moltiplicati devono ancora essere normalizzati. Per andare al sodo, ecco la versione corretta per Swift 4. Solo per UIImageuso .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)
        }
    }
}

Il motivo per cui devi disegnare / convertire prima l'immagine in un buffer è perché le immagini possono avere diversi formati. Questo passaggio è necessario per convertirlo in un formato coerente che puoi leggere.


Come posso usare questa estensione per la mia immagine? let imageObj = UIImage (chiamato: "greencolorImg.png")
Sagar Sukode

let imageObj = UIImage(named:"greencolorImg.png"); imageObj.cgImage?.colors(at: [pt1, pt2])
Erik Aigner,

tienilo a mente pt1e pt2sono CGPointvariabili.
AnBisw,

mi ha salvato la giornata :)
Dari,

1
@ErikAigner funzionerà se volessi ottenere il colore di ogni pixel di un'immagine? qual è il costo delle prestazioni?
Ameet Dhas,

9

Ecco un thread SO in cui @Matt esegue il rendering del solo pixel desiderato in un contesto 1x1 spostando l'immagine in modo che il pixel desiderato si allinei con un pixel nel contesto.


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

Ho pubblicato questo e ha funzionato per me, ma passando dal buffer a un UIImage non riesco ancora a capire, qualche idea?
Nidal Fakhouri,

8

UIImage è un wrapper i byte sono CGImage o CIImage

Secondo il riferimento Apple su UIImage, l'oggetto è immutabile e non si ha accesso ai byte di supporto. Anche se è vero che puoi accedere ai dati CGImage se hai popolato UIImagecon un CGImage(esplicitamente o implicitamente), tornerà NULLse UIImageè supportato da unCIImage e viceversa.

Gli oggetti immagine non forniscono accesso diretto ai dati immagine sottostanti. Tuttavia, puoi recuperare i dati delle immagini in altri formati da utilizzare nella tua app. In particolare, è possibile utilizzare le proprietà cgImage e ciImage per recuperare versioni dell'immagine che sono compatibili rispettivamente con Core Graphics e Core Image. Puoi anche usare UIImagePNGRepresentation ( :) e UIImageJPEGRepresentation ( : _ :) per generare un oggetto NSData contenente i dati di immagine in formato PNG o JPEG.

Trucchi comuni per aggirare questo problema

Come indicato, le opzioni sono

  • UIImagePNGRepresentation o JPEG
  • Determina se l'immagine ha dati di supporto CGImage o CIImage e portali lì

Nessuno di questi è un trucco particolarmente utile se si desidera un output che non sia ARGB, PNG o JPEG e che i dati non siano già supportati da CIImage.

La mia raccomandazione, prova CIImage

Durante lo sviluppo del tuo progetto potrebbe avere più senso evitare completamente UIImage e scegliere qualcos'altro. UIImage, come wrapper di immagini Obj-C, è spesso supportato da CGImage al punto in cui lo diamo per scontato. CIImage tende ad essere un formato wrapper migliore in quanto è possibile utilizzare un CIContext per ottenere il formato desiderato senza bisogno di sapere come è stato creato. Nel tuo caso, ottenere la bitmap sarebbe una questione di chiamata

- render: toBitmap: rowBytes: bounds: formato: colorSpace:

Come bonus aggiuntivo puoi iniziare a fare delle belle manipolazioni all'immagine concatenando i filtri sull'immagine. Questo risolve molti problemi in cui l'immagine è capovolta o deve essere ruotata / ridimensionata ecc.


6

Sulla base della risposta di Olie e Algal, ecco una risposta aggiornata per 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
}


3

Versione Swift 5

Le risposte fornite qui sono obsolete o errate perché non tengono conto di quanto segue:

  1. La dimensione in pixel dell'immagine può differire dalla dimensione in punti restituita da image.size.width/image.size.height .
  2. Ci possono essere vari layout usati dai componenti pixel nell'immagine, come BGRA, ABGR, ARGB ecc. O potrebbero non avere affatto un componente alfa, come BGR e RGB. Ad esempio, il UIView.drawHierarchy(in:afterScreenUpdates:)metodo può produrre immagini BGRA.
  3. I componenti di colore possono essere premoltiplicati per l'alfa per tutti i pixel nell'immagine e devono essere divisi per alfa per ripristinare il colore originale.
  4. Per l'ottimizzazione della memoria utilizzata da CGImage, la dimensione di una riga di pixel in byte può essere maggiore della semplice moltiplicazione della larghezza del pixel per 4.

Il codice qui sotto è quello di fornire una soluzione universale Swift 5 per ottenere UIColorun pixel per tutti questi casi speciali. Il codice è ottimizzato per usabilità e chiarezza, non per prestazioni.

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
    }

}

Che dire dei cambiamenti di orientamento? Non è necessario controllare anche UIImage.Orientation?
Endavid

Inoltre, il vostro componentLayoutnon è presente il caso in cui c'è solo alfa, .alphaOnly.
terminato il

@endavid "Solo Alpha" non è supportato (per essere un caso molto raro). L'orientamento dell'immagine non rientra nell'ambito di questo codice, ma ci sono molte risorse su come normalizzare l'orientamento di a UIImage, che è quello che faresti prima di iniziare a leggere i suoi colori di pixel.
Desmond Hume,

2

Basato su risposte diverse ma principalmente su questo , questo funziona per quello che mi serve:

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

Come risultato di queste righe, avrai un pixel in formato AARRGGBB con alpha sempre impostato su FF nell'intero senza segno a 4 byte pixel1.


1

Per accedere ai valori RGB grezzi di un UIImage in Swift 5 utilizzare il CGImage sottostante e il suo 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/

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.