Jak uzyskać dane pikselowe z UIImage (Cocoa Touch) lub CGImage (Core Graphics)?

200

Mam UIImage (Cocoa Touch). Z tego powodu cieszę się, że mogę uzyskać CGImage lub cokolwiek innego, co chcesz, jest dostępne. Chciałbym napisać tę funkcję:

- (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
źródło

Odpowiedzi:

249

Do twojej wiadomości, połączyłem odpowiedź Keremka z moim oryginalnym konturem, posprzątałem literówki, uogólniłem go, aby zwrócić tablicę kolorów i wszystko skompilowałem. Oto wynik:

+ (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
źródło
4
Nie zapomnij o nieprzystosowaniu kolorów. Te mnożenia przez 1 nic nie robią.
Peter Hosey
2
Nawet jeśli jest to poprawne. Gdy liczba jest duża (np. Długość wiersza obrazu lub cały rozmiar obrazu!), Wydajność tej metody nie będzie dobra, ponieważ zbyt duża liczba obiektów UIColor jest naprawdę ciężka. W prawdziwym życiu, kiedy potrzebuję piksela, UIColor jest w porządku (NSArray of UIColors to dla mnie za dużo). A kiedy potrzebujesz wiązki kolorów, nie użyję UIColors, ponieważ prawdopodobnie będę używać OpenGL itp. Moim zdaniem ta metoda nie ma rzeczywistego zastosowania, ale jest bardzo dobra do celów edukacyjnych.
nacho4d 12.01.11
4
Zbyt niewygodne centrum handlowe dużej przestrzeni tylko do odczytu jednego piksela.
ragnarius
46
KORZYSTAJ Z CALLOC ZAMIAST MALLOC !!!! Używałem tego kodu, aby sprawdzić, czy niektóre piksele są przezroczyste, i otrzymywałem fałszywe informacje, gdzie piksele miały być przezroczyste, ponieważ pamięć nie została najpierw wyczyszczona. Użycie calloc (szerokość * wysokość, 4) zamiast malloc załatwiło sprawę. Reszta kodu jest świetna, DZIĘKI!
DonnaLea,
5
To jest świetne. Dzięki. Uwaga na temat użytkowania: xiy to współrzędne w obrazie, od których zaczyna się pobieranie RGBA, a „liczba” to liczba pikseli od tego punktu, którą należy uzyskać, przechodząc od lewej do prawej, a następnie rząd po rzędzie. Przykład użycia: Aby uzyskać RGBA dla całego obrazu: getRGBAsFromImage:image atX:0 andY:0 count:image.size.width*image.size.height Aby uzyskać RGBA dla ostatniego wiersza obrazu: getRGBAsFromImage:image atX:0 andY:image.size.height-1 count:image.size.width
Harris
49

Jednym ze sposobów jest narysowanie obrazu w kontekście bitmapy, który jest wspierany przez dany bufor dla danej przestrzeni kolorów (w tym przypadku jest to RGB): (zauważ, że spowoduje to skopiowanie danych obrazu do tego bufora, więc robisz to chcesz buforować go zamiast wykonywać tę operację za każdym razem, gdy potrzebujesz wartości pikseli)

Zobacz poniżej jako przykład:

// 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
źródło
Zrobiłem coś podobnego w mojej aplikacji. Aby wyodrębnić dowolny piksel, wystarczy narysować mapę bitową 1x1 ze znanym formatem bitmapy, odpowiednio dostosowując początek CGBitmapContext.
Mark Bessey
5
To przecieka pamięć. Wydanie rawData: darmowy (rawData);
Adam Waite,
5
CGContextDrawImagewymaga trzech argumentów, a powyżej podane są tylko dwa ... Myślę, że tak powinno byćCGContextDrawImage(context, CGRectMake(0, 0, width, height), image)
1135469
25

Apple Q & A Techniczne QA1509 przedstawia następujące proste podejście:

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

Użyj, CFDataGetBytePtraby przejść do rzeczywistych bajtów (i różnych CGImageGet*metod, aby zrozumieć, jak je interpretować).

Jakowlew
źródło
10
To nie jest świetne podejście, ponieważ format pikseli często różni się w zależności od obrazu. Istnieje kilka rzeczy, które można zmienić, w tym 1. orientacja obrazu 2. format składnika alfa i 3. kolejność bajtów RGB. Osobiście poświęciłem trochę czasu na odkodowanie dokumentów Apple'a, jak to zrobić, ale nie jestem pewien, czy warto. Jeśli chcesz to zrobić szybko, po prostu rozwiązanie Keremka.
Tyler
1
Ta metoda zawsze daje mi format BGR, nawet jeśli
zapiszę
1
@Soumyaljit Spowoduje to skopiowanie danych w formacie, w którym dane CGImageRef były pierwotnie przechowywane. W większości przypadków będzie to BGRA, ale może to być również YUV.
Cameron Lowell Palmer,
18

Nie mogłem uwierzyć, że nie ma tu ani jednej poprawnej odpowiedzi . Nie ma potrzeby przydzielania wskaźników, a niezmnożone wartości nadal wymagają normalizacji. Aby przejść do sedna, oto odpowiednia wersja dla Swift 4. UIImageTylko do użytku .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)
        }
    }
}

Powodem, dla którego musisz najpierw narysować / przekonwertować obraz na bufor, jest to, że obrazy mogą mieć kilka różnych formatów. Ten krok jest wymagany do przekonwertowania go na spójny format, który można odczytać.

Erik Aigner
źródło
Jak mogę użyć tego rozszerzenia do mojego obrazu? let imageObj = UIImage (o nazwie: „greencolorImg.png”)
Sagar Sukode
let imageObj = UIImage(named:"greencolorImg.png"); imageObj.cgImage?.colors(at: [pt1, pt2])
Erik Aigner,
pamiętaj o tym pt1i pt2są to CGPointzmienne.
AnBisw
uratował mi dzień :)
Dari
1
@ErikAigner będzie działać, jeśli chcę uzyskać kolor każdego piksela obrazu? jaki jest koszt wydajności?
Ameet Dhas,
9

Oto wątek SO, w którym @Matt renderuje tylko pożądany piksel w kontekście 1x1 poprzez przemieszczenie obrazu, tak aby żądany piksel był wyrównany z jednym pikselem w kontekście.

wir
źródło
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
źródło
Wysłałem to i to zadziałało dla mnie, ale wracając z bufora do UIImage, nie mogę się jeszcze domyślić, jakieś pomysły?
Nidal Fakhouri,
8

UIImage to opakowanie, bajty to CGImage lub CIImage

Według Apple Reference na UIImage obiekt jest niezmienny i nie masz dostępu do bajtów kopii zapasowej. Chociaż prawdą jest, że możesz uzyskać dostęp do danych CGImage, jeśli zapełniłeś UIImageje CGImage(jawnie lub niejawnie), zwróci je, NULLjeśli UIImagejest poparte przez CIImagei odwrotnie.

Obiekty obrazu nie zapewniają bezpośredniego dostępu do podstawowych danych obrazu. Możesz jednak pobrać dane obrazu w innych formatach do użycia w aplikacji. W szczególności można użyć właściwości cgImage i ciImage do pobrania wersji obrazu zgodnych odpowiednio z Core Graphics i Core Image. Możesz także użyć funkcji UIImagePNGRepresentation ( :) i UIImageJPEGRepresentation ( : _ :) do wygenerowania obiektu NSData zawierającego dane obrazu w formacie PNG lub JPEG.

Typowe wskazówki pozwalające obejść ten problem

Jak już wspomniano, dostępne są opcje

  • UIImagePNGRepresentation lub JPEG
  • Sprawdź, czy obraz ma dane kopii zapasowej CGImage lub CIImage i uzyskaj je tam

Żadne z tych nie są szczególnie dobrymi sztuczkami, jeśli chcesz uzyskać dane wyjściowe, które nie są danymi ARGB, PNG lub JPEG, a dane nie są jeszcze wspierane przez CIImage.

Moja rekomendacja, spróbuj CIImage

Opracowując projekt, sensowniejsze może być całkowite uniknięcie UIImage i wybranie czegoś innego. UIImage, jako opakowanie obrazu Obj-C, jest często wspierane przez CGImage do tego stopnia, że ​​uznajemy to za coś oczywistego. CIImage jest zwykle lepszym formatem opakowania, ponieważ można użyć CIContext, aby uzyskać pożądany format bez konieczności poznawania sposobu jego utworzenia. W twoim przypadku uzyskanie mapy bitowej byłoby kwestią sprawdzenia

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

Jako dodatkowy bonus możesz zacząć robić fajne manipulacje obrazem, łącząc filtry z obrazem. Rozwiązuje to wiele problemów, w których obraz jest odwrócony do góry nogami lub wymaga obracania / skalowania itp.

Cameron Lowell Palmer
źródło
6

Opierając się na odpowiedzi Olie i Algala, oto zaktualizowana odpowiedź dla 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
źródło
3

Wersja Swift 5

Podane tutaj odpowiedzi są nieaktualne lub niepoprawne, ponieważ nie uwzględniają następujących elementów:

  1. Rozmiar piksela obrazu może różnić się od jego wielkości punktowej zwracanej przez image.size.width/ image.size.height.
  2. Komponenty pikselowe w obrazie mogą mieć różne układy, takie jak BGRA, ABGR, ARGB itp., Lub mogą w ogóle nie zawierać składnika alfa, takiego jak BGR i RGB. Na przykład UIView.drawHierarchy(in:afterScreenUpdates:)metoda może generować obrazy BGRA.
  3. Składniki kolorów można wstępnie pomnożyć przez alfa dla wszystkich pikseli na obrazie i należy je podzielić przez alfa, aby przywrócić pierwotny kolor.
  4. W celu optymalizacji pamięci używanej przez CGImage, rozmiar rzędu pikseli w bajtach może być większy niż zwykłe pomnożenie szerokości piksela przez 4.

Poniższy kod ma zapewnić uniwersalne rozwiązanie Swift 5, aby uzyskać UIColorpiksel dla wszystkich takich specjalnych przypadków. Kod jest zoptymalizowany pod kątem użyteczności i przejrzystości, a nie wydajności.

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
źródło
Co ze zmianami orientacji? Nie musisz też sprawdzać UIImage.Orientation?
endavid
Również swój componentLayoutbrakuje przypadek, gdy istnieje tylko alfa .alphaOnly.
endavid
@endavid „Tylko alfa” nie jest obsługiwane (ponieważ występuje w bardzo rzadkich przypadkach). Orientacja obrazu jest poza zakresem tego kodu, ale istnieje wiele zasobów na temat normalizacji orientacji a UIImage, co należy zrobić przed rozpoczęciem czytania kolorów pikseli.
Desmond Hume
2

Na podstawie różnych odpowiedzi, ale głównie na tym , działa to, czego potrzebuję:

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

W wyniku tych linii będziesz miał piksel w formacie AARRGGBB z wartością alfa zawsze ustawioną na FF w 4-bajtowej liczbie całkowitej bez znaku pixel1.

cprcrack
źródło
1

Aby uzyskać dostęp do surowych wartości RGB obrazu UIImage w Swift 5, użyj bazowego CGImage i jego 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
źródło