2

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.

Cameron Lowell Palmer
  • 21,528
  • 7
  • 125
  • 126

2 Answers2

5

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!)
Cameron Lowell Palmer
  • 21,528
  • 7
  • 125
  • 126
  • `image` is undefined in the first function, that does not compile. And why not use optional binding instead of forced unwrapping (compare http://stackoverflow.com/questions/29717210/when-should-i-compare-an-optional-value-to-nil)? Also alloc() can fail. – Martin R Jan 08 '16 at 13:09
  • @MartinR I did some cleanup to avoid the explicit nil checks – Cameron Lowell Palmer Mar 03 '16 at 19:21
0

I was having trouble getting Cameron's code above to work, so I wanted to test another method. I found Vacawama's code, which relies on ARGB pixels. You can use that solution and convert each grayscale value to an ARGB value by simply mapping on each value:

/// Assuming grayscale pixels contains floats in the range 0...1
let grayscalePixels: [Float] = ...
let pixels = grayscalePixels.map { 
  let intensity = UInt8(round($0 / Float(UInt8.max)))
  return PixelData(a: UInt8.max, r: intensity, g: intensity, b: intensity)
}
let image = UIImage(pixels: pixels, width: width, height: height)
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • Updated for current Swift - the purpose of the original code was to deal with raw intensity values of B&W images as might be found in Matlab data and was absolutely not meant to operate with ARGB pixel data. Furthermore, pixel data representation is complicated and dealing with strict intensity values and allowing CoreGraphics to manage the complexity is absolutely a smart choice. – Cameron Lowell Palmer Dec 12 '20 at 08:42