Comment obtenir des données de pixels à partir d'un UIImage (Cocoa Touch) ou CGImage (Core Graphics)?

200

J'ai un UIImage (Cocoa Touch). De cela, je suis heureux d'obtenir une image CG ou tout autre élément que vous aimeriez voir disponible. Je voudrais écrire cette fonction:

- (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
la source

Réponses:

249

Pour info, j'ai combiné la réponse de Keremk avec mon contour d'origine, nettoyé les fautes de frappe, généralisé pour retourner un tableau de couleurs et obtenu le tout à compiler. Voici le résultat:

+ (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
la source
4
N'oubliez pas de démultiplier les couleurs. De plus, ces multiplications par 1 ne font rien.
Peter Hosey
2
Même si c'est correct. Lorsque le nombre est un grand nombre (comme la longueur d'une ligne de l'image ou la taille de l'image entière!) Je ne pense pas que les performances de cette méthode seront bonnes car avoir trop d'objets UIColor est vraiment lourd. Dans la vraie vie quand j'ai besoin d'un pixel, alors UIColor est ok, (Un NSArray d'UIColors est trop pour moi). Et lorsque vous avez besoin de beaucoup de couleurs, je n'utiliserai pas UIColors car j'utiliserai probablement OpenGL, etc. À mon avis, cette méthode n'a pas d'utilisation réelle mais est très bonne à des fins éducatives.
nacho4d
4
Malloc beaucoup trop encombrant un grand espace uniquement pour lire un pixel.
ragnarius
46
UTILISEZ CALLOC AU LIEU DE MALLOC !!!! J'utilisais ce code pour tester si certains pixels étaient transparents et je récupérais de fausses informations là où les pixels étaient censés être transparents car la mémoire n'était pas effacée en premier. L'utilisation de calloc (largeur * hauteur, 4) au lieu de malloc a fait l'affaire. Le reste du code est super, MERCI!
DonnaLea
5
C'est bien. Merci. Remarque sur l'utilisation: x et y sont les coordonnées à l'intérieur de l'image pour commencer à obtenir des RGBAs et `` count '' est le nombre de pixels à partir de ce point à obtenir, en allant de gauche à droite, puis ligne par ligne. Exemple d'utilisation: Pour obtenir des RGBAs pour une image entière: getRGBAsFromImage:image atX:0 andY:0 count:image.size.width*image.size.height Pour obtenir des RGBAs pour la dernière ligne d'une image: getRGBAsFromImage:image atX:0 andY:image.size.height-1 count:image.size.width
Harris
49

Une façon de le faire est de dessiner l'image dans un contexte bitmap qui est soutenu par un tampon donné pour un espace colorimétrique donné (dans ce cas, il est RVB): (notez que cela copiera les données d'image dans ce tampon, donc vous faites voulez le mettre en cache au lieu de faire cette opération chaque fois que vous avez besoin d'obtenir des valeurs en pixels)

Voir ci-dessous comme exemple:

// 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
la source
J'ai fait quelque chose de similaire dans mon application. Pour extraire un pixel arbitraire, il vous suffit de dessiner dans un bitmap 1x1 avec un format bitmap connu, en ajustant correctement l'origine de CGBitmapContext.
Mark Bessey
5
Cela fuit la mémoire. Release rawData: gratuit (rawData);
Adam Waite
5
CGContextDrawImagenécessite trois arguments et au-dessus, seuls deux sont donnés ... Je pense que cela devrait êtreCGContextDrawImage(context, CGRectMake(0, 0, width, height), image)
user1135469
25

Apple technique Q & A QA1509 montre l'approche simple suivante:

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

Utilisez CFDataGetBytePtrpour accéder aux octets réels (et diverses CGImageGet*méthodes pour comprendre comment les interpréter).

yakovlev
la source
10
Ce n'est pas une excellente approche car le format de pixel a tendance à varier beaucoup par image. Il y a plusieurs choses qui peuvent changer, notamment 1. l'orientation de l'image 2. le format du composant alpha et 3. l'ordre des octets de RVB. J'ai personnellement passé du temps à essayer de décoder les documents d'Apple sur la façon de le faire, mais je ne suis pas sûr que cela en vaille la peine. Si vous voulez que cela se fasse rapidement, juste la solution de Keremk.
Tyler
1
Cette méthode me donne toujours le format BGR même si je sauvegarde l'image (depuis mon application dans l'espace colorimétrique RGBA)
Soumyajit
1
@Soumyaljit Cela va copier les données dans le format dans lequel les données CGImageRef ont été stockées à l'origine. Dans la plupart des cas, ce sera BGRA, mais pourrait également être YUV.
Cameron Lowell Palmer
18

Je ne pouvais pas croire qu'il n'y a pas une seule bonne réponse ici. Pas besoin d'allouer des pointeurs et les valeurs non multipliées doivent encore être normalisées. Pour aller droit au but, voici la version correcte pour Swift 4. Pour UIImageune utilisation juste .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)
        }
    }
}

La raison pour laquelle vous devez d'abord dessiner / convertir l'image dans un tampon est que les images peuvent avoir plusieurs formats différents. Cette étape est nécessaire pour le convertir dans un format cohérent que vous pouvez lire.

Erik Aigner
la source
Comment puis-je utiliser cette extension pour mon image? let imageObj = UIImage (nommé: "greencolorImg.png")
Sagar Sukode
let imageObj = UIImage(named:"greencolorImg.png"); imageObj.cgImage?.colors(at: [pt1, pt2])
Erik Aigner
garder à l' esprit que pt1et pt2sont des CGPointvariables.
AnBisw
sauvé ma journée :)
Dari
1
@ErikAigner est-ce que cela fonctionnera si je voulais obtenir la couleur de chaque pixel d'une image? quel est le coût de la performance?
Ameet Dhas
9

Voici un thread SO où @Matt rend uniquement le pixel souhaité dans un contexte 1x1 en déplaçant l'image de sorte que le pixel souhaité s'aligne avec le pixel du contexte.

tourbillon
la source
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
la source
J'ai posté cela et cela a fonctionné pour moi, mais en passant du tampon à un UIImage, je ne peux pas encore comprendre, des idées?
Nidal Fakhouri
8

UIImage est un wrapper dont les octets sont CGImage ou CIImage

Selon la référence Apple sur UIImage, l'objet est immuable et vous n'avez pas accès aux octets de support. S'il est vrai que vous pouvez accéder aux données CGImage si vous avez rempli le UIImageavec un CGImage(explicitement ou implicitement), il reviendra NULLsi le UIImageest soutenu par un CIImageet vice-versa.

Les objets image ne fournissent pas un accès direct à leurs données d'image sous-jacentes. Cependant, vous pouvez récupérer les données d'image dans d'autres formats pour les utiliser dans votre application. Plus précisément, vous pouvez utiliser les propriétés cgImage et ciImage pour récupérer des versions de l'image qui sont compatibles avec Core Graphics et Core Image, respectivement. Vous pouvez également utiliser les fonctions UIImagePNGRepresentation ( :) et UIImageJPEGRepresentation ( : _ :) pour générer un objet NSData contenant les données d'image au format PNG ou JPEG.

Astuces courantes pour contourner ce problème

Comme indiqué, vos options sont

  • UIImagePNGRepresentation ou JPEG
  • Déterminez si l'image contient des données de support CGImage ou CIImage et obtenez-les

Aucun de ces éléments n'est particulièrement bon si vous voulez une sortie qui n'est pas des données ARGB, PNG ou JPEG et les données ne sont pas déjà sauvegardées par CIImage.

Ma recommandation, essayez CIImage

Lors du développement de votre projet, il peut être plus judicieux d'éviter complètement UIImage et de choisir autre chose. UIImage, en tant que wrapper d'image Obj-C, est souvent soutenu par CGImage au point où nous le tenons pour acquis. CIImage a tendance à être un meilleur format de wrapper dans la mesure où vous pouvez utiliser un CIContext pour obtenir le format souhaité sans avoir besoin de savoir comment il a été créé. Dans votre cas, obtenir le bitmap reviendrait à appeler

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

En prime, vous pouvez commencer à faire de belles manipulations sur l'image en enchaînant les filtres sur l'image. Cela résout un grand nombre des problèmes où l'image est à l'envers ou doit être tournée / mise à l'échelle, etc.

Cameron Lowell Palmer
la source
6

S'appuyant sur la réponse d'Olie et d'Algal, voici une réponse mise à jour pour 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
la source
3

Version Swift 5

Les réponses données ici sont obsolètes ou incorrectes car elles ne prennent pas en compte les éléments suivants:

  1. La taille en pixels de l'image peut différer de sa taille en points renvoyée par image.size.width/image.size.height .
  2. Il peut y avoir différentes dispositions utilisées par les composants pixel de l'image, tels que BGRA, ABGR, ARGB, etc. ou peuvent ne pas avoir de composant alpha du tout, tels que BGR et RGB. Par exemple, la UIView.drawHierarchy(in:afterScreenUpdates:)méthode peut produire des images BGRA.
  3. Les composants de couleur peuvent être prémultipliés par l'alpha pour tous les pixels de l'image et doivent être divisés par l'alpha afin de restaurer la couleur d'origine.
  4. Pour l'optimisation de la mémoire utilisée par CGImage, la taille d'une ligne de pixels en octets peut être supérieure à la simple multiplication de la largeur de pixel par 4.

Le code ci-dessous est de fournir une solution Swift 5 universelle pour obtenir le UIColorpixel pour tous ces cas spéciaux. Le code est optimisé pour la convivialité et la clarté, pas pour les performances.

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
la source
Qu'en est-il des changements d'orientation? Vous n'avez pas besoin de vérifier également UIImage.Orientation?
endavid
En outre, votre componentLayoutmanque le cas où il n'y a que l' alpha, .alphaOnly.
endavid
@endavid "Alpha uniquement" n'est pas pris en charge (car il s'agit d'un cas très rare). L'orientation de l'image est hors de portée de ce code, mais il existe de nombreuses ressources sur la façon de normaliser l'orientation d'un UIImage, ce que vous feriez avant de commencer à lire ses couleurs de pixels.
Desmond Hume
2

Basé sur différentes réponses mais principalement sur cela , cela fonctionne pour ce dont j'ai besoin:

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

À la suite de ces lignes, vous aurez un pixel au format AARRGGBB avec alpha toujours défini sur FF dans l'entier non signé de 4 octets pixel1.

cprcrack
la source
1

Pour accéder aux valeurs RVB brutes d'un UIImage dans Swift 5, utilisez le CGImage sous-jacent et son 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
la source