[英]How to normalize pixel values of an UIImage in Swift?
我们正在尝试规范化UIImage
以便可以将其正确传递到CoreML模型中。
我们从每个像素中检索RGB值的方法是,首先为每个像素初始化一个名为rawData
的[CGFloat]
数组,以获取每个像素的值,以便为红色,绿色,蓝色和alpha值定位。 在bitmapInfo
,我们从原始UIimage本身获取原始像素值并进行操作。 这用于填充context
的CGContext
变量中的bitmapInfo
参数。 稍后,我们将使用context
变量来draw
CGImage
,然后将其标准化后的CGImage
转换回UIImage
。
使用通过x
和y
坐标进行嵌套的for循环迭代,可以找到所有像素中所有颜色(通过CGFloat
的原始数据数组找到)中的最小和最大像素颜色值。 将绑定变量设置为终止for循环,否则它将出现超出范围的错误。
range
指示可能的RGB值的范围(即最大颜色值和最小颜色值之间的差)。
使用公式对每个像素值进行归一化:
A = Image
curPixel = current pixel (R,G, B or Alpha)
NormalizedPixel = (curPixel-minPixel(A))/range
以及从上方嵌套的for循环类似设计,可通过rawData
数组进行解析,并根据此归一化修改每个像素的颜色。
我们的大多数代码来自:
我们使用CGFloat
代替UInt8
因为归一化的像素值应该是介于0和1之间的实数,而不是0或1。
func normalize() -> UIImage?{
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let cgImage = cgImage else {
return nil
}
let width = Int(size.width)
let height = Int(size.height)
var rawData = [CGFloat](repeating: 0, count: width * height * 4)
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width
let bytesPerComponent = 8
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue & CGBitmapInfo.alphaInfoMask.rawValue
let context = CGContext(data: &rawData,
width: width,
height: height,
bitsPerComponent: bytesPerComponent,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: bitmapInfo)
let drawingRect = CGRect(origin: .zero, size: CGSize(width: width, height: height))
context?.draw(cgImage, in: drawingRect)
let bound = rawData.count
//find minimum and maximum
var minPixel: CGFloat = 1.0
var maxPixel: CGFloat = 0.0
for x in 0..<width {
for y in 0..<height {
let byteIndex = (bytesPerRow * x) + y * bytesPerPixel
if(byteIndex > bound - 4){
break
}
minPixel = min(CGFloat(rawData[byteIndex]), minPixel)
minPixel = min(CGFloat(rawData[byteIndex + 1]), minPixel)
minPixel = min(CGFloat(rawData[byteIndex + 2]), minPixel)
minPixel = min(CGFloat(rawData[byteIndex + 3]), minPixel)
maxPixel = max(CGFloat(rawData[byteIndex]), maxPixel)
maxPixel = max(CGFloat(rawData[byteIndex + 1]), maxPixel)
maxPixel = max(CGFloat(rawData[byteIndex + 2]), maxPixel)
maxPixel = max(CGFloat(rawData[byteIndex + 3]), maxPixel)
}
}
let range = maxPixel - minPixel
print("minPixel: \(minPixel)")
print("maxPixel : \(maxPixel)")
print("range: \(range)")
for x in 0..<width {
for y in 0..<height {
let byteIndex = (bytesPerRow * x) + y * bytesPerPixel
if(byteIndex > bound - 4){
break
}
rawData[byteIndex] = (CGFloat(rawData[byteIndex]) - minPixel) / range
rawData[byteIndex+1] = (CGFloat(rawData[byteIndex+1]) - minPixel) / range
rawData[byteIndex+2] = (CGFloat(rawData[byteIndex+2]) - minPixel) / range
rawData[byteIndex+3] = (CGFloat(rawData[byteIndex+3]) - minPixel) / range
}
}
let cgImage0 = context!.makeImage()
return UIImage.init(cgImage: cgImage0!)
}
归一化之前,我们希望像素值范围为0-255,归一化之后,我们希望像素值范围为0-1。
归一化公式能够将像素值归一化为介于0和1之间的值。但是,当我们尝试打印(在遍历像素值时只需添加打印语句)归一化之前的像素值,以验证我们获得的原始像素值正确无误,我们发现这些值的范围不正确。 例如,一个像素值的值为3.506e + 305(大于255)。我们认为我们在一开始就弄错了原始像素值。
我们不熟悉Swift中的图像处理,并且不确定整个规范化过程是否正确。 任何帮助,将不胜感激!
一些观察:
您的rawData
是浮点数CGFloat
数组,但是您的上下文中没有填充浮点数据,而是UInt8
数据。 如果需要浮点缓冲区,请使用CGBitmapInfo.floatComponents
构建浮点上下文,并相应地调整上下文参数。 例如:
func normalize() -> UIImage? { let colorSpace = CGColorSpaceCreateDeviceRGB() guard let cgImage = cgImage else { return nil } let width = cgImage.width let height = cgImage.height var rawData = [Float](repeating: 0, count: width * height * 4) let bytesPerPixel = 16 let bytesPerRow = bytesPerPixel * width let bitsPerComponent = 32 let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.floatComponents.rawValue | CGBitmapInfo.byteOrder32Little.rawValue guard let context = CGContext(data: &rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else { return nil } let drawingRect = CGRect(origin: .zero, size: CGSize(width: width, height: height)) context.draw(cgImage, in: drawingRect) var maxValue: Float = 0 var minValue: Float = 1 for pixel in 0 ..< width * height { let baseOffset = pixel * 4 for offset in baseOffset ..< baseOffset + 3 { let value = rawData[offset] if value > maxValue { maxValue = value } if value < minValue { minValue = value } } } let range = maxValue - minValue guard range > 0 else { return nil } for pixel in 0 ..< width * height { let baseOffset = pixel * 4 for offset in baseOffset ..< baseOffset + 3 { rawData[offset] = (rawData[offset] - minValue) / range } } return context.makeImage().map { UIImage(cgImage: $0, scale: scale, orientation: imageOrientation) } }
但这引出了一个问题,为什么您要打扰浮点数据。 如果将这个浮点数据返回到ML模型,那么我可以想象它可能会有用,但是您只是在创建一个新图像。 因此,您还必须有机会仅检索UInt8
数据,进行浮点运算,然后更新UInt8
缓冲区并从中创建图像。 从而:
func normalize() -> UIImage? { let colorSpace = CGColorSpaceCreateDeviceRGB() guard let cgImage = cgImage else { return nil } let width = cgImage.width let height = cgImage.height var rawData = [UInt8](repeating: 0, count: width * height * 4) let bytesPerPixel = 4 let bytesPerRow = bytesPerPixel * width let bitsPerComponent = 8 let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue guard let context = CGContext(data: &rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else { return nil } let drawingRect = CGRect(origin: .zero, size: CGSize(width: width, height: height)) context.draw(cgImage, in: drawingRect) var maxValue: UInt8 = 0 var minValue: UInt8 = 255 for pixel in 0 ..< width * height { let baseOffset = pixel * 4 for offset in baseOffset ..< baseOffset + 3 { let value = rawData[offset] if value > maxValue { maxValue = value } if value < minValue { minValue = value } } } let range = Float(maxValue - minValue) guard range > 0 else { return nil } for pixel in 0 ..< width * height { let baseOffset = pixel * 4 for offset in baseOffset ..< baseOffset + 3 { rawData[offset] = UInt8(Float(rawData[offset] - minValue) / range * 255) } } return context.makeImage().map { UIImage(cgImage: $0, scale: scale, orientation: imageOrientation) } }
我仅取决于您是否真的为ML模型需要此浮点缓冲区(在这种情况下,您可能在第一个示例中返回浮点数组,而不是创建新图像)还是目标只是创建规范化对象? UIImage
。
我对此进行了基准测试,它在iPhone XS Max上比浮点渲染快了一点,但占用了四分之一的内存(例如,使用UInt8拍摄2000×2000px图像需要UInt8
,而使用Float
需要64mb)。
最后,我应该提到vImage具有高度优化的功能,即vImageContrastStretch_ARGB8888
,其功能与我们之前所做的非常相似。 只需import Accelerate
,然后您可以执行以下操作:
func normalize3() -> UIImage? { let colorSpace = CGColorSpaceCreateDeviceRGB() guard let cgImage = cgImage else { return nil } var format = vImage_CGImageFormat(bitsPerComponent: UInt32(cgImage.bitsPerComponent), bitsPerPixel: UInt32(cgImage.bitsPerPixel), colorSpace: Unmanaged.passRetained(colorSpace), bitmapInfo: cgImage.bitmapInfo, version: 0, decode: nil, renderingIntent: cgImage.renderingIntent) var source = vImage_Buffer() var result = vImageBuffer_InitWithCGImage( &source, &format, nil, cgImage, vImage_Flags(kvImageNoFlags)) guard result == kvImageNoError else { return nil } defer { free(source.data) } var destination = vImage_Buffer() result = vImageBuffer_Init( &destination, vImagePixelCount(cgImage.height), vImagePixelCount(cgImage.width), 32, vImage_Flags(kvImageNoFlags)) guard result == kvImageNoError else { return nil } result = vImageContrastStretch_ARGB8888(&source, &destination, vImage_Flags(kvImageNoFlags)) guard result == kvImageNoError else { return nil } defer { free(destination.data) } return vImageCreateCGImageFromBuffer(&destination, &format, nil, nil, vImage_Flags(kvImageNoFlags), nil).map { UIImage(cgImage: $0.takeRetainedValue(), scale: scale, orientation: imageOrientation) } }
尽管此算法使用的算法略有不同,但值得考虑,因为在我的基准测试中,在我的iPhone XS Max上,它的速度是浮点表示法的5倍以上。
一些无关的发现:
您的代码段也在规范化alpha通道。 我不确定您是否要这样做。 通常,颜色和Alpha通道是独立的。 在上面,我假设您确实只想对颜色通道进行标准化。 如果您也想规范化Alpha通道,那么您可能对Alpha通道有一个单独的min-max值范围,并分别进行处理。 但是使用与色彩通道相同的值范围来标准化Alpha通道并没有多大意义(反之亦然)。
我不是使用UIImage
宽度和高度,而是使用CGImage
的值。 如果图像的比例尺可能不为1,这是重要的区别。
例如,如果范围已经为0-255(即无需标准化),则可能要考虑提前退出。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.