Создание CGImage / UIImage из матрицы оттенков серого - PullRequest
1 голос
/ 03 июня 2019

У меня есть матрица пикселей изображения в оттенках серого, например:

[ [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($0) / 255.0) * redValue)
            return PixelData(r: redInt, g: 0, b: 0, a: alpha)
        }
        let green: [PixelData] = pixels.map {
            let greenInt: UInt8 = UInt8((Float($0) / 255.0) * greenValue)
            return PixelData(r: 0, g: greenInt, b: 0, a: alpha) }
        let blue: [PixelData] = pixels.map {
            let blueInt: UInt8 = UInt8((Float($0) / 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
    }
}

Проблема в том, что вывод выглядит бессмысленным. Я использовал этот учебник и этот SO поток , но безуспешно. Результат на детской площадке:

enter image description here

(вывод есть, едва видимый)

Любая помощь приветствуется!

1 Ответ

1 голос
/ 03 июня 2019

Есть два ключевых вопроса.

  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($0) }

    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 на width и height, чтобы я мог обрабатывать неквадратные изображения и т. Д.

Я бы также советовал не использовать вычисляемое свойство для чего-либо такого сложного в вычислительном отношении, поэтому я дал этому асинхронный метод, который вы могли бы использовать следующим образом:

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

В любом случае, приведенное выше дает следующее, где самым правым изображением является ваше тонированное изображение:

enter image description here


Излишне говорить, что для преобразования вашей матрицы в массив pixels вы можете просто сгладить массив массивов:

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

Вот распараллеленное воспроизведение, которое также немного более эффективно по отношению к буферу памяти:

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: $0) }
}
...