Bagaimana cara mendapatkan data piksel dari UIImage (Cocoa Touch) atau CGImage (Core Graphics)?

200

Saya memiliki UIImage (Cocoa Touch). Dari itu, saya senang mendapatkan CGImage atau apa pun yang Anda inginkan tersedia. Saya ingin menulis fungsi ini:

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

Jawaban:

249

FYI, saya menggabungkan jawaban Keremk dengan garis besar asli saya, membersihkan kesalahan ketik, menggeneralisasikannya untuk mengembalikan serangkaian warna dan mengumpulkan semuanya untuk dikompilasi. Inilah hasilnya:

+ (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
sumber
4
Jangan lupa untuk warna-warnanya. Juga, perkalian-perkalian tersebut dengan 1 tidak melakukan apa-apa.
Peter Hosey
2
Bahkan jika itu benar. Ketika menghitung adalah angka yang besar (seperti panjang deretan gambar atau seluruh ukuran gambar!) Saya tidak melakukan kinerja metode ini dengan baik karena memiliki terlalu banyak objek UIColor sangat berat. Dalam kehidupan nyata ketika saya membutuhkan pixel maka UIColor ok, (NSArray dari UIColors terlalu banyak untuk saya). Dan ketika Anda membutuhkan banyak warna saya tidak akan menggunakan UIColors karena saya mungkin akan menggunakan OpenGL, dll. Menurut pendapat saya metode ini tidak memiliki penggunaan kehidupan nyata tetapi sangat baik untuk tujuan pendidikan.
nacho4d
4
Malloc terlalu rumit ruang yang besar hanya untuk membaca satu piksel.
ragnarius
46
GUNAKAN CALLOC BUKAN MALLOC !!!! Saya menggunakan kode ini untuk menguji apakah piksel tertentu transparan dan saya mendapatkan kembali informasi palsu di mana piksel dimaksudkan untuk menjadi transparan karena memori tidak dibersihkan terlebih dahulu. Menggunakan calloc (lebar * tinggi, 4) alih-alih malloc yang melakukan trik. Sisa kodenya bagus, TERIMA KASIH!
DonnaLea
5
Ini bagus. Terima kasih. Catatan tentang penggunaan: x dan y adalah koordinat dalam gambar untuk mulai mendapatkan RGBA dari dan 'menghitung' adalah jumlah piksel dari titik itu untuk mendapatkan, pergi ke kiri ke kanan, lalu baris demi baris. Contoh Penggunaan: Untuk mendapatkan RGBA untuk seluruh gambar: getRGBAsFromImage:image atX:0 andY:0 count:image.size.width*image.size.height Untuk mendapatkan RGBA untuk baris terakhir gambar: getRGBAsFromImage:image atX:0 andY:image.size.height-1 count:image.size.width
Harris
49

Salah satu cara melakukannya adalah dengan menggambar gambar ke konteks bitmap yang didukung oleh buffer yang diberikan untuk ruang warna yang diberikan (dalam hal ini adalah RGB): (perhatikan bahwa ini akan menyalin data gambar ke buffer itu, jadi Anda lakukan ingin menyimpannya alih-alih melakukan operasi ini setiap kali Anda perlu mendapatkan nilai piksel)

Lihat di bawah sebagai sampel:

// 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
sumber
Saya melakukan sesuatu yang serupa di aplikasi saya. Untuk mengekstrak piksel yang berubah-ubah, Anda cukup menggambar dalam bitmap 1x1 dengan format bitmap yang dikenal, menyesuaikan asal-usul CGBitmapContext dengan tepat.
Mark Bessey
5
Memori ini bocor. Rilis rawData: gratis (rawData);
Adam Waite
5
CGContextDrawImagemembutuhkan tiga argumen dan di atas hanya dua yang diberikan ... Saya pikir seharusnyaCGContextDrawImage(context, CGRectMake(0, 0, width, height), image)
user1135469
25

Tanya Jawab Teknis Apple QA1509 menunjukkan pendekatan sederhana berikut:

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

Gunakan CFDataGetBytePtruntuk sampai ke byte aktual (dan berbagai CGImageGet*metode untuk memahami bagaimana menafsirkannya).

yakovlev
sumber
10
Ini bukan pendekatan yang bagus karena format piksel cenderung banyak bervariasi per gambar. Ada beberapa hal yang dapat berubah, termasuk 1. orientasi gambar 2. format komponen alfa dan 3. urutan byte RGB. Saya pribadi menghabiskan waktu mencoba memecahkan kode dokumen Apple tentang cara melakukan ini, tetapi saya tidak yakin itu sepadan. Jika Anda ingin itu dilakukan dengan cepat, hanya solusi keremk.
Tyler
1
Metode ini selalu memberi saya format BGR bahkan jika saya menyimpan gambar (dari dalam aplikasi saya di colorspace RGBA)
Soumyajit
1
@Soumyaljit Ini akan menyalin data dalam format penyimpanan data CGImageRef. Dalam kebanyakan kasus ini akan menjadi BGRA, tetapi bisa juga YUV.
Cameron Lowell Palmer
18

Tidak percaya bahwa tidak ada satu jawaban yang benar di sini. Tidak perlu mengalokasikan pointer, dan nilai-nilai unmultiplied masih perlu dinormalisasi. Untuk memotong ke pengejaran, berikut adalah versi yang benar untuk Swift 4. Untuk UIImagedigunakan saja .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)
        }
    }
}

Alasan Anda harus menggambar / mengubah gambar terlebih dahulu menjadi buffer adalah karena gambar dapat memiliki beberapa format berbeda. Langkah ini diperlukan untuk mengubahnya menjadi format yang konsisten yang dapat Anda baca.

Erik Aigner
sumber
Bagaimana saya bisa menggunakan ekstensi ini untuk gambar saya? biarkan imageObj = UIImage (bernama: "greencolorImg.png")
Sagar Sukode
let imageObj = UIImage(named:"greencolorImg.png"); imageObj.cgImage?.colors(at: [pt1, pt2])
Erik Aigner
perlu diingat bahwa pt1dan pt2adalah CGPointvariabel.
AnBisw
menyelamatkan hari saya :)
Dari
1
@ErikAigner apakah ini akan berfungsi jika saya ingin mendapatkan warna dari setiap piksel gambar? berapa biaya kinerjanya?
Ameet Dhas
9

Berikut adalah utas SO di mana @Matt hanya merender piksel yang diinginkan ke dalam konteks 1x1 dengan menggeser gambar sehingga piksel yang diinginkan sejajar dengan satu piksel dalam konteks.

eddy
sumber
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
sumber
Saya memposting ini dan itu berhasil untuk saya, tetapi beralih dari buffer kembali ke UIImage yang sepertinya belum saya ketahui, ada ide?
Nidal Fakhouri
8

UIImage adalah pembungkus byte yang merupakan CGImage atau CIImage

Menurut Referensi Apple pada UIImage objek tidak dapat diubah dan Anda tidak memiliki akses ke backing byte. Meskipun benar bahwa Anda dapat mengakses data CGImage jika Anda mengisi UIImagedengan CGImage(secara eksplisit atau implisit), itu akan kembali NULLjika UIImagedidukung oleh a CIImagedan sebaliknya.

Objek gambar tidak menyediakan akses langsung ke data gambar yang mendasarinya. Namun, Anda dapat mengambil data gambar dalam format lain untuk digunakan di aplikasi Anda. Khususnya, Anda dapat menggunakan properti cgImage dan ciImage untuk mengambil versi gambar yang masing-masing kompatibel dengan Core Graphics dan Core Image. Anda juga dapat menggunakan fungsi UIImagePNGRepresentation ( :) dan UIImageJPEGRepresentation ( : _ :) untuk menghasilkan objek NSData yang berisi data gambar dalam format PNG atau JPEG.

Trik umum untuk mengatasi masalah ini

Seperti yang disebutkan pilihan Anda

  • UIImagePNGRepresentation atau JPEG
  • Menentukan apakah gambar memiliki data dukungan CGImage atau CIImage dan mendapatkannya di sana

Tak satu pun dari ini adalah trik yang sangat baik jika Anda ingin output yang bukan data ARGB, PNG, atau JPEG dan data belum didukung oleh CIImage.

Rekomendasi saya, coba CIImage

Saat mengembangkan proyek Anda, mungkin lebih masuk akal bagi Anda untuk menghindari UIImage sama sekali dan memilih sesuatu yang lain. UIImage, sebagai pembungkus gambar Obj-C, sering didukung oleh CGImage ke titik di mana kami menerima begitu saja. CIImage cenderung menjadi format pembungkus yang lebih baik karena Anda dapat menggunakan konteks CIC untuk mengeluarkan format yang Anda inginkan tanpa perlu tahu bagaimana itu dibuat. Dalam kasus Anda, mendapatkan bitmap adalah masalah menelepon

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

Sebagai bonus tambahan, Anda dapat mulai melakukan manipulasi gambar dengan merantai filter ke gambar. Ini memecahkan banyak masalah di mana gambar terbalik atau perlu diputar / diskalakan dll.

Cameron Lowell Palmer
sumber
6

Berdasarkan jawaban Olie dan Algal, berikut adalah jawaban terbaru untuk 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
sumber
3

Versi Swift 5

Jawaban yang diberikan di sini sudah kedaluwarsa atau salah karena mereka tidak memperhitungkan yang berikut ini:

  1. Ukuran piksel gambar dapat berbeda dari ukuran titiknya yang dikembalikan oleh image.size.width/image.size.height .
  2. Mungkin ada berbagai tata letak yang digunakan oleh komponen piksel dalam gambar, seperti BGRA, ABGR, ARGB, dll. Atau mungkin tidak memiliki komponen alfa sama sekali, seperti BGR dan RGB. Misalnya, UIView.drawHierarchy(in:afterScreenUpdates:)metode dapat menghasilkan gambar BGRA.
  3. Komponen warna dapat di-prapultiplied oleh alpha untuk semua piksel dalam gambar dan perlu dibagi dengan alpha untuk mengembalikan warna asli.
  4. Untuk optimasi memori yang digunakan oleh CGImage, ukuran baris piksel dalam byte bisa lebih besar dari sekadar perkalian lebar piksel dengan 4.

Kode di bawah ini adalah untuk menyediakan solusi Swift 5 universal untuk mendapatkan UIColorpixel untuk semua kasus khusus tersebut. Kode dioptimalkan untuk kegunaan dan kejelasan, bukan untuk kinerja.

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
sumber
Bagaimana dengan perubahan orientasi? Apakah kamu tidak perlu memeriksa UIImage. Orientasi juga?
endavid
Anda componentLayoutjuga kehilangan kasing ketika hanya ada alpha .alphaOnly,.
endavid
@endavid "Alpha only" tidak didukung (karena sangat jarang terjadi). Orientasi gambar berada di luar cakupan kode ini, tetapi ada banyak sumber daya tentang cara menormalkan orientasi a UIImage, yang akan Anda lakukan sebelum mulai membaca warna pikselnya.
Desmond Hume
2

Berdasarkan jawaban yang berbeda tetapi terutama pada ini , ini bekerja untuk apa yang saya butuhkan:

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

Sebagai hasil dari baris ini, Anda akan memiliki pixel dalam format AARRGGBB dengan alpha selalu diatur ke FF dalam integer unsigned 4 byte pixel1.

cprcrack
sumber
1

Untuk mengakses nilai RGB mentah dari UIImage di Swift 5 gunakan CGImage yang mendasarinya dan dataProvider-nya:

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
sumber