Reputation: 3124
For a given multi-color PNG UIImage
(with transparency), what is the best/Swift-idiomatic way to:
UIImage
There are a few related questions on SO but I haven't been able to find something that works.
Upvotes: 30
Views: 16867
Reputation: 2614
Just to give you massive speed improved version via DispatchQueue.concurrentPerform
for manipulating pixels. Here is my version to make complemented version of an input image.
To give some perspective - to invert an image this way, it takes a little bit longer then native Apple's solution for inverting an image.
EDIT: I am using DynamicColor library for complementing colors. You also need converter from RGBA32 to UIColor and vice versa.
static func processPixelsPixelation1(inputCGImage: CGImage, pixelBuffer: UnsafeMutablePointer<RGBA32>)
{
let width = inputCGImage.width
let height = inputCGImage.height
DispatchQueue.concurrentPerform(iterations: Int(height) - 1) { row in
DispatchQueue.concurrentPerform(iterations: Int(width) - 1) { column in
let offset = row * width + column
let color = pixelBuffer[offset]
let c = color.convertToUIColor()
let modified = c.complemented()
let modifiedToRGBA = modified.convertToRGBA32()
pixelBuffer[offset] = modifiedToRGBA
}
}
}
Use processor
as a generic pointer to the function (as an input parameter) (in this case processPixelsPixelation1
):
.
.
.
let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)
// PROCESS IMAGE !!!
processor(inputCGImage, pixelBuffer)
.
.
.
extension RGBA32
{
func convertToUIColor() -> UIColor
{
return UIColor(
red: self.redComponent,
green: self.greenComponent,
blue: self.blueComponent,
alpha: self.alphaComponent
)
}
}
typealias ColorComponents = (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat)
extension UIColor
{
func getColorComponents() -> ColorComponents
{
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
self.getRed(&r, green: &g, blue: &b, alpha: &a)
return ColorComponents(r, g, b, a)
}
convenience init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
self.init(
red: CGFloat(red) / 255,
green: CGFloat(green) / 255,
blue: CGFloat(blue) / 255,
alpha: CGFloat(alpha) / 255
)
}
func convertToRGBA32() -> RGBA32
{
let tmpComponents = self.getColorComponents()
return RGBA32(
red: UInt8(255 * tmpComponents.r),
green: UInt8(255 * tmpComponents.g),
blue: UInt8(255 * tmpComponents.b),
alpha: UInt8(255 * tmpComponents.a)
)
}
}
Upvotes: 0
Reputation: 2214
For getting better result we can search color range in image pixels, referring to @Rob answer I made update and now the result is better.
func processByPixel(in image: UIImage) -> UIImage? {
guard let inputCGImage = image.cgImage else { print("unable to get cgImage"); return nil }
let colorSpace = CGColorSpaceCreateDeviceRGB()
let width = inputCGImage.width
let height = inputCGImage.height
let bytesPerPixel = 4
let bitsPerComponent = 8
let bytesPerRow = bytesPerPixel * width
let bitmapInfo = RGBA32.bitmapInfo
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
print("Cannot create context!"); return nil
}
context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let buffer = context.data else { print("Cannot get context data!"); return nil }
let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)
for row in 0 ..< Int(height) {
for column in 0 ..< Int(width) {
let offset = row * width + column
/*
* Here I'm looking for color : RGBA32(red: 231, green: 239, blue: 247, alpha: 255)
* and I will convert pixels color that in range of above color to transparent
* so comparetion can done like this (pixelColorRedComp >= ourColorRedComp - 1 && pixelColorRedComp <= ourColorRedComp + 1 && green && blue)
*/
if pixelBuffer[offset].redComponent >= 230 && pixelBuffer[offset].redComponent <= 232 &&
pixelBuffer[offset].greenComponent >= 238 && pixelBuffer[offset].greenComponent <= 240 &&
pixelBuffer[offset].blueComponent >= 246 && pixelBuffer[offset].blueComponent <= 248 &&
pixelBuffer[offset].alphaComponent == 255 {
pixelBuffer[offset] = .transparent
}
}
}
let outputCGImage = context.makeImage()!
let outputImage = UIImage(cgImage: outputCGImage, scale: image.scale, orientation: image.imageOrientation)
return outputImage
}
I hope this helps someone 🎉
Upvotes: 2
Reputation: 437682
You have to extract the pixel buffer of the image, at which point you can loop through, changing pixels as you see fit. At the end, create a new image from the buffer.
In Swift 3, this looks like:
func processPixels(in image: UIImage) -> UIImage? {
guard let inputCGImage = image.cgImage else {
print("unable to get cgImage")
return nil
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let width = inputCGImage.width
let height = inputCGImage.height
let bytesPerPixel = 4
let bitsPerComponent = 8
let bytesPerRow = bytesPerPixel * width
let bitmapInfo = RGBA32.bitmapInfo
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
print("unable to create context")
return nil
}
context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let buffer = context.data else {
print("unable to get context data")
return nil
}
let pixelBuffer = buffer.bindMemory(to: RGBA32.self, capacity: width * height)
for row in 0 ..< Int(height) {
for column in 0 ..< Int(width) {
let offset = row * width + column
if pixelBuffer[offset] == .black {
pixelBuffer[offset] = .red
}
}
}
let outputCGImage = context.makeImage()!
let outputImage = UIImage(cgImage: outputCGImage, scale: image.scale, orientation: image.imageOrientation)
return outputImage
}
struct RGBA32: Equatable {
private var color: UInt32
var redComponent: UInt8 {
return UInt8((color >> 24) & 255)
}
var greenComponent: UInt8 {
return UInt8((color >> 16) & 255)
}
var blueComponent: UInt8 {
return UInt8((color >> 8) & 255)
}
var alphaComponent: UInt8 {
return UInt8((color >> 0) & 255)
}
init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) {
let red = UInt32(red)
let green = UInt32(green)
let blue = UInt32(blue)
let alpha = UInt32(alpha)
color = (red << 24) | (green << 16) | (blue << 8) | (alpha << 0)
}
static let red = RGBA32(red: 255, green: 0, blue: 0, alpha: 255)
static let green = RGBA32(red: 0, green: 255, blue: 0, alpha: 255)
static let blue = RGBA32(red: 0, green: 0, blue: 255, alpha: 255)
static let white = RGBA32(red: 255, green: 255, blue: 255, alpha: 255)
static let black = RGBA32(red: 0, green: 0, blue: 0, alpha: 255)
static let magenta = RGBA32(red: 255, green: 0, blue: 255, alpha: 255)
static let yellow = RGBA32(red: 255, green: 255, blue: 0, alpha: 255)
static let cyan = RGBA32(red: 0, green: 255, blue: 255, alpha: 255)
static let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
static func ==(lhs: RGBA32, rhs: RGBA32) -> Bool {
return lhs.color == rhs.color
}
}
For Swift 2 rendition, see previous revision of this answer.
Upvotes: 66