Tenloy's Blog

图形处理(三) - 图形处理实践案例

Word count: 4.1kReading time: 19 min
2021/09/15 Share

在项目中因为性能原因,最好不要叠加太多UIView,CALayer层级的显示;但是很多情况又必须使用到图片的合成或是像素、滤镜处理。这里针这些常用的图片处理使用不同图形处理框架进行相关编码实践。原文链接 — iOS图片处理实践项目代码

一、图片手动解码

写在前面:图片编码解码理论见上上篇

场景:适用于需要快速显示图片的地方,例如tableCell,先把图片进行bitmap解码操作加入缓存。同时如果是超大图可以和下面第三节的图片压缩方法搭配使用。

解决方案:通过CGBitmapContextCreate 重绘图片,这种压缩的图片等于手动进行了一次解码,可以加快图片的展示

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
//图片处理-强制解压缩操作-把元数据绘制到当前的上下文-压缩图片
- (UIImage *)compressWithBitmap:(CGFloat)scale {
//获取当前图片数据源
CGImageRef imageRef = self.CGImage;
//设置大小改变压缩图片
NSUInteger width = CGImageGetWidth(imageRef)*scale;
NSUInteger height = CGImageGetHeight(imageRef)*scale;
//创建颜色空间
CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
/*
创建绘制当前图片的上下文
CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
data:所需要的内存空间 传nil会自动分配
width/height:当前画布的大小
bitsPerComponent:每个颜色分量的大小 RGBA 每一个分量占1个字节
bytesPerRow:每一行使用的字节数 4*width
bitmapInfo:RGBA绘制的顺序
*/
CGContextRef contextRef =
CGBitmapContextCreate(nil,
width,
height,
8,
4*width,
colorSpace,
kCGImageAlphaNoneSkipLast);
//根据数据源在上下文(画板)绘制图片
CGContextDrawImage(contextRef, CGRectMake(0, 0, width, height), imageRef);

imageRef = CGBitmapContextCreateImage(contextRef);
CGContextRelease(contextRef);
return [UIImage imageWithCGImage:imageRef scale:self.scale orientation:UIImageOrientationUp];
}

二、大图在本地的高效显示

项目场景:1、下载大图后需要显示在屏幕上;2、本地读取大图显示在屏幕上。特别是对性能和图片要求较高的时候。

最佳解决方案:WWDC2018 苹果给的方案,见上上篇最后一节。

三、图片压缩

写在前面:首先介绍两种最简单最常见的压缩方式,下面复杂的压缩方式也是在此之上的扩展,可以根据实际情况进行调整;

关于质量的压缩,苹果提供了一个方法:

1
UIImageJPEGRepresentation(image, compression);

关于这个方法,理论上值越小表示图片质量越低,图片文件自然越小。但是并不是 compression 取 0,就是0b大小,取 1 就是原图。而且如果你是一张很大的图,即使compression = 0.0001等或更小,图片压缩到一定大小后,都无法再被压缩下去。

3.1 按照指定压缩比例压缩图片

1
2
3
4
5
6
7
//按照质量压缩
//主要弊端:如果有大图按这个方法,尺寸有可能依然很大
- (UIImage *)compressWithQuality:(CGFloat)rate {
NSData *data = UIImageJPEGRepresentation(self, rate);
UIImage *resultImage = [UIImage imageWithData:data];
return resultImage;
}

3.2 按照指定尺寸压缩图片

1
2
3
4
5
6
7
8
9
// 按照尺寸压缩
// 主要弊端:图片可能会变形,质量也无法保证
- (UIImage *)compressWithSize:(CGSize)size {
UIGraphicsBeginImageContext(size);
[self drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resultImage;
}

3.3 具体的应用场景分析

1. 上传或存储有大小要求的图片

循环逐渐减小图片尺寸,直到图片稍小于指定大小,这样做的好处是可以在我们限定图片大小后,图片尺寸也是此时最大的。问题是循环次数多,效率低,耗时长。可以用二分法来提高效率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 循环逐渐减小图片尺寸,直到图片稍小于指定大小
// 同样的问题是循环次数多,效率低,耗时长。可以用二分法来提高效率,具体代码省略。这里介绍另外一种方法,比二分法更好,压缩次数少,而且可以使图片压缩后刚好小于指定大小(不只是 < maxLength, > maxLength * 0.9)。
- (UIImage *)compressWithCycleSize:(NSInteger)maxLength {
UIImage *resultImage = self;
NSData *data = UIImageJPEGRepresentation(resultImage, 1);
NSUInteger lastDataLength = 0;
while (data.length > maxLength && data.length != lastDataLength) {
lastDataLength = data.length;
CGFloat ratio = (CGFloat)maxLength / data.length;
CGSize size = CGSizeMake((NSUInteger)(resultImage.size.width * sqrtf(ratio)),
(NSUInteger)(resultImage.size.height * sqrtf(ratio))); // Use NSUInteger to prevent white blank
UIGraphicsBeginImageContext(size);
// Use image to draw (drawInRect:), image is larger but more compression time
// Use result image to draw, image is smaller but less compression time
[resultImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
data = UIImageJPEGRepresentation(resultImage, 1);
}
return resultImage;
}

2. 上传或存储有质量要求的图片

循环压缩图片质量直到图片稍小于指定大小,默认循环6次,循环太多次后面也再也压不下去,当然这个次数可以自行配置。好处就是最大限度的保证了图片质量。同样用二分法来提高效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 循环压缩图片质量直到图片稍小于指定大小。
// ⚠️:注意:当图片质量低于一定程度时,继续压缩没有效果。默认压缩最多6次,通过二分法来优化循环次数多
// 压缩图片质量的优点在于,尽可能保留图片清晰度,图片不会明显模糊;缺点在于,不能保证图片压缩后小于指定大小。
- (UIImage *)compressWithCycleQulity:(NSInteger)maxLength {
CGFloat compression = 1;
NSData *data = UIImageJPEGRepresentation(self, compression);
if (data.length < maxLength) return self;
CGFloat max = 1;
CGFloat min = 0;
for (int i = 0; i < 6; ++i) {
compression = (max + min) / 2;
data = UIImageJPEGRepresentation(self, compression);
if (data.length < maxLength * 0.9) {
min = compression;
} else if (data.length > maxLength) {
max = compression;
} else {
break;
}
}
UIImage *resultImage = [UIImage imageWithData:data];
return resultImage;
}

3. 在大小有上限的情况下尽量保证质量

两种图片压缩方法结合 尽量兼顾质量和大小。以确保大小合适为标准。好处就是在大小限定的情况下最大保证了质量和尺寸。

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
38
- (UIImage *)compressWithQulitySize:(NSInteger)maxLength {
// Compress by quality
CGFloat compression = 1;
NSData *data = UIImageJPEGRepresentation(self, compression);
if (data.length < maxLength) return self;

CGFloat max = 1;
CGFloat min = 0;
for (int i = 0; i < 6; ++i) {
compression = (max + min) / 2;
data = UIImageJPEGRepresentation(self, compression);
if (data.length < maxLength * 0.9) {
min = compression;
} else if (data.length > maxLength) {
max = compression;
} else {
break;
}
}
UIImage *resultImage = [UIImage imageWithData:data];
if (data.length < maxLength) return resultImage;

// Compress by size
NSUInteger lastDataLength = 0;
while (data.length > maxLength && data.length != lastDataLength) {
lastDataLength = data.length;
CGFloat ratio = (CGFloat)maxLength / data.length;
CGSize size = CGSizeMake((NSUInteger)(resultImage.size.width * sqrtf(ratio)),
(NSUInteger)(resultImage.size.height * sqrtf(ratio))); // Use NSUInteger to prevent white blank
UIGraphicsBeginImageContext(size);
[resultImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
data = UIImageJPEGRepresentation(resultImage, compression);
}

return resultImage;
}

四、图片像素修改操作

写在前面:这部分的理论都是通过图片重绘来修改修该图片位图中的像素值,从而达到图片的修改。

4.1 图片灰度图(黑白图)

灰度图的三种颜色转换算法:

  1. 浮点算法:R = G = B = 0.3R + 0.59G + 0.11*B

  2. 平均值法:R = G = B = (R+G+B)/3

  3. 任取一个分量色:R = G = B = R或G或B

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
38
39
40
41
42
- (UIImage *)imageToGray:(NSInteger)type {
CGImageRef imageRef = self.CGImage;
//1、获取图片宽高
NSUInteger width = CGImageGetWidth(imageRef);
NSUInteger height = CGImageGetHeight(imageRef);
//2、创建颜色空间
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
//3、根据像素点个数创建一个所需要的空间
UInt32 *imagePiexl = (UInt32 *)calloc(width*height, sizeof(UInt32));
CGContextRef contextRef = CGBitmapContextCreate(imagePiexl, width, height, 8, 4*width, colorSpaceRef, kCGImageAlphaNoneSkipLast);

//4、根据图片数据源绘制上下文
CGContextDrawImage(contextRef, CGRectMake(0, 0, width, height), self.CGImage);
//5、将彩色图片像素点重新设置颜色
//取平均值 R=G=B=(R+G+B)/3
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
//计算平均值重新存储像素点-直接操作像素点
uint8_t *rgbPiexl = (uint8_t *)&imagePiexl[y*width+x];
//rgbPiexl[0],rgbPiexl[1],rgbPiexl[2];
//(rgbPiexl[0]+rgbPiexl[1]+rgbPiexl[2])/3;
uint32_t gray = rgbPiexl[0]*0.3+rgbPiexl[1]*0.59+rgbPiexl[2]*0.11;
if (type == 0) {
gray = rgbPiexl[1];
}else if(type == 1) {
gray = (rgbPiexl[0]+rgbPiexl[1]+rgbPiexl[2])/3;
}else if (type == 2) {
gray = rgbPiexl[0]*0.3+rgbPiexl[1]*0.59+rgbPiexl[2]*0.11;
}
rgbPiexl[0] = gray;
rgbPiexl[1] = gray;
rgbPiexl[2] = gray;
}
}
//根据上下文绘制
CGImageRef finalRef = CGBitmapContextCreateImage(contextRef);
//释放用过的内存
CGContextRelease(contextRef);
CGColorSpaceRelease(colorSpaceRef);
free(imagePiexl);
return [UIImage imageWithCGImage:finalRef scale:self.scale orientation:UIImageOrientationUp];
}

4.2 修改图片的RGB值

通过修改图片的RGB值来控制图片的颜色显示。或者替换某种颜色。

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
- (UIImage *)imageToRGB:(CGFloat)rk g:(CGFloat)gk b:(CGFloat)bk {
CGImageRef imageRef = self.CGImage;
//1、获取图片宽高
NSUInteger width = CGImageGetWidth(imageRef);
NSUInteger height = CGImageGetHeight(imageRef);
//2、创建颜色空间
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
//3、根据像素点个数创建一个所需要的空间
UInt32 *imagePiexl = (UInt32 *)calloc(width*height, sizeof(UInt32));
CGContextRef contextRef = CGBitmapContextCreate(imagePiexl, width, height, 8, 4*width, colorSpaceRef, kCGImageAlphaNoneSkipLast);
//4、根据图片数据源绘制上下文
CGContextDrawImage(contextRef, CGRectMake(0, 0, width, height), imageRef);
//5、将彩色图片像素点重新设置颜色
//取平均值 R=G=B=(R+G+B)/3
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
//操作像素点
uint8_t *rgbPiexl = (uint8_t *)&imagePiexl[y*width+x];
//该色值下不做处理
if (rgbPiexl[0]>245&&rgbPiexl[1]>245&&rgbPiexl[2]>245) {
NSLog(@"该色值下不做处理");
}else{
rgbPiexl[0] = rgbPiexl[0]*rk;
rgbPiexl[1] = rgbPiexl[1]*gk;
rgbPiexl[2] = rgbPiexl[2]*bk;
}
}
}
//根据上下文绘制
CGImageRef finalRef = CGBitmapContextCreateImage(contextRef);
//释放用过的内存
CGContextRelease(contextRef);
CGColorSpaceRelease(colorSpaceRef);
free(imagePiexl);
return [UIImage imageWithCGImage:finalRef scale:self.scale orientation:UIImageOrientationUp];
}

4.3 图片打码

马赛克就是让图片看上去模糊不清。将特定区域的像素点设置为同一种颜色,整体就会变得模糊,区域块越大越模糊,越小越接近于原始像素。

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
38
39
40
41
42
43
44
45
46
47
48
49
//设置马赛克
//马赛克就是让图片看上去模糊不清。将特定区域的像素点设置为同一种颜色,整体就会变得模糊,区域块越大越模糊,越小越接近于原始像素。
//同样使用强制解压缩操作,操作像素点,马赛克部分实际操作
//1、设置区域大小;
//2、在该区域获取一个像素点(第一个)作为整个区域的取色;
//3、将取色设置到区域中;
//4、取下一个区域同上去色设置区域
- (UIImage *)imageToMosaic:(NSInteger)size; {
CGImageRef imageRef = self.CGImage;
//1、获取图片宽高
NSUInteger width = CGImageGetWidth(imageRef);
NSUInteger height = CGImageGetHeight(imageRef);
//2、创建颜色空间
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
//3、根据像素点个数创建一个所需要的空间
UInt32 *imagePiexl = (UInt32 *)calloc(width*height, sizeof(UInt32));
CGContextRef contextRef = CGBitmapContextCreate(imagePiexl, width, height, 8, 4*width, colorSpaceRef, kCGImageAlphaNoneSkipLast);
//4、根据图片数据源绘制上下文
CGContextDrawImage(contextRef, CGRectMake(0, 0, width, height), imageRef);
//5、获取像素数组
UInt8 *bitmapPixels = CGBitmapContextGetData(contextRef);
UInt8 *pixels[4] = {0};
NSUInteger currentPixels = 0;//当前的像素点
NSUInteger preCurrentPiexls = 0;//
NSUInteger mosaicSize = size;//马赛克尺寸
if (size == 0) return self;
for (NSUInteger i = 0; i < height - 1; i++) {
for (NSUInteger j = 0 ; j < width - 1; j++) {
currentPixels = i * width + j;
if (i % mosaicSize == 0) {
if (j % mosaicSize == 0) {
memcpy(pixels, bitmapPixels + 4 * currentPixels, 4);
}else{
memcpy(bitmapPixels + 4 * currentPixels, pixels, 4);
}
}else{
preCurrentPiexls = (i - 1) * width + j;
memcpy(bitmapPixels + 4 * currentPixels, bitmapPixels + 4 * preCurrentPiexls, 4);
}
}
}
//根据上下文创建图片数据源
CGImageRef finalRef = CGBitmapContextCreateImage(contextRef);
//释放用过的内存
CGContextRelease(contextRef);
CGColorSpaceRelease(colorSpaceRef);
free(imagePiexl);
return [UIImage imageWithCGImage:finalRef scale:self.scale orientation:UIImageOrientationUp];
}

五、图形框架合成图片

使用不同图形框架合成图片,添加滤镜水印等。

写在前面:理论和上面像素修改一样,通过操作像素达到修改图片的目的,但是这里使用了系统提供的不同框架和第三方GPUImage。不同框架效率也有所不一样。这里每段代码都加入了对应像素(黑白处理),只是为了学习,后面可以根据需求在对应代码块添加或替换对应对像素的操作,亦可后面加入参数进行封装。

5.1 直接绘图合成

此方案原理上就是通过绘图,将多张图片的像素按照自己的设计绘制在一张图片上。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
- (UIImage *)processUsingPixels:(UIImage *)backImage frontImage:(UIImage *)frontImage; {
// 1. Get the raw pixels of the image
UInt32 * backPixels;

CGImageRef backCGImage = [backImage CGImage];
NSUInteger backWidth = CGImageGetWidth(backCGImage);
NSUInteger backHeight = CGImageGetHeight(backCGImage);

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

NSUInteger bytesPerPixel = 4;
NSUInteger bitsPerComponent = 8;

NSUInteger backBytesPerRow = bytesPerPixel * backWidth;

backPixels = (UInt32 *)calloc(backHeight * backWidth, sizeof(UInt32));

CGContextRef context = CGBitmapContextCreate(backPixels, backWidth, backHeight,
bitsPerComponent, backBytesPerRow, colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

CGContextDrawImage(context, CGRectMake(0, 0, backWidth, backHeight), backCGImage);

// 2. Blend the pattern onto the image
CGImageRef frontCGImage = [frontImage CGImage];

// 2.1 Calculate the size & position of the pattern
CGFloat frontImageAspectRatio = frontImage.size.width / frontImage.size.height;
NSInteger targetFrontWidth = backWidth * 0.25;
CGSize frontSize = CGSizeMake(targetFrontWidth, targetFrontWidth / frontImageAspectRatio);
// CGPoint frontOrigin = CGPointMake(backWidth * 0.5, backHeight * 0.2);
CGPoint frontOrigin = CGPointMake(0, 0);

// 2.2 Scale & Get pixels of the pattern
NSUInteger frontBytesPerRow = bytesPerPixel * frontSize.width;

UInt32 *frontPixels = (UInt32 *)calloc(frontSize.width * frontSize.height, sizeof(UInt32));

CGContextRef frontContext = CGBitmapContextCreate(frontPixels, frontSize.width, frontSize.height,
bitsPerComponent, frontBytesPerRow, colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);

CGContextDrawImage(frontContext, CGRectMake(0, 0, frontSize.width, frontSize.height),frontCGImage);

// 2.3 Blend each pixel
NSUInteger offsetPixelCountForInput = frontOrigin.y * backWidth + frontOrigin.x;
for (NSUInteger j = 0; j < frontSize.height; j++) {
for (NSUInteger i = 0; i < frontSize.width; i++) {
UInt32 *backPixel = backPixels + j * backWidth + i + offsetPixelCountForInput;
UInt32 backColor = *backPixel;

UInt32 * frontPixel = frontPixels + j * (int)frontSize.width + i;
UInt32 frontColor = *frontPixel;

// Blend the pattern with 50% alpha
// CGFloat frontAlpha = 0.5f * (A(frontColor) / 255.0);
CGFloat frontAlpha = 1.0f * (A(frontColor) / 255.0);
UInt32 newR = R(backColor) * (1 - frontAlpha) + R(frontColor) * frontAlpha;
UInt32 newG = G(backColor) * (1 - frontAlpha) + G(frontColor) * frontAlpha;
UInt32 newB = B(backColor) * (1 - frontAlpha) + B(frontColor) * frontAlpha;

//Clamp, not really useful here :p
newR = MAX(0,MIN(255, newR));
newG = MAX(0,MIN(255, newG));
newB = MAX(0,MIN(255, newB));

*backPixel = RGBAMake(newR, newG, newB, A(backColor));
}
}

// 3. Convert the image to Black & White
for (NSUInteger j = 0; j < backHeight; j++) {
for (NSUInteger i = 0; i < backWidth; i++) {
UInt32 * currentPixel = backPixels + (j * backWidth) + i;
UInt32 color = *currentPixel;

// Average of RGB = greyscale
UInt32 averageColor = (R(color) + G(color) + B(color)) / 3.0;

*currentPixel = RGBAMake(averageColor, averageColor, averageColor, A(color));
}
}

// 4. Create a new UIImage
CGImageRef newCGImage = CGBitmapContextCreateImage(context);
UIImage * processedImage = [UIImage imageWithCGImage:newCGImage];

// 5. Cleanup!
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CGContextRelease(frontContext);
free(backPixels);
free(frontPixels);

return processedImage;
}

5.2 CoreGraphics 框架合成图片

使用CoreGraphics框架合成图片

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
- (UIImage *)processUsingCoreGraphics:(UIImage *)backImage frontImage:(UIImage *)frontImage; {
CGRect imageRect = {CGPointZero,backImage.size};
NSInteger backWidth = CGRectGetWidth(imageRect);
NSInteger backHeight = CGRectGetHeight(imageRect);

// 1. Blend the pattern onto our image
CGFloat frontImageAspectRatio = frontImage.size.width / frontImage.size.height;

NSInteger targetFrontWidth = backWidth * 0.25;
CGSize frontSize = CGSizeMake(targetFrontWidth, targetFrontWidth / frontImageAspectRatio);
// CGPoint frontOrigin = CGPointMake(backWidth * 0.5, backHeight * 0.2);
CGPoint frontOrigin = CGPointMake(0, 0);

CGRect frontRect = {frontOrigin, frontSize};

UIGraphicsBeginImageContext(backImage.size);
CGContextRef context = UIGraphicsGetCurrentContext();

// flip drawing context
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);
CGAffineTransform flipThenShift = CGAffineTransformTranslate(flip,0,-backHeight);
CGContextConcatCTM(context, flipThenShift);

// 1.1 Draw our image into a new CGContext
CGContextDrawImage(context, imageRect, [backImage CGImage]);

// 1.2 Set Alpha to 0.5 and draw our pattern on
CGContextSetBlendMode(context, kCGBlendModeSourceAtop);
CGContextSetAlpha(context,0.5);
CGRect transformedpatternRect = CGRectApplyAffineTransform(frontRect, flipThenShift);
CGContextDrawImage(context, transformedpatternRect, [frontImage CGImage]);

UIImage * imageWithFront = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// 2. Convert our image to Black and White

// 2.1 Create a new context with a gray color space
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
context = CGBitmapContextCreate(nil, backWidth, backHeight,
8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaNone);

// 2.2 Draw our image into the new context
CGContextDrawImage(context, imageRect, [imageWithFront CGImage]);

// 2.3 Get our new B&W Image
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage * finalImage = [UIImage imageWithCGImage:imageRef];

// Cleanup
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CFRelease(imageRef);

return finalImage;
}

5.3 CoreImage 框架合成图片

使用CoreImage 框架以添加滤镜形式合成图片

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
38
39
- (UIImage *)processUsingCoreImage:(UIImage *)backImage frontImage:(UIImage *)frontImage {
CIImage * backCIImage = [[CIImage alloc] initWithImage:backImage];

// 1. Create a grayscale filter
CIFilter * grayFilter = [CIFilter filterWithName:@"CIColorControls"];
[grayFilter setValue:@(0) forKeyPath:@"inputSaturation"];

// 2. Create our pattern filter

// Cheat: create a larger pattern image
UIImage * patternFrontImage = [self createPaddedPatternImageWithSize:backImage.size pattern:frontImage];
CIImage * frontCIImage = [[CIImage alloc] initWithImage:patternFrontImage];

CIFilter * alphaFilter = [CIFilter filterWithName:@"CIColorMatrix"];
// CIVector * alphaVector = [CIVector vectorWithX:0 Y:0 Z:0.5 W:0];
CIVector * alphaVector = [CIVector vectorWithX:0 Y:0 Z:1.0 W:0];
[alphaFilter setValue:alphaVector forKeyPath:@"inputAVector"];

CIFilter * blendFilter = [CIFilter filterWithName:@"CISourceAtopCompositing"];

// 3. Apply our filters
[alphaFilter setValue:frontCIImage forKeyPath:@"inputImage"];
frontCIImage = [alphaFilter outputImage];

[blendFilter setValue:frontCIImage forKeyPath:@"inputImage"];
[blendFilter setValue:backCIImage forKeyPath:@"inputBackgroundImage"];
CIImage * blendOutput = [blendFilter outputImage];

[grayFilter setValue:blendOutput forKeyPath:@"inputImage"];
CIImage * outputCIImage = [grayFilter outputImage];

// 4. Render our output image
CIContext * context = [CIContext contextWithOptions:nil];
CGImageRef outputCGImage = [context createCGImage:outputCIImage fromRect:[outputCIImage extent]];
UIImage * outputImage = [UIImage imageWithCGImage:outputCGImage];
CGImageRelease(outputCGImage);

return outputImage;
}

createPaddedPatternImageWithSize 这是个生成滤镜图案的代码块具体请看DEMO

5.4 GPUImage 框架合成图片

使用GPUImage 框架以添加滤镜形式合成图片

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
- (UIImage *)processUsingGPUImage:(UIImage *)backImage frontImage:(UIImage *)frontImage {

// 1. Create our GPUImagePictures
GPUImagePicture * backGPUImage = [[GPUImagePicture alloc] initWithImage:backImage];

UIImage *fliterImage = [self createPaddedPatternImageWithSize:backImage.size pattern:frontImage];
GPUImagePicture * frontGPUImage = [[GPUImagePicture alloc] initWithImage:fliterImage];

// 2. Setup our filter chain
GPUImageAlphaBlendFilter * alphaBlendFilter = [[GPUImageAlphaBlendFilter alloc] init];
alphaBlendFilter.mix = 0.5;

[backGPUImage addTarget:alphaBlendFilter atTextureLocation:0];
[frontGPUImage addTarget:alphaBlendFilter atTextureLocation:1];

GPUImageGrayscaleFilter * grayscaleFilter = [[GPUImageGrayscaleFilter alloc] init];

[alphaBlendFilter addTarget:grayscaleFilter];

// 3. Process & grab output image
[backGPUImage processImage];
[frontGPUImage processImage];
[grayscaleFilter useNextFrameForImageCapture];

UIImage * output = [grayscaleFilter imageFromCurrentFramebuffer];

return output;
}

5.5 对比总结

  • 从代码量来看:明显1直接绘图合成的代码量明显高出许多。CoreImage,和GPUImage的方案要自己加入pattern图,其实代码量也不算少。因此仅从合成图这个功能来看。代码量上 CoreGraphic方案最优。
  • 从性能来看:本地测试,CoreGraphic,直接绘图合成,速度最快。GPUImage也差不多,CoreImage添加滤镜方案最慢。
  • 从可控多样性需求来说:GPUImage本来就提供很多滤镜,同时开源。无疑当前最佳,但是其他的都可以自己进行对应功能封装。

总的来说还是要看项目需求,个人觉得一般性添加水印,合成图片什么如果要直接用CoreGraphic是个不错的选择,以后有时间可以基于CoreGraphic封装功能。

六、参考文档

Author:Tenloy

原文链接:https://tenloy.github.io/2021/09/15/graphics-processing-case.html

发表日期:2021.09.15 , 3:54 PM

更新日期:2024.04.07 , 8:02 PM

版权声明:本文采用Crative Commons 4.0 许可协议进行许可

CATALOG
  1. 一、图片手动解码
  2. 二、大图在本地的高效显示
  3. 三、图片压缩
    1. 3.1 按照指定压缩比例压缩图片
    2. 3.2 按照指定尺寸压缩图片
    3. 3.3 具体的应用场景分析
      1. 1. 上传或存储有大小要求的图片
      2. 2. 上传或存储有质量要求的图片
      3. 3. 在大小有上限的情况下尽量保证质量
  4. 四、图片像素修改操作
    1. 4.1 图片灰度图(黑白图)
    2. 4.2 修改图片的RGB值
    3. 4.3 图片打码
  5. 五、图形框架合成图片
    1. 5.1 直接绘图合成
    2. 5.2 CoreGraphics 框架合成图片
    3. 5.3 CoreImage 框架合成图片
    4. 5.4 GPUImage 框架合成图片
    5. 5.5 对比总结
  6. 六、参考文档