使用 CGContext 缩放到小尺寸时,CGImage 的像素数据不完整

Posted

技术标签:

【中文标题】使用 CGContext 缩放到小尺寸时,CGImage 的像素数据不完整【英文标题】:Pixel data of CGImage is incomplete when scaled with CGContext to small size 【发布时间】:2018-03-23 16:52:35 【问题描述】:

我在大小为 512x512 的透明背景上有以下白色圆圈的 png 图像:

在 Stack Overflow 上看不到这里,所以这里又出现了,以黑色背景的 UIImageView 显示:

我正在尝试使用高质量插值和抗锯齿将图像缩小到不同的尺寸,然后读取像素信息并继续处理。

缩放是使用常规的CGContext

private func scaleImageAntialiased(_ cgImage: CGImage, _ size: CGSize) -> CGImage 

    UIGraphicsBeginImageContext(size)
    guard let context = UIGraphicsGetCurrentContext() else  fatalError() 

    context.interpolationQuality = .high
    context.setShouldAntialias(true)

    context.draw(cgImage, in: CGRect(origin: .zero, size: size))

    guard let image = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else  fatalError() 
    return image

然后我使用dataProviderCFDataGetBytePtr读取像素信息:

var image = UIImage(named: "Round_512")!.cgImage!

let width: size_t = image.width
let height: size_t = image.height

let cfData = image.dataProvider!.data!
let dataPtr = CFDataGetBytePtr(cfData)
let bytesPerPixel = 4
var data: [GLubyte] = Array(UnsafeBufferPointer(start: dataPtr, count: width * height * bytesPerPixel))

在我尝试将新尺寸设置为 4x4px 或更低之前,一切正常。

let size = 4
let newSize = CGSize(width: size, height: size)
image = scaleImageAntialiased(image, newSize) 

虽然缩放后的图像仍然显示在UIImageView 内部,但您会期望它

原始 RGBA 字节数据缺少两行信息:

[3, 3, 3, 3,   75, 75, 75, 75,   74, 74, 74, 74,   3, 3, 3, 3, 
 0, 0, 0, 0,     0, 0, 0, 0,       0, 0, 0, 0,     0, 0, 0, 0, 
 75, 75, 75, 75, 254, 254, 254, 254, 254, 254, 254, 254, 74, 74, 74, 74, 
 0, 0, 0, 0,     0, 0, 0, 0,       0, 0, 0, 0,     0, 0, 0, 0]

(我对print(data) 的上述结果进行了格式化,以使其更加明显)。如您所见,第二行和第四行字节返回 0,而不是与其他两行具有相同的值(对称)。

只有当我首先使用CGContext 缩放图像然后尝试使用上述方法读取它时,才会出现此问题。如果我拍摄缩放后的图像,将其保存到文件中,然后从该 4x4 文件中读取像素信息(使用相同的方法),我会得到预期的结果。

所以缩放本身是有效的。像素的读取也有效。只有组合没有。

谁能解释为什么会这样?

为了完整起见,这里有一个 512px 图像的 download link 和一个完整的 Playground 文件,您可以使用它来重现该问题:

import UIKit
import GLKit
import PlaygroundSupport

private func scaleImageAntialiased(_ cgImage: CGImage, _ size: CGSize) -> CGImage 

    UIGraphicsBeginImageContext(size)
    guard let context = UIGraphicsGetCurrentContext() else  fatalError() 

    context.interpolationQuality = .high
    context.setShouldAntialias(true)

    context.draw(cgImage, in: CGRect(origin: .zero, size: size))

    guard let image = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else  fatalError() 
    return image


var image = UIImage(named: "Round_512")!.cgImage!

let size = 4
let newSize = CGSize(width: size, height: size)
image = scaleImageAntialiased(image, newSize)

let width: size_t = image.width
let height: size_t = image.height

let cfData = image.dataProvider!.data!
let dataPtr = CFDataGetBytePtr(cfData)
let bytesPerPixel = 4
var data: [GLubyte] = Array(UnsafeBufferPointer(start: dataPtr, count: width * height * bytesPerPixel))

print(data)

let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
imageView.image = UIImage(cgImage: image)
imageView.backgroundColor = .black

PlaygroundPage.current.liveView = imageView

// Save the test image
let path = playgroundSharedDataDirectory.appendingPathComponent("Saved.png")
let pngData = UIImagePNGRepresentation(imageView.image!)
//try! pngData?.write(to: path)

【问题讨论】:

【参考方案1】:

我相信CGImage 使用 32 bytesPerRow 的倍数...如果您只有 4 个像素,每个像素由 4 个字节表示,则每行的数据将被“填充”(填充的数据被忽略)。

如果您检查图像:

print(image.bytesPerRow)

您会看到它打印的是 32 而不是 16

你想使用:

let bytesPerRow = image.bytesPerRow
var data: [GLubyte] = Array(UnsafeBufferPointer(start: dataPtr, count: height * bytesPerRow))

所以height * bytesPerRow 而不是width * height * bytesPerPixel

【讨论】:

以上是关于使用 CGContext 缩放到小尺寸时,CGImage 的像素数据不完整的主要内容,如果未能解决你的问题,请参考以下文章

使用对象检测api的默认配置时,不同尺寸的图像缩放器有啥影响

不同屏幕尺寸的 iOS 缩放约束

使用 SpriteKit 缩放 SkSpriteNodes

Unity RectTransform Scale Handler - 如何在Runtime运行时拖动缩放窗口尺寸

使用 CGContext 绘制后擦除

iphone 6 和 6 plus 尺寸缩放