¿Cómo obtener datos de píxeles de un UIImage (Cocoa Touch) o CGImage (Core Graphics)?

200

Tengo un UIImage (Cocoa Touch). A partir de eso, estoy feliz de obtener una CGImage o cualquier otra cosa que desee que esté disponible. Me gustaría escribir esta función:

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

Respuestas:

249

Para su información, combiné la respuesta de Keremk con mi esquema original, limpié los errores tipográficos, lo generalicé para devolver una variedad de colores y conseguí que todo se compilara. Aquí está el 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
fuente
44
No te olvides de desmultiplicar los colores. Además, esas multiplicaciones por 1 no hacen nada.
Peter Hosey
2
Incluso si es correcto. Cuando el recuento es un número grande (como la longitud de una fila de la imagen o el tamaño completo de la imagen) No creo que el rendimiento de este método sea bueno ya que tener demasiados objetos UIColor es realmente pesado. En la vida real, cuando necesito un píxel, entonces UIColor está bien (un NSArray of UIColors es demasiado para mí). Y cuando necesite un montón de colores, no usaré UIColors ya que probablemente usaré OpenGL, etc. En mi opinión, este método no tiene uso en la vida real, pero es muy bueno para fines educativos.
nacho4d
44
Demasiado engorroso malloc un gran espacio solo para leer un píxel.
ragnarius
46
¡UTILICE CALLOC EN LUGAR DE MALLOC! Estaba usando este código para probar si ciertos píxeles eran transparentes y estaba volviendo a obtener información falsa donde los píxeles debían ser transparentes porque la memoria no se borró primero. Usar calloc (ancho * alto, 4) en lugar de malloc hizo el truco. El resto del código es genial, ¡GRACIAS!
DonnaLea
55
Esto es genial. Gracias. Nota sobre el uso: x e y son las coordenadas dentro de la imagen para comenzar a obtener RGBA y 'contar' es el número de píxeles desde ese punto para obtener, yendo de izquierda a derecha, luego fila por fila. Ejemplo de uso: Para obtener RGBA para una imagen completa: getRGBAsFromImage:image atX:0 andY:0 count:image.size.width*image.size.height Para obtener RGBA para la última fila de una imagen: getRGBAsFromImage:image atX:0 andY:image.size.height-1 count:image.size.width
Harris
49

Una forma de hacerlo es dibujar la imagen en un contexto de mapa de bits respaldado por un búfer determinado para un espacio de color determinado (en este caso, es RGB): (tenga en cuenta que esto copiará los datos de la imagen a ese búfer, por lo que debe hacerlo desea almacenarlo en caché en lugar de hacer esta operación cada vez que necesite obtener valores de píxeles)

Vea a continuación como muestra:

// 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
fuente
Hice algo similar en mi aplicación. Para extraer un píxel arbitrario, simplemente dibuje en un mapa de bits 1x1 con un formato de mapa de bits conocido, ajustando el origen del CGBitmapContext adecuadamente.
Mark Bessey el
55
Esto pierde memoria. Release rawData: free (rawData);
Adam Waite
55
CGContextDrawImagerequiere tres argumentos y arriba solo se dan dos ... Creo que debería serloCGContextDrawImage(context, CGRectMake(0, 0, width, height), image)
user1135469
25

Preguntas y respuestas técnicas de Apple QA1509 muestra el siguiente enfoque simple:

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

Utilícelo CFDataGetBytePtrpara llegar a los bytes reales (y varios CGImageGet*métodos para comprender cómo interpretarlos).

yakovlev
fuente
10
Este no es un gran enfoque porque el formato de píxeles tiende a variar mucho por imagen. Hay varias cosas que pueden cambiar, incluyendo 1. la orientación de la imagen 2. el formato del componente alfa y 3. el orden de bytes de RGB. Personalmente, pasé algún tiempo tratando de decodificar los documentos de Apple sobre cómo hacerlo, pero no estoy seguro de que valga la pena. Si quieres hacerlo rápido, solo la solución de keremk.
Tyler
1
Este método siempre me da formato BGR incluso si
guardo
1
@Soumyaljit Esto va a copiar los datos en el formato en que se almacenaron originalmente los datos de CGImageRef. En la mayoría de los casos, esto será BGRA, pero también podría ser YUV.
Cameron Lowell Palmer
18

No podía creer que no haya una sola respuesta correcta aquí. No es necesario asignar punteros, y los valores no multiplicados aún deben normalizarse. Para ir al grano, aquí está la versión correcta para Swift 4. UIImageSolo para 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)
        }
    }
}

La razón por la que primero tiene que dibujar / convertir la imagen en un búfer es porque las imágenes pueden tener varios formatos diferentes. Este paso es necesario para convertirlo a un formato consistente que pueda leer.

Erik Aigner
fuente
¿Cómo puedo usar esta extensión para mi imagen? let imageObj = UIImage (llamado: "greencolorImg.png")
Sagar Sukode el
let imageObj = UIImage(named:"greencolorImg.png"); imageObj.cgImage?.colors(at: [pt1, pt2])
Erik Aigner
ten en cuenta que pt1y pt2son CGPointvariables.
AnBisw
me salvó el día :)
Dari
1
@ErikAigner ¿funcionará esto si quisiera obtener el color de cada píxel de una imagen? ¿Cuál es el costo de rendimiento?
Ameet Dhas
9

Aquí hay un subproceso SO en el que @Matt procesa solo el píxel deseado en un contexto de 1x1 desplazando la imagen para que el píxel deseado se alinee con el píxel en el contexto.

remolino
fuente
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
fuente
Publiqué esto y funcionó para mí, pero yendo desde el búfer de regreso a un UIImage todavía no puedo entender, ¿alguna idea?
Nidal Fakhouri
8

UIImage es un contenedor, los bytes son CGImage o CIImage

De acuerdo con la Referencia de Apple sobre UIImage, el objeto es inmutable y no tiene acceso a los bytes de respaldo. Si bien es cierto que puede acceder a los datos de CGImage si completó el UIImagecon un CGImage(explícita o implícitamente), volverá NULLsi UIImageestá respaldado por ay CIImageviceversa.

Los objetos de imagen no proporcionan acceso directo a sus datos de imagen subyacentes. Sin embargo, puede recuperar los datos de la imagen en otros formatos para usar en su aplicación. Específicamente, puede usar las propiedades cgImage y ciImage para recuperar versiones de la imagen que sean compatibles con Core Graphics y Core Image, respectivamente. También puede usar las funciones UIImagePNGRepresentation ( :) y UIImageJPEGRepresentation ( : _ :) para generar un objeto NSData que contenga los datos de la imagen en formato PNG o JPEG.

Trucos comunes para solucionar este problema

Como se indicó, sus opciones son

  • UIImagePNGRepresentation o JPEG
  • Determine si la imagen tiene datos de respaldo CGImage o CIImage y llévela allí

Ninguno de estos son trucos particularmente buenos si desea resultados que no sean datos ARGB, PNG o JPEG y los datos no estén respaldados por CIImage.

Mi recomendación, prueba CIImage

Al desarrollar su proyecto, puede tener más sentido evitar el UIImage por completo y elegir otra cosa. UIImage, como un contenedor de imágenes Obj-C, a menudo está respaldado por CGImage hasta el punto en que lo damos por sentado. CIImage tiende a ser un mejor formato envoltorio, ya que puede usar un CIContext para obtener el formato que desee sin necesidad de saber cómo se creó. En su caso, obtener el mapa de bits sería una cuestión de llamar

- render: toBitmap: rowBytes: límites: formato: colorSpace:

Como una ventaja adicional, puede comenzar a hacer buenas manipulaciones en la imagen encadenando filtros en la imagen. Esto resuelve muchos de los problemas donde la imagen está al revés o necesita ser rotada / escalada, etc.

Cameron Lowell Palmer
fuente
6

Sobre la base de la respuesta de Olie y Algal, aquí hay una respuesta actualizada para 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
fuente
3

Versión Swift 5

Las respuestas dadas aquí están desactualizadas o son incorrectas porque no tienen en cuenta lo siguiente:

  1. El tamaño de píxel de la imagen puede diferir de su tamaño de punto que es devuelto por image.size.width/ image.size.height.
  2. Puede haber varios diseños utilizados por los componentes de píxeles en la imagen, como BGRA, ABGR, ARGB, etc. o puede que no tengan un componente alfa, como BGR y RGB. Por ejemplo, el UIView.drawHierarchy(in:afterScreenUpdates:)método puede producir imágenes BGRA.
  3. Los componentes de color pueden ser premultiplicados por el alfa para todos los píxeles en la imagen y deben dividirse por alfa para restaurar el color original.
  4. Para la optimización de memoria utilizada por CGImage, el tamaño de una fila de píxeles en bytes puede ser mayor que la mera multiplicación del ancho de píxeles por 4.

El siguiente código es para proporcionar una solución universal de Swift 5 para obtener UIColorun píxel para todos esos casos especiales. El código está optimizado para facilidad de uso y claridad, no para rendimiento.

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
fuente
¿Qué pasa con los cambios en la orientación? ¿No necesita verificar UIImage.Orientation también?
endavid
Además, su componentLayoutfalta el caso cuando sólo hay alfa, .alphaOnly.
endavid
@endavid "Alpha only" no es compatible (por ser un caso muy raro). La orientación de la imagen está fuera del alcance de este código, pero hay muchos recursos sobre cómo normalizar la orientación de a UIImage, que es lo que haría antes de comenzar a leer sus colores de píxeles.
Desmond Hume
2

Basado en diferentes respuestas pero principalmente en esto , esto funciona para lo que necesito:

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 de estas líneas, tendrá un píxel en formato AARRGGBB con alfa siempre configurado en FF en el entero sin signo de 4 bytes pixel1.

cprcrack
fuente
1

Para acceder a los valores RGB sin procesar de un UIImage en Swift 5, use la imagen CG subyacente y su proveedor de datos:

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
fuente