Search code examples
swiftuikitcore-graphics

How to reconstruct grayscale image from intensity values?


It is commonly required to get the pixel data from an image or reconstruct that image from pixel data. How can I take an image, convert it to an array of pixel values and then reconstruct it using the pixel array in Swift using CoreGraphics?

The quality of the answers to this question have been all over the place so I'd like a canonical answer.


Solution

  • Get pixel values as an array

    This function can easily be extended to a color image. For simplicity I'm using grayscale, but I have commented the changes to get RGB.

    func pixelValuesFromImage(imageRef: CGImage?) -> (pixelValues: [UInt8]?, width: Int, height: Int)
    {
        var width = 0
        var height = 0
        var pixelValues: [UInt8]?
        if let imageRef = imageRef {
            let totalBytes = imageRef.width * imageRef.height
            let colorSpace = CGColorSpaceCreateDeviceGray()
            
            pixelValues = [UInt8](repeating: 0, count: totalBytes)
            pixelValues?.withUnsafeMutableBytes({
                width = imageRef.width
                height = imageRef.height
                let contextRef = CGContext(data: $0.baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: colorSpace, bitmapInfo: 0)
                let drawRect = CGRect(x: 0.0, y:0.0, width: CGFloat(width), height: CGFloat(height))
                contextRef?.draw(imageRef, in: drawRect)
            })
        }
    
        return (pixelValues, width, height)
    }
    

    Get image from pixel values

    I reconstruct an image, in this case grayscale 8-bits per pixel, back into a CGImage.

    func imageFromPixelValues(pixelValues: [UInt8]?, width: Int, height: Int) ->  CGImage?
    {
        var imageRef: CGImage?
        if let pixelValues = pixelValues {
            let bitsPerComponent = 8
            let bytesPerPixel = 1
            let bitsPerPixel = bytesPerPixel * bitsPerComponent
            let bytesPerRow = bytesPerPixel * width
            let totalBytes = width * height
            let unusedCallback: CGDataProviderReleaseDataCallback = { optionalPointer, pointer, valueInt in }
            let providerRef = CGDataProvider(dataInfo: nil, data: pixelValues, size: totalBytes, releaseData: unusedCallback)
    
            let bitmapInfo: CGBitmapInfo = [CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue), CGBitmapInfo(rawValue: CGImageByteOrderInfo.orderDefault.rawValue)]
            imageRef = CGImage(width: width,
                               height: height,
                               bitsPerComponent: bitsPerComponent,
                               bitsPerPixel: bitsPerPixel,
                               bytesPerRow: bytesPerRow,
                               space: CGColorSpaceCreateDeviceGray(),
                               bitmapInfo: bitmapInfo,
                               provider: providerRef!,
                               decode: nil,
                               shouldInterpolate: false,
                               intent: .defaultIntent)
        }
    
        return imageRef
    }
        
    

    Demoing the code in a Playground

    You'll need an image copied into the Playground's Resources folder and then change the filename and extension below to match. The result on the last line is a UIImage constructed from the CGImage.

    import Foundation
    import CoreGraphics
    import UIKit
    import PlaygroundSupport
    
    let URL = playgroundSharedDataDirectory.appendingPathComponent("zebra.jpg")
    print("URL \(URL)")
    
    var image: UIImage? = nil
    if FileManager().fileExists(atPath: URL.path) {
        do {
            try NSData(contentsOf: URL, options: .mappedIfSafe)
        } catch let error as NSError {
            print ("Error: \(error.localizedDescription)")
        }
        image = UIImage(contentsOfFile: URL.path)
    } else {
        print("File not found")
    }
    
    let (intensityValues, width, height) = pixelValuesFromImage(imageRef: image?.cgImage)
    let roundTrippedImage = imageFromPixelValues(pixelValues: intensityValues, width: width, height: height)
    let zebra = UIImage(cgImage: roundTrippedImage!)