使用CGContext获取像素的RGB值该怎么做?

4

我想通过改变像素来编辑图像。

以下是我的代码:

    let imageRect = CGRectMake(0, 0, self.image.image!.size.width, self.image.image!.size.height)

    UIGraphicsBeginImageContext(self.image.image!.size)
    let context = UIGraphicsGetCurrentContext()

    CGContextSaveGState(context)
    CGContextDrawImage(context, imageRect, self.image.image!.CGImage)

    for x in 0...Int(self.image.image!.size.width) {
        for y in 0...Int(self.image.image!.size.height) {
            var red = 0
            if y % 2 == 0 {
                red = 255
            }

            CGContextSetRGBFillColor(context, CGFloat(red/255), 0.5, 0.5, 1)
            CGContextFillRect(context, CGRectMake(CGFloat(x), CGFloat(y), 1, 1))
        }
    }
    CGContextRestoreGState(context)
    self.image.image = UIGraphicsGetImageFromCurrentImageContext()

我正在循环遍历所有像素并更改每个像素的值,然后将其转换回图像。我想要做的是以某种方式获取当前像素(在y-for-loop中)的值,并对该数据进行处理。我在互联网上没有找到关于这个特定问题的任何信息。


1
你尝试过这个吗:https://dev59.com/zXVC5IYBdhLWcg3w7Vxq - Teja Nandamuri
相关的答案在我看来很不错。 - Daij-Djan
3个回答

4
在内部,UIGraphicsBeginImageContext创建了一个CGBitmapContext。您可以使用CGBitmapContextGetData来访问上下文的像素存储。这种方法的问题在于UIGraphicsBeginImageContext函数选择用于存储像素数据的字节顺序和颜色空间。那些选择(特别是字节顺序)可能会在未来的iOS版本(甚至不同设备上)发生改变。
因此,我们应该直接使用CGBitmapContextCreate创建上下文,以确保字节顺序和颜色空间的准确性。
在我的playground中,我添加了一个名为pic@2x.jpeg的测试图像。
import XCPlayground
import UIKit

let image = UIImage(named: "pic.jpeg")!
XCPCaptureValue("image", value: image)

以下是我们创建位图上下文的方法,考虑到图片的比例尺寸(这是你在问题中没有考虑到的):

let rowCount = Int(image.size.height * image.scale)
let columnCount = Int(image.size.width * image.scale)
let stride = 64 * ((columnCount * 4 + 63) / 64)
let context = CGBitmapContextCreate(nil, columnCount, rowCount, 8, stride,
    CGColorSpaceCreateDeviceRGB(),
    CGBitmapInfo.ByteOrder32Little.rawValue |
    CGImageAlphaInfo.PremultipliedLast.rawValue)

接下来,我们调整坐标系统以匹配UIGraphicsBeginImageContextWithOptions的操作,这样我们就可以轻松地正确绘制图像:

CGContextTranslateCTM(context, 0, CGFloat(rowCount))
CGContextScaleCTM(context, image.scale, -image.scale)

UIGraphicsPushContext(context!)
image.drawAtPoint(CGPointZero)
UIGraphicsPopContext()

请注意,UIImage.drawAtPoint会考虑image.orientation。而CGContextDrawImage则不会。
现在让我们从上下文中获取原始像素数据的指针。如果我们定义一个结构来访问每个像素的各个组件,代码将更清晰:
struct Pixel {
    var a: UInt8
    var b: UInt8
    var g: UInt8
    var r: UInt8
}

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

请注意,Pixel 成员的顺序已定义为匹配我在 CGBitmapContextCreatebitmapInfo 参数中设置的特定位。
现在我们可以循环遍历像素。请注意,我们使用上面计算出的 rowCountcolumnCount 来访问所有的像素,无论图像缩放比例如何。
for y in 0 ..< rowCount {
    if y % 2 == 0 {
        for x in 0 ..< columnCount {
            let pixel = pixels.advancedBy(y * stride / sizeof(Pixel.self) + x)
            pixel.memory.r = 255
        }
    }
}

最后,我们从上下文中获得了一张新的图片:
let newImage = UIImage(CGImage: CGBitmapContextCreateImage(context)!, scale: image.scale, orientation: UIImageOrientation.Up)

XCPCaptureValue("newImage", value: newImage)

在我的 playground 时间轴上,结果如下:

时间轴截图

最后,注意如果你的图片很大,逐像素遍历会很慢。如果你能找到一种使用 Core Image 或 GPUImage 进行图片处理的方法,速度会快得多。如果做不到这一点,使用 Objective-C 并手动矢量化(使用 NEON intrinsics)可能会提供很大的帮助。


你有 NEON intrinsics 的链接吗? - SemAllush
它能允许我更改像素数据吗? - SemAllush
NEON是一组ARM CPU指令和寄存器,可以同时操作多个数据单元。 (Intel x86的等效物称为SSE。)您可以使用它们来加速处理。如果您可以访问像素数据,则可以使用它们修改像素数据。我在这里的答案展示了如何访问CGBitmapContext的像素数据。 - rob mayoff

3

本答案假定您已经创建了一张图像的CGContext。答案的一个重要部分是将行偏移量舍入至8的倍数,以确保其适用于任何图像尺寸,这是其他在线解决方案中没有看到的。

Swift 5

func colorAt(x: Int, y: Int) -> UIColor {
    let capacity = context.width * context.height
    let widthMultiple = 8
    let rowOffset = ((context.width + widthMultiple - 1) / widthMultiple) * widthMultiple // Round up to multiple of 8
    let data: UnsafeMutablePointer<UInt8> = context!.data!.bindMemory(to: UInt8.self, capacity: capacity)
    let offset = 4 * ((y * rowOffset) + x)

    let red = data[offset+2]
    let green = data[offset+1]
    let blue = data[offset]
    let alpha = data[offset+3]

    return UIColor(red: CGFloat(red)/255.0, green: CGFloat(green)/255.0, blue: CGFloat(blue)/255.0, alpha: CGFloat(alpha)/255.0)
}

2
好的,我认为我有一个在Swift 2中适用的解决方案。以下UIColor扩展的功劳归于此答案
由于我需要一张图片来测试这个,所以我选择了你的gravatar的一个切片(50 x 50 - 左上角)...
因此下面的代码将这个转换:
enter image description here 到这个:
enter image description here 这在playground中对我有效-你所要做的就是复制并粘贴到playground中查看结果:
//: Playground - noun: a place where people can play

import UIKit
import XCPlayground

extension CALayer {

    func colorOfPoint(point:CGPoint) -> UIColor
    {
        var pixel:[CUnsignedChar] = [0,0,0,0]

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue)

        let context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, colorSpace,bitmapInfo.rawValue)

        CGContextTranslateCTM(context, -point.x, -point.y)

        self.renderInContext(context!)

        let red:CGFloat = CGFloat(pixel[0])/255.0
        let green:CGFloat = CGFloat(pixel[1])/255.0
        let blue:CGFloat = CGFloat(pixel[2])/255.0
        let alpha:CGFloat = CGFloat(pixel[3])/255.0

        //println("point color - red:\(red) green:\(green) blue:\(blue)")

        let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)

        return color
    }
}

extension UIColor {
    var components:(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
        var r:CGFloat = 0
        var g:CGFloat = 0
        var b:CGFloat = 0
        var a:CGFloat = 0
        getRed(&r, green: &g, blue: &b, alpha: &a)
        return (r,g,b,a)
    }
}


//get an image we can work on
var imageFromURL = UIImage(data: NSData(contentsOfURL: NSURL(string:"https://www.gravatar.com/avatar/ba4178644a33a51e928ffd820269347c?s=328&d=identicon&r=PG&f=1")!)!)
//only use a small area of that image - 50 x 50 square
let imageSliceArea = CGRectMake(0, 0, 50, 50);
let imageSlice  = CGImageCreateWithImageInRect(imageFromURL?.CGImage, imageSliceArea);
//we'll work on this image
var image = UIImage(CGImage: imageSlice!)


let imageView = UIImageView(image: image)
//test out the extension above on the point (0,0) - returns r 0.541 g 0.78 b 0.227 a 1.0
var pointColor = imageView.layer.colorOfPoint(CGPoint(x: 0, y: 0))



let imageRect = CGRectMake(0, 0, image.size.width, image.size.height)

UIGraphicsBeginImageContext(image.size)
let context = UIGraphicsGetCurrentContext()

CGContextSaveGState(context)
CGContextDrawImage(context, imageRect, image.CGImage)

for x in 0...Int(image.size.width) {
    for y in 0...Int(image.size.height) {
        var pointColor = imageView.layer.colorOfPoint(CGPoint(x: x, y: y))
        //I used my own creativity here - change this to whatever logic you want
        if y % 2 == 0 {
            CGContextSetRGBFillColor(context, pointColor.components.red , 0.5, 0.5, 1)
        }
        else {
            CGContextSetRGBFillColor(context, 255, 0.5, 0.5, 1)
        }

        CGContextFillRect(context, CGRectMake(CGFloat(x), CGFloat(y), 1, 1))
    }
}
CGContextRestoreGState(context)
image = UIGraphicsGetImageFromCurrentImageContext()

我希望这对你有用。我玩得很开心!

你的意思是在定义上下文时使用bitmapInfo.rawValue吗? - SemAllush
无法工作:layer.bounds.maxX与image.size.width不同,因此它会缩小图片并用黑色填充其余部分。 - SemAllush
诚实的错误。我一开始是用20 x 20的尺寸,但那只是纯绿色,不是很有趣。于是我修改了注释。 - ProgrammierTier
不知道为什么,对我来说还是不起作用,我要再试一下。 - SemAllush
让我们在聊天中继续这个讨论。点击此处进入聊天室 - ProgrammierTier
显示剩余2条评论

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