从灰度矩阵创建 CGImage/UIImage

Creating CGImage/UIImage from grayscale matrix

我有一个灰度图像像素矩阵,例如:

[ [0, 0, 125],
  [10, 50, 255],
  [90, 0, 255] ]

我的目标是对其应用色调 (UIColor) 并从包含它的结构中导出 CGImage/UIImage

public typealias Pixel = UInt8

extension UIColor {
    var red: Float { return Float(CIColor(color: self).red * 255) }
    var green: Float { return Float(CIColor(color: self).green * 255) }
    var blue: Float { return Float(CIColor(color: self).blue * 255) }
    var alpha: Float { return Float(CIColor(color: self).alpha * 255) }
}

public struct PixelData {
    let r: UInt8
    let g: UInt8
    let b: UInt8
    let a: UInt8
}

public struct Map {
    let pixelCount: UInt
    let pixels: [Pixel] //all pixels of an image, linear
    let dimension: UInt //square root of pixel count
    let tintColor: UIColor = UIColor(red: 9/255, green: 133/255, blue: 61/255, alpha: 1)

    public var image: UIImage? {
        var pixelsData = [PixelData]()
        pixelsData.reserveCapacity(Int(pixelCount) * 3)
        let alpha = UInt8(tintColor.alpha)
        let redValue = tintColor.red
        let greenValue = tintColor.green
        let blueValue = tintColor.blue
        let red: [PixelData] = pixels.map {
            let redInt: UInt8 = UInt8((Float([=12=]) / 255.0) * redValue)
            return PixelData(r: redInt, g: 0, b: 0, a: alpha)
        }
        let green: [PixelData] = pixels.map {
            let greenInt: UInt8 = UInt8((Float([=12=]) / 255.0) * greenValue)
            return PixelData(r: 0, g: greenInt, b: 0, a: alpha) }
        let blue: [PixelData] = pixels.map {
            let blueInt: UInt8 = UInt8((Float([=12=]) / 255.0) * blueValue)
            return PixelData(r: 0, g: 0, b: blueInt, a: alpha) }
        pixelsData.append(contentsOf: red)
        pixelsData.append(contentsOf: green)
        pixelsData.append(contentsOf: blue)
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
        let bitsPerComponent = 8
        let bitsPerPixel = 32
        let dimension: Int = Int(self.dimension)
        var data = pixelsData
        guard let providerRef = CGDataProvider(
            data: NSData(bytes: &data, length: data.count * MemoryLayout<PixelData>.size)
            ) else { return nil }
        if let cgim = CGImage(
            width: dimension,
            height: dimension,
            bitsPerComponent: bitsPerComponent,
            bitsPerPixel: bitsPerPixel,
            bytesPerRow: dimension * MemoryLayout<PixelData>.size,
            space: rgbColorSpace,
            bitmapInfo: bitmapInfo,
            provider: providerRef,
            decode: nil,
            shouldInterpolate: true,
            intent: .defaultIntent
            ) {
            return UIImage(cgImage: cgim)
        }
        return nil
    }
}

问题是输出看起来乱码。我用过 this tutorial and 但没有成功。操场上的结果是:

(输出在那里,只是勉强可见)

感谢任何帮助!

有两个关键问题。

  1. 代码正在计算每个灰度像素的所有红色值并为每个创建四个字节 PixelData(即使只填充了红色通道)并将其添加到 pixelsData数组。然后它对绿色值重复该操作,然后对蓝色值重复该操作。结果是图像所需数据的三倍,而且只使用了红色通道数据。

    相反,我们应该计算一次 RGBA 值,为每个值创建一个 PixelData,然后逐个像素地重复这个。

  2. premultipliedFirst表示ARGB。但是你的结构使用的是 RGBA,所以你想要 premultipliedLast.

因此:

func generateTintedImage(completion: @escaping (UIImage?) -> Void) {
    DispatchQueue.global(qos: .userInitiated).async {
        let image = self.tintedImage()
        DispatchQueue.main.async {
            completion(image)
        }
    }
}

private func tintedImage() -> UIImage? {
    let tintRed = tintColor.red
    let tintGreen = tintColor.green
    let tintBlue = tintColor.blue
    let tintAlpha = tintColor.alpha

    let data = pixels.map { pixel -> PixelData in
        let red = UInt8((Float(pixel) / 255) * tintRed)
        let green = UInt8((Float(pixel) / 255) * tintGreen)
        let blue = UInt8((Float(pixel) / 255) * tintBlue)
        let alpha = UInt8(tintAlpha)
        return PixelData(r: red, g: green, b: blue, a: alpha)
    }.withUnsafeBytes { Data([=10=]) }

    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
    let bitsPerComponent = 8
    let bitsPerPixel = 32

    guard
        let providerRef = CGDataProvider(data: data as CFData),
        let cgImage = CGImage(width: width,
                              height: height,
                              bitsPerComponent: bitsPerComponent,
                              bitsPerPixel: bitsPerPixel,
                              bytesPerRow: width * MemoryLayout<PixelData>.stride,
                              space: rgbColorSpace,
                              bitmapInfo: bitmapInfo,
                              provider: providerRef,
                              decode: nil,
                              shouldInterpolate: true,
                              intent: .defaultIntent)
    else {
        return nil
    }

    return UIImage(cgImage: cgImage)
}

我还重命名了一些变量,使用 stride 而不是 size,将 dimension 替换为 widthheight 以便我可以处理non-square 图片等

我还建议不要将计算的 属性 用于这种计算密集型的任何事情,因此我给了它一个异步方法,您可以按如下方式使用它:

let map = Map(with: image)
map.generateTintedImage { image in
    self.tintedImageView.image = image
}

无论如何,上面的结果如下,其中最右边的图像是您的着色图像:


不用说,要将矩阵转换为 pixels 数组,只需展平数组的数组即可:

let matrix: [[Pixel]] = [
    [0, 0, 125],
    [10, 50, 255],
    [90, 0, 255]
]
pixels = matrix.flatMap { [=12=] }

这是一个并行化的再现,它在内存缓冲区方面也稍微更有效:

private func tintedImage() -> UIImage? {
    let tintAlpha = tintColor.alpha
    let tintRed = tintColor.red / 255
    let tintGreen = tintColor.green / 255
    let tintBlue = tintColor.blue / 255

    let alpha = UInt8(tintAlpha)

    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
    let bitsPerComponent = 8
    let bytesPerRow = width * MemoryLayout<PixelData>.stride

    guard
        let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
        let data = context.data
    else {
        return nil
    }

    let buffer = data.bindMemory(to: PixelData.self, capacity: width * height)

    DispatchQueue.concurrentPerform(iterations: height) { row in
        let start = width * row
        let end = start + width
        for i in start ..< end {
            let pixel = pixels[i]
            let red = UInt8(Float(pixel) * tintRed)
            let green = UInt8(Float(pixel) * tintGreen)
            let blue = UInt8(Float(pixel) * tintBlue)
            buffer[i] = PixelData(r: red, g: green, b: blue, a: alpha)
        }
    }

    return context.makeImage()
        .flatMap { UIImage(cgImage: [=13=]) }
}