3

I'm trying to darken UIImage by grabbing the CGImage, getting each pixel and subtracting 0xa from it, then saving each pixel to a new buffer. But when I try to load that buffer back as an image, the function [to create a CGImage] returns nil. This means I must have done something wrong (I wouldn't be surprised) in my code. I expect it has something to do with the buffer being improperly formatted or something. Can somebody familiar with Core Graphics help me spot the error?

var provider = CGImageGetDataProvider(imageArray[imageNumber]?.CGImage) //Get data provider for image in an array at index No. imageNumber
    let data = CGDataProviderCopyData(provider)
    var buffer = [Byte](count: CFDataGetLength(data), repeatedValue: 0) //create buffer for image data
    CFDataGetBytes(data, CFRangeMake(0, CFDataGetLength(data)), &buffer) //load the image's bytes into buffer
    var newBuffer = [Byte](count:buffer.count, repeatedValue: 0) //Going to make some changes, need a place to save new image
    var index = 0
    for aByte in buffer {
        if aByte > 0xa && aByte != 0xff {
       newBuffer[index] = (aByte - 0xa) //subtract 0xa from buffer, where possible
        }
        else{
            newBuffer[index] = (0xff) //I *think* there is no alpha channel, but every fourth byte in buffer is 0xff
        }
        index += 1
    }
    var coreGraphicsImage = CGImageCreateWithJPEGDataProvider(CGDataProviderCreateWithCFData( CFDataCreate(kCFAllocatorDefault, newBuffer, newBuffer.count)), nil, true, kCGRenderingIntentDefault) //create CGimage from newBuffer.RETURNS NIL!
    let myImage = UIImage(CGImage: coreGraphicsImage) //also nil
    imageView.image = myImage
Dharmesh Kheni
  • 71,228
  • 33
  • 160
  • 165
PopKernel
  • 4,110
  • 5
  • 29
  • 51
  • Rather than going pixel by pixel, create an image filled with the color (0xa), and subtract it from your original image. You can fill an image with a color using UIRectFill. Set the fill color using setFill() on a UIColor. Then, subtract - http://stackoverflow.com/questions/9690576/subtract-one-image-from-another-ios – Anna Dickinson Nov 01 '14 at 03:48

1 Answers1

4

A couple of thoughts:

  1. Before you go writing your own image processing routine, you might consider applying one of the Core Image filters. It might make your life much easier, and it may give you more refined results. Just reducing each channel by some fixed number will introduce distortion that you weren't expecting (e.g. color shifts, saturation changes, etc.).

  2. If you were going to do this, I'd be wary about just grabbing the data provider and manipulating it as is. You'd probably want to introduce all sorts of conditional logic for the nature of the image's provider (bits per channel, ARGB vs RGBA, etc.). If you look at Apple's example in Q&A #1509, you can instead retrieve a pixel buffer of a predetermined format (in their example ARGB, 8 bits per component, four bytes per pixel.

    This example is dated, but it shows how to create a context of a predetermined format and then draw an image into that context. You can then manipulate that data, and create a new image using this predetermined format using your own provider rather than the JPEG data provider.

  3. The most significant issue in your code sample is that you are trying to use CGImageCreateWithJPEGDataProvider, which expects a "data provider supplying JPEG-encoded data." But your provider probably isn't JPEG-encoded, so it's going to fail. If you're going to use the data in the format of the original image's provider, then you have to create a new image using CGImageCreate (manually supplying the width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, bitmapInfo, etc.).

  4. There are some less serious problems with your routine:

    • You note that you're seeing every fourth byte is a 0xff. In answer to your question in your code comments, that is undoubtedly the alpha channel. (You could confirm this by examining the CGBitmapInfo of the original CGImageRef.) You might not be using the alpha channel, but it's clearly there.

    • Your routine, if the channel's value was less than 0x0a, is setting it to 0xff. That's clearly not your intent (e.g. if the pixel was black, you'll make it white!). You should check that logic.

    • In my tests, this method of iterating/manipulating the Byte array was very slow. I'm not entirely sure why that is, but if you manipulate byte buffer directly, it is much faster.

So, below, please find routine that creates context of predetermined format (RGBA, 8 bits per component, etc), manipulates it (I'm converting to B&W though you can do whatever you want), and creates new image from that. So, in Swift 2:

func blackAndWhiteImage(image: UIImage) -> UIImage? {
    // get information about image

    let imageref = image.CGImage
    let width = CGImageGetWidth(imageref)
    let height = CGImageGetHeight(imageref)

    // create new bitmap context

    let bitsPerComponent = 8
    let bytesPerPixel = 4
    let bytesPerRow = width * bytesPerPixel
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = Pixel.bitmapInfo
    let context = CGBitmapContextCreate(nil, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo)

    // draw image to context

    let rect = CGRectMake(0, 0, CGFloat(width), CGFloat(height))
    CGContextDrawImage(context, rect, imageref)

    // manipulate binary data

    let pixels = UnsafeMutablePointer<Pixel>(CGBitmapContextGetData(context))

    for row in 0 ..< height {
        for col in 0 ..< width {
            let offset = Int(row * width + col)

            let red = Float(pixels[offset].red)
            let green = Float(pixels[offset].green)
            let blue = Float(pixels[offset].blue)
            let alpha = pixels[offset].alpha
            let luminance = UInt8(0.2126 * red + 0.7152 * green + 0.0722 * blue)
            pixels[offset] = Pixel(red: luminance, green: luminance, blue: luminance, alpha: alpha)
        }
    }

    // return the image

    let outputImage = CGBitmapContextCreateImage(context)!
    return UIImage(CGImage: outputImage, scale: image.scale, orientation: image.imageOrientation)
}

Where

struct Pixel: Equatable {
    private var rgba: UInt32

    var red: UInt8 {
        return UInt8((rgba >> 24) & 255)
    }

    var green: UInt8 {
        return UInt8((rgba >> 16) & 255)
    }

    var blue: UInt8 {
        return UInt8((rgba >> 8) & 255)
    }

    var alpha: UInt8 {
        return UInt8((rgba >> 0) & 255)
    }

    init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
        rgba = (UInt32(red) << 24) | (UInt32(green) << 16) | (UInt32(blue) << 8) | (UInt32(alpha) << 0)
    }

    static let bitmapInfo = CGImageAlphaInfo.PremultipliedLast.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue
}

func ==(lhs: Pixel, rhs: Pixel) -> Bool {
    return lhs.rgba == rhs.rgba
}

Or, in Swift 3:

func blackAndWhite(image: UIImage) -> UIImage? {
    // get information about image

    let imageref = image.cgImage!
    let width = imageref.width
    let height = imageref.height

    // create new bitmap context

    let bitsPerComponent = 8
    let bytesPerPixel = 4
    let bytesPerRow = width * bytesPerPixel
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = Pixel.bitmapInfo
    let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)!

    // draw image to context

    let rect = CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height))
    context.draw(imageref, in: rect)

    // manipulate binary data

    guard let buffer = context.data else {
        print("unable to get context data")
        return nil
    }

    let pixels = buffer.bindMemory(to: Pixel.self, capacity: width * height)

    for row in 0 ..< height {
        for col in 0 ..< width {
            let offset = Int(row * width + col)

            let red = Float(pixels[offset].red)
            let green = Float(pixels[offset].green)
            let blue = Float(pixels[offset].blue)
            let alpha = pixels[offset].alpha
            let luminance = UInt8(0.2126 * red + 0.7152 * green + 0.0722 * blue)
            pixels[offset] = Pixel(red: luminance, green: luminance, blue: luminance, alpha: alpha)
        }
    }

    // return the image

    let outputImage = context.makeImage()!
    return UIImage(cgImage: outputImage, scale: image.scale, orientation: image.imageOrientation)
}

struct Pixel: Equatable {
    private var rgba: UInt32

    var red: UInt8 {
        return UInt8((rgba >> 24) & 255)
    }

    var green: UInt8 {
        return UInt8((rgba >> 16) & 255)
    }

    var blue: UInt8 {
        return UInt8((rgba >> 8) & 255)
    }

    var alpha: UInt8 {
        return UInt8((rgba >> 0) & 255)
    }

    init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
        rgba = (UInt32(red) << 24) | (UInt32(green) << 16) | (UInt32(blue) << 8) | (UInt32(alpha) << 0)
    }

    static let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue

    static func ==(lhs: Pixel, rhs: Pixel) -> Bool {
        return lhs.rgba == rhs.rgba
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Your answer is great & appreciated, but I would like to note that this is more of an academic excercise in manipulating buffers, hex values and pixels-- I know the actual results would be distorted and that the function is inefficient-- it takes two minutes to run on my 2012 iMac! ;) That said, your answer is very useful, especially for when/if I spin this experiment off into an actual product. Right now though I am wondering why the creation of a CGImage returns nil? Is there some way I'm supposed to mark a buffer as image data, or maybe my control flow resulted in invalid hex values? – PopKernel Nov 01 '14 at 04:30
  • 2
    1. You're getting `nil` most likely because the provider you created doesn't contain "JPEG encoded data". 2. It's horribly slow because of something going on in `Array`: when I used `UnsafeMutablePointer` it was two orders of magnitude faster. See revised answer. – Rob Nov 01 '14 at 18:27