从CGImage获取像素格式

时间:2016-09-04 19:01:48

标签: swift bitmap core-graphics endianness

我理解位图布局和像素格式主题相当不错,但在处理通过NSImage加载的png / jpeg图像时遇到问题 - 我无法弄清楚我得到的是预期的行为还是一个错误。

let nsImage:NSImage = NSImage(byReferencingURL: …)
let cgImage:CGImage = nsImage.CGImageForProposedRect(nil, context: nil, hints: nil)!
let bitmapInfo:CGBitmapInfo = CGImageGetBitmapInfo(cgImage)
Swift.print(bitmapInfo.contains(CGBitmapInfo.ByteOrderDefault)) // True

我的kCGBitmapByteOrder32Host是小端,这意味着像素格式也是小端 - 在这种情况下是BGRA。但是...... png格式是规范的大端,这是字节在数据中的实际排列方式 - 与位图信息告诉我的方式相反。

有人知道发生了什么吗?当然,系统知道如何处理这个问题,因为pngs正确显示。是否有检测CGImage像素格式的防弹方式?完整的demo project可在GitHub上找到。

P上。 S。我将原始像素数据通过CFDataGetBytePtr缓冲区复制到另一个库缓冲区,然后进行处理和保存。为此,我需要明确指定像素格式。我正在处理的实际图像(我已经检查过的任何png / jpeg文件)显示正确,例如:

但是相同图像的位图信息给出了错误的字节序信息,导致位图被处理为BGRA像素格式而不是实际的RGBA,当我处理它时,结果如下所示:

生成的图像演示了红色和蓝色像素之间的颜色交换,如果明确指定RGBA像素格式,一切都很完美,但我需要将此检测自动化。

P上。 P. S. 文档简要提到CGColorSpace是另一个定义像素格式/字节顺序的重要变量,但我没有提到如何将它从那里拿出来。

2 个答案:

答案 0 :(得分:4)

几年后,在测试我的生产结果后,我可以很自信地分享它们,但是希望有理论知识的人能在这里更好地解释一下吗?刷新记忆的好地方:

基于此,您可以使用以下扩展程序:

public enum PixelFormat
{
    case abgr
    case argb
    case bgra
    case rgba
}

extension CGBitmapInfo
{
    public static var byteOrder16Host: CGBitmapInfo {
        return CFByteOrderGetCurrent() == Int(CFByteOrderLittleEndian.rawValue) ? .byteOrder16Little : .byteOrder16Big
    }

    public static var byteOrder32Host: CGBitmapInfo {
        return CFByteOrderGetCurrent() == Int(CFByteOrderLittleEndian.rawValue) ? .byteOrder32Little : .byteOrder32Big
    }
}

extension CGBitmapInfo
{
    public var pixelFormat: PixelFormat? {

        // AlphaFirst – the alpha channel is next to the red channel, argb and bgra are both alpha first formats.
        // AlphaLast – the alpha channel is next to the blue channel, rgba and abgr are both alpha last formats.
        // LittleEndian – blue comes before red, bgra and abgr are little endian formats.
        // Little endian ordered pixels are BGR (BGRX, XBGR, BGRA, ABGR, BGR).
        // BigEndian – red comes before blue, argb and rgba are big endian formats.
        // Big endian ordered pixels are RGB (XRGB, RGBX, ARGB, RGBA, RGB).

        let alphaInfo: CGImageAlphaInfo? = CGImageAlphaInfo(rawValue: self.rawValue & type(of: self).alphaInfoMask.rawValue)
        let alphaFirst: Bool = alphaInfo == .premultipliedFirst || alphaInfo == .first || alphaInfo == .noneSkipFirst
        let alphaLast: Bool = alphaInfo == .premultipliedLast || alphaInfo == .last || alphaInfo == .noneSkipLast
        let endianLittle: Bool = self.contains(.byteOrder32Little)

        // This is slippery… while byte order host returns little endian, default bytes are stored in big endian
        // format. Here we just assume if no byte order is given, then simple RGB is used, aka big endian, though…

        if alphaFirst && endianLittle {
            return .bgra
        } else if alphaFirst {
            return .argb
        } else if alphaLast && endianLittle {
            return .abgr
        } else if alphaLast {
            return .rgba
        } else {
            return nil
        }
    }
}

请注意,始终注意色彩空间 - 它会直接影响原始像素数据的存储方式。 CGColorSpace(name: CGColorSpace.sRGB)可能是最安全的 - 它以普通格式存储颜色,例如,如果处理红色RGB,它将被存储就像那样(255,0,0),而设备颜色空间会给你类似的东西(235,73,53)。

要在实践中看到这一点,请将上方和下方放入游乐场。您需要两个带有alpha且不带alpha的单像素红色图像,thisthis应该有效。

import AppKit
import CoreGraphics

extension CFData
{
    public var pixelComponents: [UInt8] {
        let buffer: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer.allocate(capacity: 4)
        defer { buffer.deallocate(capacity: 4) }
        CFDataGetBytes(self, CFRange(location: 0, length: CFDataGetLength(self)), buffer)
        return Array(UnsafeBufferPointer(start: buffer, count: 4))
    }
}

let color: NSColor = .red
Thread.sleep(forTimeInterval: 2)

// Must flip coordinates to capture what we want…
let screen: NSScreen = NSScreen.screens.first(where: { $0.frame.contains(NSEvent.mouseLocation) })!
let rect: CGRect = CGRect(origin: CGPoint(x: NSEvent.mouseLocation.x - 10, y: screen.frame.height - NSEvent.mouseLocation.y), size: CGSize(width: 1, height: 1))

Swift.print("Will capture image with \(rect) frame.")

let screenImage: CGImage = CGWindowListCreateImage(rect, [], kCGNullWindowID, [])!
let urlImageWithAlpha: CGImage = NSImage(byReferencing: URL(fileURLWithPath: "/Users/ianbytchek/Downloads/red-pixel-with-alpha.png")).cgImage(forProposedRect: nil, context: nil, hints: nil)!
let urlImageNoAlpha: CGImage = NSImage(byReferencing: URL(fileURLWithPath: "/Users/ianbytchek/Downloads/red-pixel-no-alpha.png")).cgImage(forProposedRect: nil, context: nil, hints: nil)!

Swift.print(screenImage.colorSpace!, screenImage.bitmapInfo, screenImage.bitmapInfo.pixelFormat!, screenImage.dataProvider!.data!.pixelComponents)
Swift.print(urlImageWithAlpha.colorSpace!, urlImageWithAlpha.bitmapInfo, urlImageWithAlpha.bitmapInfo.pixelFormat!, urlImageWithAlpha.dataProvider!.data!.pixelComponents)
Swift.print(urlImageNoAlpha.colorSpace!, urlImageNoAlpha.bitmapInfo, urlImageNoAlpha.bitmapInfo.pixelFormat!, urlImageNoAlpha.dataProvider!.data!.pixelComponents)

let formats: [CGBitmapInfo.RawValue] = [
    CGImageAlphaInfo.premultipliedFirst.rawValue,
    CGImageAlphaInfo.noneSkipFirst.rawValue,
    CGImageAlphaInfo.premultipliedLast.rawValue,
    CGImageAlphaInfo.noneSkipLast.rawValue,
]

for format in formats {

    // This "paints" and prints out components in the order they are stored in data.

    let context: CGContext = CGContext(data: nil, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 32, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: format)!
    let components: UnsafeBufferPointer<UInt8> = UnsafeBufferPointer(start: context.data!.assumingMemoryBound(to: UInt8.self), count: 4)

    context.setFillColor(red: 1 / 0xFF, green: 2 / 0xFF, blue: 3 / 0xFF, alpha: 1)
    context.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
    Swift.print(context.colorSpace!, context.bitmapInfo, context.bitmapInfo.pixelFormat!, Array(components))
}

这将输出以下内容。注意屏幕捕获的图像与从磁盘加载的图像有何不同。

Will capture image with (285.7734375, 294.5, 1.0, 1.0) frame.
<CGColorSpace 0x7fde4e9103e0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; iMac) CGBitmapInfo(rawValue: 8194) bgra [27, 13, 252, 255]
<CGColorSpace 0x7fde4d703b20> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; Color LCD) CGBitmapInfo(rawValue: 3) rgba [235, 73, 53, 255]
<CGColorSpace 0x7fde4e915dc0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; Color LCD) CGBitmapInfo(rawValue: 5) rgba [235, 73, 53, 255]
<CGColorSpace 0x7fde4d60d390> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1) CGBitmapInfo(rawValue: 2) argb [255, 1, 2, 3]
<CGColorSpace 0x7fde4d60d390> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1) CGBitmapInfo(rawValue: 6) argb [255, 1, 2, 3]
<CGColorSpace 0x7fde4d60d390> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1) CGBitmapInfo(rawValue: 1) rgba [1, 2, 3, 255]
<CGColorSpace 0x7fde4d60d390> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1) CGBitmapInfo(rawValue: 5) rgba [1, 2, 3, 255]

Playground

答案 1 :(得分:1)

你能使用NSBitmapFormat吗?

我写了一个类来从图像中获取颜色方案,这就是我用来确定位图格式的方法。这是我如何使用它的片段:

var averageColorImage: CIImage?
var averageColorImageBitmap: NSBitmapImageRep

//... core image filter code

averageColorImage = filter?.outputImage

averageColorImageBitmap = NSBitmapImageRep(CIImage: averageColorImage!)

let red, green, blue: Int
switch averageColorImageBitmap.bitmapFormat {

    case NSBitmapFormat.NSAlphaFirstBitmapFormat:
        red = Int(averageColorImageBitmap.bitmapData.advancedBy(1).memory)
        green = Int(averageColorImageBitmap.bitmapData.advancedBy(2).memory)
        blue = Int(averageColorImageBitmap.bitmapData.advancedBy(3).memory)
    default:
        red = Int(averageColorImageBitmap.bitmapData.memory)
        green = Int(averageColorImageBitmap.bitmapData.advancedBy(1).memory)
        blue = Int(averageColorImageBitmap.bitmapData.advancedBy(2).memory)
}