基于CoreGraphics图像处理

本文记录如何使用CoreGraphics对图像进行简单处理,并粗略的实现一些图片的特效。

整体思路如下

  1. 创建位图上下文,按照格式把原图片色彩信息写入内存中,这里是百度百科对位图的介绍
  2. 遍历存储原图片色彩信息的内存数据,通过地址和图片像素的关系,来获取原图每个像素的R G B A
  3. 修改R G B A的值,如果能够按照某种特效的算法来修改,那么最终渲染的结果就是该特效
  4. 从位图上下文中渲染出新图片
  5. 内存管理

before code需要新建一个UIImage的扩展文件用于封装抽离处理图片的代码。

获取包含位图上下文

我们创建一个位图上下文并且申请一块内存,把原图的色彩信息存入到这块内存中,存的方式:(pic.width pic.height)个像素,每个像素单色彩通道占8位(0-255即一个字节),每个像素共占4个字节分别描述该像素的R/G/B/A,这块内存占width height * 4个字节,这样我们图片的色彩数字数据和内存地址一一对应起来了,内存地址偏移0个字节为第一个像素的R,偏移1个字节为第一个像素的G,偏移4个字节为第二个像素的R
核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
extension UIImage {
/// 采用自动申请内存的方式把图片信息存入位图上下文中
func convertToDataAutoMallocMemory() -> CGContext? {

guard let cgImage = cgImage else {
return nil
}
//let data = malloc(cgImage.height * cgImage.bytesPerRow)
let ctx = CGContext(data: nil, width: cgImage.width,
height: cgImage.height,
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: cgImage.bytesPerRow,
space: cgImage.colorSpace!,
bitmapInfo: cgImage.bitmapInfo.rawValue)!
ctx.draw(cgImage, in: CGRect(origin: .zero, size: size))
return ctx
}

/// 采用手动申请内存的方式将图片信息存入位图上下文中
func convertToDataManualMallocMemory() -> UnsafeMutableRawPointer? {

guard let cgImage = cgImage else {
return nil
}
let length = cgImage.height * cgImage.bytesPerRow
let data = malloc(length)
memset(data, 0, length)
let ctx = CGContext(data: data, width: cgImage.width,
height: cgImage.height,
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: cgImage.bytesPerRow,
space: cgImage.colorSpace!,
bitmapInfo: cgImage.bitmapInfo.rawValue)!
ctx.draw(cgImage, in: CGRect(origin: .zero, size: size))
return data
}
}

API参数:

  1. bitsPerComponent:单色彩通道信息占几位,如果是0-255的话,8位就够了,所以demo中也是用UnSignedChar类型
  2. 单像素占字节数 = Int(bitsPerComponent * number of components + 7)/8(加7有向上取整的效果)
  3. bytesPerRow 单行占字节数,等于width * 单像素的字节数(RGBA就是4个字节)
  4. data是我们存放位图信息的指针,大小至少为height 单行占字节数,如果设置为nil,那么上下文会自动去申请这块内存,并且在上下文被释放的时候比如(执行完位图上下文所在的方法),会自动释放这块内存,并且同时释放这个位图所申请的内存ctx.data*,也可以手动申请这块内存
  5. space是用的颜色格式有RGB、MYK、Gray…..,我们一般需要处理的就是RGB
  6. bitmapInfo描述如何处理Alpha通道

遍历内存的数据

在上一步已经说了内存地址和图片色彩数字数据的对应关系,这一步就是要遍历这块内存,把原图的每个像素色彩的数字数据拿出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static func traversePixels(_ ctx: CGContext,
handler: ((CUnsignedChar, CUnsignedChar, CUnsignedChar, CUnsignedChar) -> (CUnsignedChar, CUnsignedChar, CUnsignedChar, CUnsignedChar))?) {

guard let data = ctx.data else {
return
}
for row in 0 ..< ctx.height {
for column in 0 ..< ctx.width {
let bitMapIndex = (ctx.width * row + column) * 4
let red = data.load(fromByteOffset: bitMapIndex,
as: CUnsignedChar.self)
let green = data.load(fromByteOffset: bitMapIndex + 1,
as: CUnsignedChar.self)
let blue = data.load(fromByteOffset: bitMapIndex + 2,
as: CUnsignedChar.self)
let alpha = data.load(fromByteOffset: bitMapIndex + 3, as: CUnsignedChar.self)

// 将修改后的RGBA存入这块内存
if let newRGBA = handler?(red, green, blue, alpha) {
data.storeBytes(of: newRGBA.0, toByteOffset: bitMapIndex,
as: CUnsignedChar.self)
data.storeBytes(of: newRGBA.1, toByteOffset: bitMapIndex + 1,
as: CUnsignedChar.self)
data.storeBytes(of: newRGBA.2, toByteOffset: bitMapIndex + 2,
as: CUnsignedChar.self)
data.storeBytes(of: newRGBA.3, toByteOffset: bitMapIndex + 3,
as: CUnsignedChar.self)
}
}
}
}

修改每个像素的RGBA

这一步修改每个像素的RGBA,在traversePixelshandle完成操作,并返回信新值

灰度算法
1
2
3
4
5
6
7
let ctxA = image.convertToDataAutoMallocMemory()!
UIImage.traversePixels(ctxA) { (red, green, blue, alpha) -> (CUnsignedChar, CUnsignedChar, CUnsignedChar, CUnsignedChar) in

let newColorInfo = Int(red) * 77 / 255 + Int(green) * 151 / 255 + Int(blue) * 88 / 255
let gray = CUnsignedChar(newColorInfo > 255 ? 255 : newColorInfo)
return (gray, gray, gray, alpha)
}

颜色翻转
1
2
3
4
let ctxB = image.convertToDataAutoMallocMemory()!
UIImage.traversePixels(ctxB) { (red, green, blue, alpha) -> (CUnsignedChar, CUnsignedChar, CUnsignedChar, CUnsignedChar) in
return (255 - red, 255 - green, 255 - blue, alpha)
}

可以问设计师或者网上找一些其他的效果算法,比如暖色系、冷色系效果需要把那些颜色分量调高、哪些颜色分量调低、提高对比度的算法等等。这里有一个提升饱和度的算法,这里可能需要多个处理rgb->hsv -> 重新计算s ->rgb,这个我并没有尝试效果。

从位图上下文渲染图片

位图上下文自动申请内存

创建位图上下文的接口中有提到,如果位图上下文被销毁,则对应的内存就会被释放,而这块内存存的是图片的色彩信息,也是修改后的图片色彩信息(前提:直接将修改后的色彩信息存到这块内存中),所以必须要保留位图上文的引用(返回位图上下文),如果不保留上下文的引用而直接引用位图上下文.data属性,那么创建位图上下文的方法走完之后,这块内存会跟着位图上下文一块被销毁,从而导致野指针异常。
由于引用了位图上下文,并且将修改后的RGBA直接存到了这内存中(没有申请新的内存),所以我们可以直接使用ctx.image(我估计是新开辟内存存储图片新的色彩信息了,因为局部引用上下文,在图片销毁之前上下文就被自动释放了,而图片还是正常显示的)获取CGImage对象,非常方便。

手动申请内存

如果是手动申请的内存,可以使用如下方法将这块内存对应的颜色信息给绘制出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static func render(_ data: UnsafeMutableRawPointer, source: CGImage) -> UIImage? {

let size = source.bytesPerRow * source.height
let provider = CGDataProvider(dataInfo: nil, data: data, size: size) { (dataInfo, data, size) in
}
let newCGImage = CGImage(width: source.width,
height: source.height,
bitsPerComponent: source.bitsPerComponent,
bitsPerPixel: source.bitsPerPixel,
bytesPerRow: source.bytesPerRow,
space: source.colorSpace ?? CGColorSpaceCreateDeviceRGB(),
bitmapInfo: source.bitmapInfo,
provider: provider!,
decode: source.decode,
shouldInterpolate: source.shouldInterpolate,
intent: source.renderingIntent)
if let newCGImage = newCGImage {
return UIImage(cgImage: newCGImage)
}
return nil
}
}
需要在合适的时机释放掉这块内存

文中使用的灰度处理、颜色反转的效果如下:

内存释放

在图片展示结束之前,一定要保证存储存图片色彩信息的内存不能被释放,否则会触发野指针异常。
如果是手动申请的内存,则需要在图片停止展示的时候free(),个人建议新建UIImage子类,并添加属性记录这块内存,在.deinit函数中释放这个内存。
像CGContext、CGDataProvider…都会自动释放,OC需要手动释放。

13种图片滤镜

以上就是基于CoreGraphics图片处理的大致内容,整体上看还是比较简单的,四步走即可完成。更为重要的是,我们要知道如何去处理这些像素中的彩色信息来获取比较满意的效果,毕竟代码基本是死的,活的是像素处理算法,这就需要一定的图片像素处理知识理论了,当然了这已经不是我们程序员的知识范畴了,我们需要将色彩处理的逻辑(如何调节R/G/B/A)转化为代码就可以了。
我从网上偶然得知13种基于CoreGraphics的色彩处理方式(Ibokan??),并由此生成了13种图片滤镜效果:LOMO、黑白、复古、哥特、锐化、淡雅、酒红、清宁、浪漫、光晕、蓝调、梦幻、夜色,基于这13种滤镜效果我做了个简单的demo,效果如下:
这些滤镜的实现步骤和我们用的四步走基本上是一样的,所以不再详细介绍里面的代码,详细代码在这里,我们只需要看一些它是如何重新计算R/G/B/A值的即可。
每个滤镜效果都对应一个5*5矩阵,来看其中两个:

LOMO
1
2
3
4
5
const float colormatrix_lomo[] = {
1.7f, 0.1f, 0.1f, 0, -73.1f,
0, 1.7f, 0.1f, 0, -73.1f,
0, 0.1f, 1.6f, 0, -73.1f,
0, 0, 0, 1.0f, 0 };

夜色
1
2
3
4
5
6
7
// 13、夜色
const float colormatrix_yese[] = {
1.0f, 0.0f, 0.0f, 0.0f, -66.6f,
0.0f, 1.1f, 0.0f, 0.0f, -66.6f,
0.0f, 0.0f, 1.0f, 0.0f, -66.6f,
0.0f, 0.0f, 0.0f, 1.0f, 0.0f
};

R/G/B/A的计算方式如下:
1
2
3
4
*red = f[0] * redV + f[1] * greenV + f[2] * blueV + f[3] * alphaV + f[4];
*green = f[0+5] * redV + f[1+5] * greenV + f[2+5] * blueV + f[3+5] * alphaV + f[4+5];
*blue = f[0+5*2] * redV + f[1+5*2] * greenV + f[2+5*2] * blueV + f[3+5*2] * alphaV + f[4+5*2];
*alpha = f[0+5*3] * redV + f[1+5*3] * greenV + f[2+5*3] * blueV + f[3+5*3] * alphaV + f[4+5*3];

可以看出新色彩的计算方式:R/G/B/A每个通道是4个通道和矩阵进行运算的结果。
源代码都是用OC写的,我为了方便使用,用Swift实现了一遍,代码在这里。还是前面提到的,代码不重要,处理这些像素的idea才是最重要的。

显示 Gitment 评论