从UIImage(Cocoa Touch)或CGImage(Core Graphics)中获取像素数据的方法是什么?

205

我有一个UIImage(Cocoa Touch)。从那里,我很高兴获得CGImage或其他可用的内容。我想编写这个函数:

- (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;
}
11个回答

254

顺便说一下,我将Keremk的答案与我的原始提纲结合起来,修正了拼写错误,将其泛化为返回颜色数组并使整个东西编译通过。以下是结果:

+ (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
不要忘记取消预乘颜色。此外,那些乘以1的计算没有任何作用。 - Peter Hosey
4
为了读取一个像素,分配一个大内存空间太过繁琐。 - ragnarius
46
请使用calloc代替malloc!!! 我正在使用这段代码测试某些像素是否透明,但由于在使用前未清除内存,所以我得到了一些错误的信息。使用calloc(width*height, 4)代替malloc解决了这个问题。其余的代码很好,谢谢! - DonnaLea
6
太好了,谢谢。使用注意事项:x和y是图像中开始获取RGBA的坐标,“count”是从该点开始获取的像素数,从左到右逐行扫描。示例用法:获取整个图像的RGBAs:getRGBAsFromImage:image atX:0 andY:0 count:image.size.width*image.size.height获取图像最后一行的RGBAs:getRGBAsFromImage:image atX:0 andY:image.size.height-1 count:image.size.width - Elijah
2
在为 redgreenblue 赋值时,这些数字不在 0 到 1 之间。因此,使用这些值创建的 UIColor 几乎总是白色的。每个这些代码值都必须除以 255.0f,如下所示:UIColor *acolor = [UIColor colorWithRed:red/255.0f green:green/255.0f blue:blue/255.0f alpha:alpha]; - AykutE
显示剩余11条评论

48

一种方法是将图像绘制到由给定缓冲区支持的位图上下文中,以及为给定颜色空间(在此情况下为RGB)。请注意,这将复制图像数据到该缓冲区,因此您确实需要对其进行缓存,而不是每次需要获取像素值时都执行此操作。

以下是一个示例:

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

我在我的应用程序中做了类似的事情。要提取任意像素,您只需使用已知位图格式绘制到1x1位图中,并相应地调整CGBitmapContext的原点。 - Mark Bessey
5
这会导致内存泄漏。释放原始数据:free(rawData); - Adam Waite
6
“CGContextDrawImage”需要三个参数,但只传入了两个…我认为应该是CGContextDrawImage(context, CGRectMake(0, 0, width, height), image) - user1135469

27

苹果公司的技术问答 QA1509展示了以下简单方法:

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

使用CFDataGetBytePtr获取实际字节(并使用各种CGImageGet*方法来理解如何解释它们)。


10
这不是一个好方法,因为每张图片的像素格式往往会有很大的差异。有几个因素可能会发生改变,包括1. 图像的方向 2. alpha组件的格式和3. RGB字节顺序。我个人花了一些时间来解码苹果关于如何做到这一点的文档,但我不确定它是否值得。如果你想快速完成,只需使用keremk的解决方案。 - Tyler
1
这个方法总是给我BGR格式,即使我保存的图片(从我的应用程序中使用颜色空间RGBA) - Soumyajit
1
@Soumyaljit 这将以最初存储CGImageRef数据的格式复制数据。在大多数情况下,这将是BGRA,但也可能是YUV。 - Cameron Lowell Palmer

23

难以置信这里没有一个正确的答案。不需要分配指针,而且未乘值仍需标准化。为了简化问题,这是Swift 4的正确版本。对于UIImage,只需使用.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)
        }
    }
}

你需要先将图像绘制/转换到缓冲区的原因是因为图像可以有多种不同的格式。这一步骤是必需的,以将其转换为您可以读取的一致格式。


我该如何使用这个扩展来处理我的图片呢? 让我们创建一个名为imageObj的UIImage对象,它将加载名为“greencolorImg.png”的图像文件。 - Sagar Sukode
let imageObj = UIImage(named:"greencolorImg.png"); imageObj.cgImage?.colors(at: [pt1, pt2]) - user187676
请注意,pt1pt2CGPoint变量。 - Anjan Biswas
挽救了我的一天 :) - Bibek
1
@ErikAigner 如果我想获取图像每个像素的颜色,这个方法可行吗?性能成本如何? - Ameet Dhas
显示剩余2条评论

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

我发布了这个代码,它对我有效,但是从缓冲区返回到UIImage时,我似乎还无法解决,有什么想法吗? - Nidal Fakhouri

10

Swift 5 版本

这里给出的答案要么过时,要么不正确,因为它们没有考虑以下几点:

  1. 图像的像素大小可能与通过 image.size.width/image.size.height 返回的点大小不同。
  2. 图像中像素组件可以使用各种布局,例如 BGRA、ABGR、ARGB 等,也可能根本没有 alpha 组件,例如 BGR 和 RGB。例如,UIView.drawHierarchy(in:afterScreenUpdates:) 方法可以生成 BGRA 图像。
  3. 颜色分量可以对所有像素的 alpha 进行预乘,并且需要除以 alpha 以恢复原始颜色。
  4. 用于 CGImage 的内存优化,字节中的像素行大小可以大于像素宽度乘以 4 的简单乘法。

下面的代码提供了一个通用的 Swift 5 解决方案,适用于所有这些特殊情况下获取像素的 UIColor 的问题。该代码针对可用性和清晰度进行了优化,而非性能。

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
    }

}

方向的变化怎么办?您不需要检查UIImage.Orientation吗? - endavid
此外,您的 componentLayout 缺少只有 alpha 的情况,即 .alphaOnly - endavid
@endavid,“仅支持Alpha”不受支持(因为这是非常罕见的情况)。图像方向超出了此代码的范围,但有很多资源可以了解如何规范化UIImage的方向,在开始读取其像素颜色之前需要这样做。 - Desmond Hume

9

这里是一个StackOverflow的讨论帖(链接),其中@Matt通过将图像位移,使所需像素与上下文中的一个像素对齐,从而仅在1x1上下文中呈现所需像素。


7

UIImage是一个包装器,字节可以是CGImage或CIImage

根据苹果关于UIImage的参考文献,该对象是不可变的,并且您无法访问其后备字节。虽然如果您使用CGImage(显式或隐式)填充了UIImage,则可以访问CGImage数据,但如果UIImageCIImage支持,则会返回NULL,反之亦然。

图像对象不提供直接访问其底层图像数据的功能。但是,您可以检索其他格式的图像数据以在应用程序中使用。具体来说,您可以使用cgImageciImage属性分别检索与Core Graphics和Core Image兼容的图像版本。您还可以使用UIImagePNGRepresentation(:)UIImageJPEGRepresentation(_:)函数生成包含PNG或JPEG格式的图像数据的NSData对象。

解决此问题的常见技巧

如上所述,您的选项为:

  • UIImagePNGRepresentation或JPEG
  • 确定图像是否具有CGImage或CIImage后备数据,并在那里获取它

如果您想要输出不是ARGB、PNG或JPEG数据且数据尚未由CIImage支持,则这些技巧都不是特别好的选择。

我的建议:尝试CIImage

在开发项目时,避免使用UIImage可能更有意义,并选择其他内容。作为Obj-C图像包装器的UIImage通常由CGImage支持,以至于我们认为这是理所当然的。CIImage往往是更好的包装格式,因为您可以使用CIContext获取所需的格式,而无需知道它是如何创建的。在您的情况下,获取位图将是调用

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

作为额外的奖励,您可以开始对图像进行漂亮的操作,通过链接滤镜到图像上来解决许多问题,例如倒置的图像或需要旋转/缩放等。


7

在Olie和Algal的回答基础上,这里是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
}

{{链接1:Swift}}


4

要在Swift 5中访问UIImage的原始RGB值,请使用底层的CGImage及其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/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接