从CGImage获取像素格式

14

我相当了解位图布局和像素格式方面的知识,但在使用通过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格式按规范是大端序的,而实际上字节在数据中的排列方式与位图信息告诉我的相反。

有人知道发生了什么吗?由于png文件能够正确显示,系统肯定以某种方式知道如何处理这个问题。是否有一种可靠的方法来检测CGImage的像素格式?完整的演示项目可在GitHub上找到。


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

但是相同图像的位图信息给出了错误的字节序信息,导致位图被处理成BGRA像素格式,而不是实际的RGBA。当我处理它时,结果看起来像这样:

如果显式指定RGBA像素格式,则得到的图像演示了红色和蓝色像素之间的颜色交换,但我需要自动化这种检测。


P. P. S. 文档简要提到CGColorSpace是另一个定义像素格式 / 字节顺序的重要变量,但我没有找到如何从中获取它的任何信息。


图片绘制不正确吗?如果它绘制正确,那么可以安全地假设图像加载器正在将字节交换到主机顺序。 - rob mayoff
Rob已经用一个例子更新了问题。图像显示正确,AppKit不知何故知道字节顺序为big endian(RGB),但位图信息表明它是little endian(BGR)。加载程序不会改变字节顺序,但是图像信息表明字节已经被交换。 - Ian Bytchek
"相同图像的位图信息给我错误的字节序信息" 你是说由原始PNG或JPG文件创建的苹果CGImage类没有给出正确的大/小端字节序信息吗?还是说在你将缓冲区复制到其他库对象后创建图像之后出现了不正确的情况?如果是第二种情况,我建议你检查一下这个库的源代码...可能在处理数据时出了问题,或者也许你需要显式地传递字节序给库(毕竟仅凭字节无法确定字节序是否正确)。 - Mikael
上面的代码显示了发生了什么-CGBitmapInfo给出了错误的字节序。该库是FFmpeg,我检查了所有地方并将问题缩小到了CoreGraphics。目前我所做的就是-显式提供字节顺序,但这并不能保证它对于所有用例都是正确的,即不同的图像数据源使用不同的字节顺序。还添加了一个链接到演示存储库。 - Ian Bytchek
我有同样的问题。尝试将图像转换为WebP时,某些照片的蓝色/红色被交换了,CGBitmapInfo始终为5 =( 你找到解决方案了吗? - Dmytro Rostopira
请参见下面的答案。 - Ian Bytchek
3个回答

18

几年后,在生产环境中测试我的发现后,我可以有信心地分享它们,但希望有理论知识的人能在这里更好地解释一下?刷新记忆的好地方:

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

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)的内容。

为了在实践中查看此内容,请将上述内容和下面的内容放入playground中。 您将需要具有alpha通道和没有alpha通道的两个一像素红色图像,这个这个 应该可以使用。

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

你能使用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)
}

我对字节序感兴趣,而不是字母顺序。自从处理CGImage以来就没有检查过CIImage,但这可能是一种替代方案。我会研究一下并回复。谢谢! - Ian Bytchek
祝你好运,不知道你是否会从CIImage得到不同的结果,但是NSBitmapFormat枚举有一些大/小端情况。 - Austin
不行,这完全没用——无法创建NSBitmapImageRep,也无法从NSImage中获取第一个,两种情况下NSBitmapFormat都是0,没有包含任何字节序信息。我正在与苹果支持讨论此问题,将发布更多信息。 - Ian Bytchek
别让赏金白白浪费了。我会额外再奖励100美元,只因为你来自怀俄明州! - Ian Bytchek
@IanBytchek,你找到解决方案了吗?我也在尝试从CGImage中找到位图格式。请告诉我。 - Sully Chen
显示剩余2条评论

0

看一下如何防止NSBitmapImageRep创建大量中间CGImages?的答案。

要点是,NSImage/NSBitmapImageRepresentation实现会自动处理输入格式。

苹果的文档没有注意到格式参数(例如在CIRenderDestination中)指定所需的输出空间。

如果你想得到特定格式的话,文档建议绘制成那个格式(已经在链接的回答中给出了一个例子)。

如果你只需要特定信息,NSBitmapImageRepresentation提供了方便地访问单个参数。我找不到一个明确和直接的路径到CIFormat,而不是设置级联手动测试。我假设某个方法存在于某个地方。


感谢您对此事的跟进。关于“绘制与复制”的问题,这是一个很好的提示,在苹果的生态系统内工作时非常有效。但是,在将数据复制到另一个非苹果图形处理框架中时,有时确实需要原始数据,并且需要知道它的格式。在这种情况下,您希望避免调整数据以使其符合“预期”格式,因为这将是实时操作中非常昂贵的操作。 - Ian Bytchek

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