【问题标题】:How to normalize pixel values of an UIImage in Swift?如何在 Swift 中标准化 UIImage 的像素值?
【发布时间】:2019-08-21 07:40:54
【问题描述】:

我们正在尝试对 UIImage 进行规范化,以便它可以正确地传递到 CoreML 模型中。

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

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

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

使用方程对每个像素值进行归一化:

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

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

我们的大部分代码来自:

  1. UIImage to UIColor array of pixel colors
  2. Change color of certain pixels in a 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 中的图像处理,我们不确定整个标准化过程是否正确。任何帮助将不胜感激!

【问题讨论】:

  • minPixel 不应该是整个 rawData 数组中的最小值吗?还是只是当前像素之前的像素?它应该是所有 4 个通道中的最小值吗?还是取决于渠道?
  • @ielyamani - 不,他只想缩放颜色通道,而不是 alpha 值。例如,如果颜色的范围是 31-63(UInt8),但 alpha 为 255,他仍然希望根据最大值 63 而不是 255 来缩放颜色。
  • @Rob rawData[byteIndex+3] = ... 不更新 alpha 通道吗?
  • @ielyamani - 确实如此,但我确信这是一个错误。如果他真的想标准化颜色和 alpha 值,他会希望为每个值保持一个单独的最小和最大范围。但我敢打赌,他真正关心的只是颜色范围。

标签: swift image-processing uiimage normalization pixel


【解决方案1】:

几个观察:

  1. 您的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) }
    }
    
  2. 但这引出了一个问题,即您为什么要为浮点数据烦恼。如果您将此浮点数据返回到您的 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 上比浮点再现要快一点,但占用内存的四分之一(例如,2000×2000px 的图像在 UInt8 时占用 16mb,但在 @987654333 时占用 64mb @)。

  3. 最后,我应该提到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 倍以上。


一些不相关的观察:

  1. 您的代码 sn-p 也在规范化 Alpha 通道。我不确定你是否愿意这样做。通常颜色和 alpha 通道是独立的。上面我假设您真的只想标准化颜色通道。如果您也想标准化 alpha 通道,那么您可能有一个单独的 alpha 通道的最小-最大值范围并单独处理。但是,用与颜色通道相同的值范围(反之亦然)对 Alpha 通道进行归一化并没有多大意义。

  2. 我没有使用 UIImage 的宽度和高度,而是使用来自 CGImage 的值。这是重要的区别,以防您的图像可能没有 1 的比例。

  3. 如果范围已经是 0-255(即不需要标准化),您可能需要考虑提前退出。

【讨论】:

  • guard range &gt; 0 else { return nil } 中返回 nil 可能太强:图像可能是一种纯灰色。我觉得这里没有必要返回可选图像。
  • @ielyamani - 如果图像是纯灰色(或范围为零的任何纯色),那么是否应该将其归一化为零?到255?对于其他一些神奇的哨兵价值?恕我直言,标准化颜色值范围的整个想法对于纯色单色图像没有意义。在上面我可能会考虑其他优化,但我个人认为让调用者知道它不能被规范化是有用的(例如,如果它加载了一些资产,知道它不需要保存可能很有用标准化图像作为新资产)。
  • 也许这是主观的,但如果是纯色,我会选择返回原始图像。无论如何,这取决于 OP 以及他们对 规范化 图像的期望。
猜你喜欢
  • 2021-10-07
  • 2012-05-15
  • 1970-01-01
  • 1970-01-01
  • 2023-03-24
  • 1970-01-01
  • 2020-05-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多