当前位置: 首页 > 知识库问答 >
问题:

如何在Swift中归一化UIImage的像素值?

潘星阑
2023-03-14

我们正试图将UIImage规范化,以便将其正确传递到CoreML模型中。

我们从每个像素检索RGB值的方法是,首先初始化一个称为每个像素值的[CGFloat]数组rawData,这样就有了红色、绿色、蓝色和alpha值的位置。在bitmapInfo中,我们从原始UIimage本身获取原始像素值,并执行以下操作。这用于填充context中的bitmapInfo参数,这是一个CGContext变量。我们稍后将使用context变量来drawaCGImage,这将在稍后将规范化的CGImage转换回UIImage

使用嵌套的for循环在xy坐标中迭代,可以找到所有像素中所有颜色(通过CGFloat的原始数据数组找到)的最小和最大像素颜色值。绑定变量被设置为终止for循环,否则,它将出现超出范围的错误。

range表示可能的RGB值的范围(即最大颜色值和最小颜色值之间的差异)。

使用公式对每个像素值进行规格化:

A = Image
curPixel = current pixel (R,G, B or Alpha) 
NormalizedPixel = (curPixel-minPixel(A))/range

和一个类似的设计嵌套的循环从上面解析通过数组的rawData和修改每个像素的颜色根据这个归一化。

我们的大多数代码来自:

  1. UIImage到UIColor像素颜色数组
  2. 更改UIImage中某些像素的颜色
  3. https://gist.github.com/pimpapare/e8187d82a3976b851fc12fe4f8965789

我们使用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中的图像处理,我们不确定整个归一化过程是否正确。

共有1个答案

慕容成文
2023-03-14

以下是几点观察:

>

  • 您的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

    我对此进行了基准测试,它在苹果XS Max上比浮点渲染快一点,但占用了四分之一的内存(例如,一个2000×2000px的图像在UInt8下需要16mb,但在Float下需要64mb)。

    最后,我应该提到vImage有一个高度优化的函数,VimageControlStretch_argb888,它的功能与我们上面所做的非常相似。只需导入加速,然后您就可以执行以下操作:

    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)
        }
    }
    

    虽然这使用了一个稍微不同的算法,但值得考虑,因为在我的基准测试中,在我的苹果XS Max上,它的速度是浮点再现的5倍多。

    一些不相关的观察结果:

    >

    我使用的不是UIImage宽度和高度,而是CGImage中的值。这是一个重要的区别,以防你的图像可能没有1的比例。

    例如,如果范围已经是0到255(即不需要标准化),您可能需要考虑早期退出。

  •  类似资料:
    • 问题内容: 我正在尝试使用Swift来获取UIImage中像素的颜色,但它似乎总是返回0。这是从@Minas 对此线程的回答中转换的代码: 提前致谢! 问题答案: 由于遇到了类似的问题,一些搜索将我引到了这里。您的代码工作正常。该问题可能是由您的图像引起的。 码: 发生的是此方法将从图像的CGImage中选择像素颜色。因此,请确保您选择的是正确的图像。例如,如果您的UIImage是200x200,

    • 问题内容: 我一直在尝试找出如何在Swift中将rgb像素数据数组转换为UIImage。 我将每个像素的rgb数据保持在一个简单的结构中: 我已经实现了以下功能,但是生成的图像不正确: 关于如何正确地将rgb数组转换为UIImage的任何提示或指针? 问题答案: 注意: 这是iOS创建的解决方案。有关macOS和的解决方案,请参见此答案。 唯一的问题是结构中的数据类型需要为。我在Playgroun

    • 问题内容: 我在Swift中编程。我想使用CALayer和UIImage遮罩图像。我正在以编程方式创建蒙版图像。创建的蒙版图像是UIImage,当我单独查看它时可以正常工作。但是,当我将其用作遮罩时,整个屏幕会变成白色。我怀疑我的问题出在配置CALayer对象。多谢您的协助。谢谢! 问题答案: 不幸的是,您提出的问题非常糟糕- 您尚未说出您实际上在试图做什么!但是,看起来好像您正在尝试使用蒙版在图

    • 问题内容: 对于给定的多色PNG (具有透明度),什么是最好的/快速惯用的方法: 创建一个副本 在副本中找到所有黑色像素并将其更改为红色 (返回修改后的副本) 关于SO有一些相关的问题,但我一直无法找到可行的方法。 问题答案: 您必须提取图像的像素缓冲区,然后可以循环浏览,并根据需要更改像素。最后,从缓冲区创建一个新图像。 在Swift 3中,这看起来像:

    • 问题内容: 我正在尝试保存到,然后在Swift中读回新的内容。要转换到我使用下面的代码: 如何将(即)转换回新的? 问题答案: 假设图像的比例为1。 在Swift 4.2中,将以下代码用于get Data()。

    • 问题内容: 在ObjectiveC中,我会这样做 但是我在Swift中尝试了所有这样的选择,但都没有成功 它显示一个错误: 使用未解析的标识符“ AlwaysOriginal” 我怎么做? 问题答案: 那将是正确的语法: (对于 Swift 3.x 或 Swift 4 ) (对于 Swift 2.x ) 但您也可以使用此“快捷方式”: