改变UIImage中某些像素的颜色

28

针对给定的多彩PNG UIImage(带透明度),以下是最佳/Swift惯用方式来完成以下任务:

  1. 创建一个重复的UIImage
  2. 查找副本中所有黑色像素并将其更改为红色
  3. (返回修改后的副本)

SO上有一些相关问题,但我还没有找到有效的解决方案。


我可以建议的一件事是逐个像素地检查,如果它是黑色的,则手动更改它。 - Aggressor
1
实际上...我的问题是如何做到这一点 :) 我是Swift的新手,对API非常不熟悉,甚至不知道该搜索什么。 - mjswensen
@mjswensen,处理速度对你来说不是一个障碍吗?我正在使用与你提到的完全相同的代码来处理相同的情况,但需要4-5秒。 - Raja Saad
2个回答

63

您需要提取图像的像素缓冲区,此时您可以循环遍历,根据需要更改像素。最后,从缓冲区创建新图像。

在Swift 3中,代码如下:

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
    }
}

如需Swift 2版本,请参阅此答案的上一个版本


Rob,感谢您的及时和详尽的回复!当我尝试使用您的代码时,我遇到了以下运行时错误:<Error>:CGBitmapContextCreate:不支持的参数组合:8个整数位/组件;24位/像素;3组分颜色空间;kCGImageAlphaNone;352字节/行。致命错误:在解包可选值时意外地发现了nil - 这可能是colorSpace引起的吗? - mjswensen
可调整大小的iPad / iOS 8.4(12H141) - mjswensen
1
是的,那个模拟器对我来说运行良好。你错误信息中的 kCGImageAlphaNone 非常可疑。这就像你正在将 0 传递给 CGBitmapContextCreate 的最后一个参数一样。仔细检查一下 bitmapInfo 参数。尝试使用 CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.rawValue) 代替 bitmapInfo - Rob
1
@VagueExplanation - 每当发生这种情况时,将有问题的行拆分为单独的语句。请参见我在上面修订答案中init方法的做法。 - Rob
1
@Rob,非常感谢你节省了我的时间!!你对每个问题和解决方案的描述超乎想象。在我按照你的指示和代码片段之后,现在我的代码运行得像个魅力一样。对于每个遇到这种问题或想要改变图像像素颜色的人,请查看上面评论中的问题链接,并享受Rob详细解释的答案。#尊重 - Raja Saad
显示剩余24条评论

2

为了获得更好的结果,我们可以在图像像素中搜索颜色范围,参考@Rob的答案,我进行了更新,现在结果更好了。

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
}

我希望这能帮助到某些人。


由于嵌套循环遍历行和列或宽度和高度,所以需要一些时间。我可以在这里发布与位图玩耍的Android代码,它只需要一个循环就能正常工作,并且速度也很快。 - Raja Saad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接