66

I'm trying to get the color of a pixel in a UIImage with Swift, but it seems to always return 0. Here is the code, translated from @Minas' answer on this thread:

func getPixelColor(pos: CGPoint) -> UIColor {
    var pixelData = CGDataProviderCopyData(CGImageGetDataProvider(self.CGImage))
    var data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

    var pixelInfo: Int = ((Int(self.size.width) * Int(pos.y)) + Int(pos.x)) * 4

    var r = CGFloat(data[pixelInfo])
    var g = CGFloat(data[pixelInfo+1])
    var b = CGFloat(data[pixelInfo+2])
    var a = CGFloat(data[pixelInfo+3])

    return UIColor(red: r, green: g, blue: b, alpha: a)
}

Thanks in advance!

Community
  • 1
  • 1
Zoyt
  • 4,809
  • 6
  • 33
  • 45
  • Am I mistaken or width must be a multiple of 4? I had wrong results when image width was NOT multiple of 4. I solved my problems to re-scale image (width) to nearest multiple of 4. – sabiland Jun 02 '17 at 14:59

15 Answers15

76

A bit of searching leads me here since I was facing the similar problem. You code works fine. The problem might be raised from your image.

Code:

  //On the top of your swift 
  extension UIImage {
      func getPixelColor(pos: CGPoint) -> UIColor {

          let pixelData = CGDataProviderCopyData(CGImageGetDataProvider(self.CGImage))
          let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

          let pixelInfo: Int = ((Int(self.size.width) * Int(pos.y)) + Int(pos.x)) * 4

          let r = CGFloat(data[pixelInfo]) / CGFloat(255.0)
          let g = CGFloat(data[pixelInfo+1]) / CGFloat(255.0)
          let b = CGFloat(data[pixelInfo+2]) / CGFloat(255.0)
          let a = CGFloat(data[pixelInfo+3]) / CGFloat(255.0)

          return UIColor(red: r, green: g, blue: b, alpha: a)
      }  
  }

What happens is this method will pick the pixel colour from the image's CGImage. So make sure you are picking from the right image. e.g. If you UIImage is 200x200, but the original image file from Imgaes.xcassets or wherever it came from, is 400x400, and you are picking point (100,100), you are actually picking the point on the upper left section of the image, instead of middle.

Two Solutions:
1, Use image from Imgaes.xcassets, and only put one @1x image in 1x field. Leave the @2x, @3x blank. Make sure you know the image size, and pick a point that is within the range.

//Make sure only 1x image is set
let image : UIImage = UIImage(named:"imageName") 
//Make sure point is within the image
let color : UIColor = image.getPixelColor(CGPointMake(xValue, yValue)) 

2, Scale you CGPoint up/down the proportion to match the UIImage. e.g. let point = CGPoint(100,100) in the example above,

let xCoordinate : Float = Float(point.x) * (400.0/200.0)
let yCoordinate : Float = Float(point.y) * (400.0/200.0) 
let newCoordinate : CGPoint = CGPointMake(CGFloat(xCoordinate), CGFloat(yCoordinate))
let image : UIImage = largeImage
let color : UIColor = image.getPixelColor(CGPointMake(xValue, yValue)) 

I've only tested the first method, and I am using it to get a colour off a colour palette. Both should work. Happy coding :)

Jack Song
  • 1,355
  • 13
  • 22
  • 1
    Nice. Thank you for this! I would suggest changing your vars to let since there is no need to mutate them. – dfmuir Oct 30 '15 at 03:13
  • Is there any guarantee that the data will be in the expected RGBA colour space? I can't find any definitive documentation. – macu Mar 08 '16 at 16:09
  • 3
    This solution was working for me, but then something changed and the data came in as ARGB instead of RGBA. Check out this link for more information and a resolution: http://stackoverflow.com/questions/34593706/why-do-i-get-the-wrong-color-of-a-pixel-with-following-code – dfmuir Mar 08 '16 at 21:22
  • 1
    As a side note for this. Make sure your image in `images.xcassets` is `.png` format. I tried using a `pdf` and was getting some real wonky results. – NSGangster Sep 30 '16 at 05:19
  • 1
    I'm capturing a snapshot of the screen of an iPhone6, then assigning the resulting image to an imageView(x:20,y:20,w:343,h:38). This code works well for y=0, but I can't make it work (i.e.) x=342,y=37. Im calling ...getPixelColot(CGPoint(x:342*2, y:37*2). I multiply *2 because the retina display, but it isn't working, any clues? – MLBDG Apr 01 '17 at 17:58
  • @JackSong Hi, sorry to bother you again, but just wondering, what coordinate system does the point of the UIImage work with? Is it UIKit - e.g. the point (0,0) is in the top left corner? Thanks! – J.Treutlein Jun 02 '18 at 11:33
  • @J.Treutlein it's been a while, can't remember exactly. From recollection, it was from top left, but can't be sure. Sorry mate. – Jack Song Jun 03 '18 at 14:00
  • I kept getting wrong results after processing the image using UIKit and wasted multiple hours trying to understand why. The solution I found is **converting the UIImage to a PNG before getting data** to make it consistent. `let correctedImage = UIImage(data: image.pngData()!)!` this line at the top of the function, and then using `correctedImage` instead of `image` throughout, does the trick. Feel free to correct the force unwrapping if you need! – Carl Aug 02 '19 at 15:25
  • I would be specific about the RGBA order, i was using it with a new API that was outputting image in BGRA, thanks. – Juan Boero Jan 09 '20 at 16:20
  • This shouldn't be the accepted answer it's wrong – Bret Jul 20 '22 at 21:09
35

SWIFT 3, XCODE 8 Tested and working

extension UIImage {
    func getPixelColor(pos: CGPoint) -> UIColor {

        let pixelData = self.cgImage!.dataProvider!.data
        let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

        let pixelInfo: Int = ((Int(self.size.width) * Int(pos.y)) + Int(pos.x)) * 4

        let r = CGFloat(data[pixelInfo]) / CGFloat(255.0)
        let g = CGFloat(data[pixelInfo+1]) / CGFloat(255.0)
        let b = CGFloat(data[pixelInfo+2]) / CGFloat(255.0)
        let a = CGFloat(data[pixelInfo+3]) / CGFloat(255.0)

        return UIColor(red: r, green: g, blue: b, alpha: a)
    }

}
Eugene
  • 10,006
  • 4
  • 37
  • 55
  • I had to modify this a bit, my image doesn't have a alpha channel, so I removed the alpha line and multiplied the pos.x by 3 instead of 4. Worked very well! – Politta Sep 05 '17 at 23:35
  • What is pos? What element is providing x and y values? –  Nov 23 '18 at 07:51
  • pos is the "position" you're requesting a color from. the CGPoint has x and y values. You would call this on a UIImage as "let color = myImage.getPixelColor(CGPoint(x:30,y:40))". – Kaolin Fire Nov 30 '18 at 05:13
  • ! can be wrong so easily ... and your whole app crashes. can I suggest returning UIColor? and changing `let pixelData = self.cgImage!.dataProvider!.data` to `guard let pixelData = self.cgImage?.dataProvider?.data else { return nil }` – Paul Stevenson Dec 12 '19 at 22:29
20

If you are calling the answered question more than once, than you should not use the function on every pixel, because you are recreating the same set of data. If you want all of the colors in an image, do something more like this:

func findColors(_ image: UIImage) -> [UIColor] {
    let pixelsWide = Int(image.size.width)
    let pixelsHigh = Int(image.size.height)

    guard let pixelData = image.cgImage?.dataProvider?.data else { return [] }
    let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

    var imageColors: [UIColor] = []
    for x in 0..<pixelsWide {
        for y in 0..<pixelsHigh {
            let point = CGPoint(x: x, y: y)
            let pixelInfo: Int = ((pixelsWide * Int(point.y)) + Int(point.x)) * 4
            let color = UIColor(red: CGFloat(data[pixelInfo]) / 255.0,
                                green: CGFloat(data[pixelInfo + 1]) / 255.0,
                                blue: CGFloat(data[pixelInfo + 2]) / 255.0,
                                alpha: CGFloat(data[pixelInfo + 3]) / 255.0)
            imageColors.append(color)
        }
    }
    return imageColors
}

Here is an Example Project

As a side note, this function is significantly faster than the accepted answer, but it gives a less defined result.. I just put the UIImageView in the sourceView parameter.

func getPixelColorAtPoint(point: CGPoint, sourceView: UIView) -> UIColor {
    let pixel = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: 4)
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
    let context = CGContext(data: pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)

    context!.translateBy(x: -point.x, y: -point.y)

    sourceView.layer.render(in: context!)
    let color: UIColor = UIColor(red: CGFloat(pixel[0])/255.0,
                                 green: CGFloat(pixel[1])/255.0,
                                 blue: CGFloat(pixel[2])/255.0,
                                 alpha: CGFloat(pixel[3])/255.0)
    pixel.deallocate(capacity: 4)
    return color
}
Sethmr
  • 3,046
  • 1
  • 24
  • 42
  • I am getting the RGB data of complete UIImage...thanks Sethmr. – Prashant Gaikwad Apr 26 '18 at 15:38
  • @Sethmr can you explain how does this work? Because I need colors on diagonal line (from topLeft to bottomRight), I have an array of CGPoints of that diagonal line. – Bhushan B Nov 29 '18 at 13:56
  • @Bhushan It has been a while, but basically, the first one iterates over the pixels in a UIImage while the second I got from somewhere else and am slightly unsure of what it does at this point. As far as having the points, if you multiply the x and y values of your CGPoint array by the UIScreen.main.scale, then you should be able to take that value and check at the top of the y loop with a *guard yourPointArray.contains(CGPoint(x: x, y: y) else { return }* to only add the colors you want to the array. – Sethmr Nov 29 '18 at 16:36
  • I am getting the following error `Thread 1: EXC_BAD_ACCESS (code=1, address=0x112aa6180)` on this line: `let color = UIColor(red: CGFloat(data[pixelInfo]) / 255.0, green: CGFloat(data[pixelInfo + 1]) / 255.0, blue: CGFloat(data[pixelInfo + 2]) / 255.0, alpha: CGFloat(data[pixelInfo + 3]) / 255.0)` . Would you know what would be causing this? – albertski Feb 08 '20 at 17:00
  • Thanks @Sethmr. When I run your exact project I get no issues. When I run your code in my project that reads images from the phone's album I get the same issue. Would you have any recommendations on what to look at? – albertski Feb 17 '20 at 22:29
  • When I print the data variable above `let color = "\(data[pixelInfo]).\(data[pixelInfo + 1]).\(data[pixelInfo + 2])"` I get `- pointerValue: 4643708928` in your code. From my code I get `0x000000011114f880`. Somehow data is not correct on my side. Can't seem to figure out why. – albertski Feb 17 '20 at 22:34
  • @albertski It is far too hard to guess this without seeing your code and looking deeper. – Sethmr Feb 19 '20 at 15:40
  • @Sethmr I figured out my problem. The issue was my PHAsset -> UIImage. Thanks again for sharing your code. – albertski Feb 21 '20 at 04:34
  • Thanks for this code, it was however failing for some images, so i improved the code using this. `let bytesPerPixel = cgImage.bitsPerPixel / 8` `let pixelInfo: Int = ((cgImage.bytesPerRow * Int(point.y)) + (Int(point.x) * bytesPerPixel))` – knig_T Feb 03 '21 at 08:41
9

I was getting swapped colors for red and blue. The original function also did not account for the actual bytes per row and bytes per pixel. I also avoid unwrapping optionals whenever possible. Here's an updated function.

import UIKit

extension UIImage {
    /// Get the pixel color at a point in the image
    func pixelColor(atLocation point: CGPoint) -> UIColor? {
        guard let cgImage = cgImage, let pixelData = cgImage.dataProvider?.data else { return nil }

        let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

        let bytesPerPixel = cgImage.bitsPerPixel / 8

        let pixelInfo: Int = ((cgImage.bytesPerRow * Int(point.y)) + (Int(point.x) * bytesPerPixel))

        let b = CGFloat(data[pixelInfo]) / CGFloat(255.0)
        let g = CGFloat(data[pixelInfo+1]) / CGFloat(255.0)
        let r = CGFloat(data[pixelInfo+2]) / CGFloat(255.0)
        let a = CGFloat(data[pixelInfo+3]) / CGFloat(255.0)

        return UIColor(red: r, green: g, blue: b, alpha: a)
    }
}
MJLyco
  • 274
  • 3
  • 4
  • 1
    bgr will be common, better (since you added ests for bitsPerPixel, bytesPerRow, etc) would be checking the color space and switching on that ( for instance, kCGColorSpaceDeviceRGB ) – Kaolin Fire Nov 30 '18 at 05:14
7

Swift3 (IOS 10.3)

Important: - This will works only for @1x image.

Request: -

if you have solution for @2x and @3x images please share. Thank you :)

extension UIImage {

    func getPixelColor(atLocation location: CGPoint, withFrameSize size: CGSize) -> UIColor {
        let x: CGFloat = (self.size.width) * location.x / size.width
        let y: CGFloat = (self.size.height) * location.y / size.height

        let pixelPoint: CGPoint = CGPoint(x: x, y: y)

        let pixelData = self.cgImage!.dataProvider!.data
        let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

        let pixelIndex: Int = ((Int(self.size.width) * Int(pixelPoint.y)) + Int(pixelPoint.x)) * 4

        let r = CGFloat(data[pixelIndex]) / CGFloat(255.0)
        let g = CGFloat(data[pixelIndex+1]) / CGFloat(255.0)
        let b = CGFloat(data[pixelIndex+2]) / CGFloat(255.0)
        let a = CGFloat(data[pixelIndex+3]) / CGFloat(255.0)

        return UIColor(red: r, green: g, blue: b, alpha: a)
    }

}

Usage

print(yourImageView.image!.getPixelColor(atLocation: location, withFrameSize: yourImageView.frame.size))

You can use tapGestureRecognizer for location.

ramchandra n
  • 1,957
  • 1
  • 15
  • 15
5

Your code works fine for me, as an extension to UIImage. How are your testing your colour? here's my example:

    let green = UIImage(named: "green.png")
    let topLeft = CGPoint(x: 0, y: 0)

    // Use your extension
    let greenColour = green.getPixelColor(topLeft)

    // Dump RGBA values
    var redval: CGFloat = 0
    var greenval: CGFloat = 0
    var blueval: CGFloat = 0
    var alphaval: CGFloat = 0
    greenColour.getRed(&redval, green: &greenval, blue: &blueval, alpha: &alphaval)
    println("Green is r: \(redval) g: \(greenval) b: \(blueval) a: \(alphaval)")

This prints:

    Green is r: 0.0 g: 1.0 b: 1.0 a: 1.0

...which is correct, given that my image is a solid green square.

(What do you mean by "it always seems to return 0"? You don't happen to be testing on a black pixel, do you?)

Matt Gibson
  • 37,886
  • 9
  • 99
  • 128
5

Im getting backwards colours in terms of R and B being swapped, not sure why this I thought the order was RGBA.

func testGeneratedColorImage() {

    let color = UIColor(red: 0.5, green: 0, blue: 1, alpha: 1)
    let size = CGSize(width: 10, height: 10)
    let image = UIImage.image(fromColor: color, size: size)

    XCTAssert(image.size == size)

    XCTAssertNotNil(image.cgImage)

    XCTAssertNotNil(image.cgImage!.dataProvider)

    let pixelData = image.cgImage!.dataProvider!.data
    let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

    let position = CGPoint(x: 1, y: 1)
    let pixelInfo: Int = ((Int(size.width) * Int(position.y)) + Int(position.x)) * 4

    let r = CGFloat(data[pixelInfo]) / CGFloat(255.0)
    let g = CGFloat(data[pixelInfo+1]) / CGFloat(255.0)
    let b = CGFloat(data[pixelInfo+2]) / CGFloat(255.0)
    let a = CGFloat(data[pixelInfo+3]) / CGFloat(255.0)

    let testColor = UIColor(red: r, green: g, blue: b, alpha: a)

    XCTAssert(testColor == color, "Colour: \(testColor) does not match: \(color)")
}

Where color looks like this: enter image description here

image looks like this: enter image description here

and testColor looks like: enter image description here

(I can understand that the blue value might be off a little bit and be 0.502 with floating point inaccuracy)

With the code switched to:

    let b = CGFloat(data[pixelInfo]) / CGFloat(255.0)
    let g = CGFloat(data[pixelInfo+1]) / CGFloat(255.0)
    let r = CGFloat(data[pixelInfo+2]) / CGFloat(255.0)
    let a = CGFloat(data[pixelInfo+3]) / CGFloat(255.0)

I get testColor as: enter image description here

richy
  • 2,716
  • 1
  • 33
  • 42
4

I think you need to divide each component by 255:

var r = CGFloat(data[pixelInfo]) / CGFloat(255.0)
var g = CGFloat(data[pixelInfo + 1]) / CGFloat(255.0)
var b = CGFloat(data[pixelInfo + 2]) / CGFloat(255.0)
var a = CGFloat(data[pixelInfo + 3]) / CGFloat(255.0)
Ben Stahl
  • 1,139
  • 11
  • 11
2

I was trying to find the colors of all four corners of an image and was getting unexpected results, including UIColor.clear.

The issue is that the pixels start at 0, so requesting a pixel at the width of the image would actually wrap back around and give me the first pixel of the second row.

For example, the top right pixel of a 640 x 480 image would actually be x: 639, y: 0, and the bottom right pixel would be x: 639, y: 479.

Here's my implementation of the UIImage extension with this adjustment:

func getPixelColor(pos: CGPoint) -> UIColor {

    guard let cgImage = cgImage, let pixelData = cgImage.dataProvider?.data else { return UIColor.clear }
    let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

    let bytesPerPixel = cgImage.bitsPerPixel / 8
    // adjust the pixels to constrain to be within the width/height of the image
    let y = pos.y > 0 ? pos.y - 1 : 0
    let x = pos.x > 0 ? pos.x - 1 : 0
    let pixelInfo = ((Int(self.size.width) * Int(y)) + Int(x)) * bytesPerPixel

    let r = CGFloat(data[pixelInfo]) / CGFloat(255.0)
    let g = CGFloat(data[pixelInfo+1]) / CGFloat(255.0)
    let b = CGFloat(data[pixelInfo+2]) / CGFloat(255.0)
    let a = CGFloat(data[pixelInfo+3]) / CGFloat(255.0)

    return UIColor(red: r, green: g, blue: b, alpha: a)
}
OffensivelyBad
  • 621
  • 8
  • 16
2

I found no answer anywhere on the internet that supplied

  • Simple code
  • HDR support
  • Color profile support for bgr etc.
  • Scale support for @2x @3x

So here it is. The as far as I can tell definitive solution:

Swift 5


import UIKit

public extension CGBitmapInfo {
  // https://stackoverflow.com/a/60247693/2585092
  enum ComponentLayout {
    case bgra
    case abgr
    case argb
    case rgba
    case bgr
    case rgb

    var count: Int {
      switch self {
      case .bgr, .rgb: return 3
      default: return 4
      }
    }
  }

  var componentLayout: ComponentLayout? {
    guard let alphaInfo = CGImageAlphaInfo(rawValue: rawValue & Self.alphaInfoMask.rawValue) else { return nil }
    let isLittleEndian = contains(.byteOrder32Little)

    if alphaInfo == .none {
      return isLittleEndian ? .bgr : .rgb
    }
    let alphaIsFirst = alphaInfo == .premultipliedFirst || alphaInfo == .first || alphaInfo == .noneSkipFirst

    if isLittleEndian {
      return alphaIsFirst ? .bgra : .abgr
    } else {
      return alphaIsFirst ? .argb : .rgba
    }
  }

  var chromaIsPremultipliedByAlpha: Bool {
    let alphaInfo = CGImageAlphaInfo(rawValue: rawValue & Self.alphaInfoMask.rawValue)
    return alphaInfo == .premultipliedFirst || alphaInfo == .premultipliedLast
  }
}

extension UIImage {
  // https://stackoverflow.com/a/68103748/2585092
  subscript(_ point: CGPoint) -> UIColor? {
    guard
      let cgImage = cgImage,
      let space = cgImage.colorSpace,
      let pixelData = cgImage.dataProvider?.data,
      let layout = cgImage.bitmapInfo.componentLayout
    else {
      return nil
    }
    let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)

    let comp = CGFloat(layout.count)
    let isHDR = CGColorSpaceUsesITUR_2100TF(space)
    let hdr = CGFloat(isHDR ? 2 : 1)
    let pixelInfo = Int((size.width * point.y * scale + point.x * scale) * comp * hdr)
    let i = Array(0 ... Int(comp - 1)).map {
      CGFloat(data[pixelInfo + $0 * Int(hdr)]) / CGFloat(255)
    }

    switch layout {
    case .bgra:
      return UIColor(red: i[2], green: i[1], blue: i[0], alpha: i[3])
    case .abgr:
      return UIColor(red: i[3], green: i[2], blue: i[1], alpha: i[0])
    case .argb:
      return UIColor(red: i[1], green: i[2], blue: i[3], alpha: i[0])
    case .rgba:
      return UIColor(red: i[0], green: i[1], blue: i[2], alpha: i[3])
    case .bgr:
      return UIColor(red: i[2], green: i[1], blue: i[0], alpha: 1)
    case .rgb:
      return UIColor(red: i[0], green: i[1], blue: i[2], alpha: 1)
    }
  }
}

Nico S.
  • 3,056
  • 1
  • 30
  • 64
yspreen
  • 1,759
  • 2
  • 20
  • 44
  • For the readers that don't know what a subscript is and how to use it: `image[CGPoint(x: 1, y: 1)]` – Nico S. May 19 '22 at 10:38
  • I also fixed the deprecation warning of `isHDR()` and replaced it with the new way of `CGColorSpaceUsesITUR_2100TF(space)` – Nico S. May 19 '22 at 10:51
  • When the alpha info is `noneSkipFirst` or `noneSkipLast` I found that this function was returning a color with the wrong alpha. – brynbodayle Mar 08 '23 at 22:42
2

As usual, late to the party, but I wanted to mention that the method indicated above, doesn't always work. If the image is not RGBA, then it can crash. In my experience, running release (optimized) code, can crash, when the debug code works fine.

I tend to use a lot of vector images in my apps, and iOS can sometimes render them in monochrome color spaces. I have experienced a number of crashes, with the code given here.

Also, we should use bytesPerRow, when skipping on the vertical. Apple tends to add padding to bitmaps, and a simple 4-byte pixel offset may not work.

I draw the image into an offscreen context, then take the sample from there.

Here's what I did. It works, but is not exactly performant. In my case, it's fine, because I only use it once, at startup:

extension UIImage {
    /* ################################################################## */
    /**
     This returns the RGB color (as a UIColor) of the pixel in the image, at the given point. It is restricted to 32-bit (RGBA/8-bit pixel) values.
     This was inspired by several of the answers [in this StackOverflow Question](https://stackoverflow.com/questions/25146557/how-do-i-get-the-color-of-a-pixel-in-a-uiimage-with-swift).
     **NOTE:** This is unlikely to be highly performant!
     
     - parameter at: The point in the image to sample (NOTE: Must be within image bounds, or nil is returned).
     - returns: A UIColor (or nil).
     */
    func getRGBColorOfThePixel(at inPoint: CGPoint) -> UIColor? {
        guard (0..<size.width).contains(inPoint.x),
              (0..<size.height).contains(inPoint.y)
        else { return nil }

        // We draw the image into a context, in order to be sure that we are accessing image data in our required format (RGBA).
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        draw(at: .zero)
        let imageData = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        guard let cgImage = imageData?.cgImage,
              let pixelData = cgImage.dataProvider?.data
        else { return nil }
        
        let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
        let bytesPerPixel = (cgImage.bitsPerPixel + 7) / 8
        let pixelByteOffset: Int = (cgImage.bytesPerRow * Int(inPoint.y)) + (Int(inPoint.x) * bytesPerPixel)
        let divisor = CGFloat(255.0)
        let r = CGFloat(data[pixelByteOffset]) / divisor
        let g = CGFloat(data[pixelByteOffset + 1]) / divisor
        let b = CGFloat(data[pixelByteOffset + 2]) / divisor
        let a = CGFloat(data[pixelByteOffset + 3]) / divisor

        return UIColor(red: r, green: g, blue: b, alpha: a)
    }
}
Chris Marshall
  • 4,910
  • 8
  • 47
  • 72
1

Swift 5, includes solution for @2x & @3x image

extension UIImage {
    subscript(_ point: CGPoint) -> UIColor? {
        guard let pixelData = self.cgImage?.dataProvider?.data else { return nil }
        let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
        let pixelInfo: Int = Int((size.width * point.y + point.x) * 4.0 * scale * scale)
        let i = Array(0 ... 3).map { CGFloat(data[pixelInfo + $0]) / CGFloat(255) }
        return UIColor(red: i[0], green: i[1], blue: i[2], alpha: i[3])
    }
}
meomeomeo
  • 764
  • 4
  • 7
1

I use this extension :

public extension UIImage {

    var pixelWidth: Int {
        return cgImage?.width ?? 0
    }

    var pixelHeight: Int {
        return cgImage?.height ?? 0
    }

    func pixelColor(x: Int, y: Int) -> UIColor {

        if 0..<pixelWidth ~= x && 0..<pixelHeight ~= y {
            log.info("Pixel coordinates are in bounds")
        }else {
            log.info("Pixel coordinates are out of bounds")
                        return .black
        }
        guard
            let cgImage = cgImage,
            let data = cgImage.dataProvider?.data,
            let dataPtr = CFDataGetBytePtr(data),
            let colorSpaceModel = cgImage.colorSpace?.model,
            let componentLayout = cgImage.bitmapInfo.componentLayout
        else {
            assertionFailure("Could not get a pixel of an image")
            return .clear
        }

        assert(
            colorSpaceModel == .rgb,
            "The only supported color space model is RGB")
        assert(
            cgImage.bitsPerPixel == 32 || cgImage.bitsPerPixel == 24,
            "A pixel is expected to be either 4 or 3 bytes in size")

        let bytesPerRow = cgImage.bytesPerRow
        let bytesPerPixel = cgImage.bitsPerPixel/8
        let pixelOffset = y*bytesPerRow + x*bytesPerPixel

        if componentLayout.count == 4 {
            let components = (
                dataPtr[pixelOffset + 0],
                dataPtr[pixelOffset + 1],
                dataPtr[pixelOffset + 2],
                dataPtr[pixelOffset + 3]
            )

            var alpha: UInt8 = 0
            var red: UInt8 = 0
            var green: UInt8 = 0
            var blue: UInt8 = 0

            switch componentLayout {
            case .bgra:
                alpha = components.3
                red = components.2
                green = components.1
                blue = components.0
            case .abgr:
                alpha = components.0
                red = components.3
                green = components.2
                blue = components.1
            case .argb:
                alpha = components.0
                red = components.1
                green = components.2
                blue = components.3
            case .rgba:
                alpha = components.3
                red = components.0
                green = components.1
                blue = components.2
            default:
                return .clear
            }

            // If chroma components are premultiplied by alpha and the alpha is `0`,
            // keep the chroma components to their current values.
            if cgImage.bitmapInfo.chromaIsPremultipliedByAlpha && alpha != 0 {
                let invUnitAlpha = 255/CGFloat(alpha)
                red = UInt8((CGFloat(red)*invUnitAlpha).rounded())
                green = UInt8((CGFloat(green)*invUnitAlpha).rounded())
                blue = UInt8((CGFloat(blue)*invUnitAlpha).rounded())
            }

            return .init(red: red, green: green, blue: blue, alpha: alpha)

        } else if componentLayout.count == 3 {
            let components = (
                dataPtr[pixelOffset + 0],
                dataPtr[pixelOffset + 1],
                dataPtr[pixelOffset + 2]
            )

            var red: UInt8 = 0
            var green: UInt8 = 0
            var blue: UInt8 = 0

            switch componentLayout {
            case .bgr:
                red = components.2
                green = components.1
                blue = components.0
            case .rgb:
                red = components.0
                green = components.1
                blue = components.2
            default:
                return .clear
            }

            return .init(red: red, green: green, blue: blue, alpha: UInt8(255))

        } else {
            assertionFailure("Unsupported number of pixel components")
            return .clear
        }
    }

}

But for a right pixel color you need use only a image in xcasset in x1 otherwise your reference is wrong and you need to use this: let correctedImage = UIImage(data: image.pngData()!) for retrive the correct origin for your point .

dgalluccio
  • 85
  • 5
1

The solution of https://stackoverflow.com/a/40237504/3286489, only works on sRGB colorspace type of image. However, for a different colorspace (extended sRGB??), it doesn't work.

So to make it work, need to convert it to a normal sRGB image type first, before getting the color from the cgImage. Note we need to add padding to the calculation to ensure the width is always a factor of 8

public extension UIImage {
    func getPixelColor(pos: CGPoint) -> UIColor {
        
        // convert to standard sRGB image
        guard let cgImage = cgImage,
            let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
            let context = CGContext(data: nil, 
                width: Int(size.width), height: Int(size.height),
                bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace,
                bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
        else { return .white }

        context.draw(cgImage, in: CGRect(origin: .zero, size: size))
        
        // Get the newly converted cgImage
        guard let newCGImage = context.makeImage(),
            let newDataProvider = newCGImage.dataProvider,
            let data = newDataProvider.data
        else { return .white }
        let pixelData: UnsafePointer<UInt8> = CFDataGetBytePtr(data)

        // Calculate the pixel position based on point given
        let remaining = 8 - ((Int(size.width)) % 8)
        let padding = (remaining < 8) ? remaining : 0
        let pixelInfo: Int = (((Int(size.width) + padding) * Int(pos.y)) + Int(pos.x)) * 4
        
        let r = CGFloat(pixelData[pixelInfo]) / CGFloat(255.0)
        let g = CGFloat(pixelData[pixelInfo+1]) / CGFloat(255.0)
        let b = CGFloat(pixelData[pixelInfo+2]) / CGFloat(255.0)
        let a = CGFloat(pixelData[pixelInfo+3]) / CGFloat(255.0)
        return UIColor(red: r, green: g, blue: b, alpha: a)
   }
}

Optionally, if one doesn't want to convert to cgImage, just replace

        // Get the newly converted cgImage
        guard let newCGImage = context.makeImage(),
            let newDataProvider = newCGImage.dataProvider,
            let newData = newDataProvider.data
            else { return .white }
        let pixelData: UnsafePointer<UInt8> = CFDataGetBytePtr(newData)

With

        // Get the data and bind it from UnsafeMutableRawPointer to UInt8
        guard let data = context.data else { return .white }
        let pixelData = data.bindMemory(
            to: UInt8.self, capacity: Int(size.width * size.height * 4))

Updated

To get an even more concise code, we can improve the convert to sRGB using UIGraphicsImageRenderer directly. The calculation does changes a bit as due such redrawing refine the pixel to be 2x further.

    func getPixelColor(pos: CGPoint) -> UIColor {
        
        let newImage = UIGraphicsImageRenderer(size: size).image { _ in
            draw(in: CGRect(origin: .zero, size: size))
        }
        
        guard let cgImage = newImage.cgImage,
            let dataProvider = cgImage.dataProvider,
            let data = dataProvider.data else { return .white }
        let pixelData: UnsafePointer<UInt8> = CFDataGetBytePtr(data)
        let remaining = 8 - ((Int(size.width) * 2) % 8)
        let padding = (remaining < 8) ? remaining : 0
        let pixelInfo: Int = (((Int(size.width * 2) + padding) * Int(pos.y * 2)) + Int(pos.x * 2)) * 4
                
        let r = CGFloat(pixelData[pixelInfo]) / CGFloat(255.0)
        let g = CGFloat(pixelData[pixelInfo+1]) / CGFloat(255.0)
        let b = CGFloat(pixelData[pixelInfo+2]) / CGFloat(255.0)
        let a = CGFloat(pixelData[pixelInfo+3]) / CGFloat(255.0)
        return UIColor(red: r, green: g, blue: b, alpha: a)
    }

This is as per solution of convert to sRGB in https://stackoverflow.com/a/64538344/3286489

Elye
  • 53,639
  • 54
  • 212
  • 474
  • Refer https://medium.com/mobile-app-development-publication/explore-ios-auto-image-background-text-coloring-627c1d350561 for more illustration. – Elye Oct 28 '20 at 13:43
  • for the direct `UIGraphicsImageRenderer` solution, replacing the `* 2` with `* UIScreen.main.scale` worked for me – richy Feb 17 '22 at 15:42
-1

If you use image from Imgaes.xcassets, and only put one @1x image in 1x field. Leave the @2x, @3x blank.

闪电狮
  • 367
  • 2
  • 9