Tenloy's Blog

图形处理(一) - 图片的加载与编解码

Word count: 4.4kReading time: 17 min
2021/09/13 Share

一、位图、色彩空间

1.1 色彩空间

色彩空间(Color space)是对色彩的组织方式/表示方式。借助色彩空间和针对物理设备的测试,可以得到色彩的固定模拟数字表示。数字表示,如Adobe RGB、sRGB、RGBA。

色彩空间并不唯一,比如:

  • 当在计算机监视器上显示颜色的时候,通常使用RGB(红色、绿色、蓝色)色彩空间定义,这是另外一种生成同样颜色的方法,红色、绿色、蓝色被当作X、Y和Z坐标轴。
  • 许多人都知道在绘画时可以使用红色、黄色和蓝色这三种原色生成不同的颜色,这些颜色就定义了一个色彩空间。我们将品红色的量定义为X 坐标轴、青色的量定义为Y坐标轴、黄色的量定义为Z坐标轴,这样就得到一个三维空间,每种可能的颜色在这个三维空间中都有唯一的一个位置。
  • 另外一个生成同样颜色的方法是使用色相(X轴)、饱和度(色度)(Y轴)和明度(Z轴)表示,这种方法称为HSV色彩空间。
  • 另外还有许多其它的色彩空间,许多可以按照这种方法用三维(X、Y、Z)、更多或者更少维表示,但是有些根本不能用这种方法表示。

常见的六种色彩空间:RGB、HSV、CMY、HSL、Lab、YUV

1.2 像素格式(pixel format)

1.2.1 像素格式

像素格式(pixel format)是指像素色彩分量的大小和排列。

  • 规定了每个像素所使用的总位数以及用于存储像素色彩的红、绿、蓝和 alpha 分量的位数。(每个分量也称为通道)
  • 描述了像素数据存储所用的格式。定义了像素在内存中的编码方式。

1.2.2 常见的像素格式

  • 位组格式Byte formats(PF_BYTE_*):每个通道对应一个byte
  • Short格式(PF_SHORT_*):每个通道对应一个unsigned short数据(16 bit整型)
  • Float16 格式(PF_FLOAT16_*):每个通道对应一个16 bit 浮点数
  • Float32格式(PF_FLOAT32_*):每个通道对应一个32 bit 浮点数
  • 压缩格式formats (PF_DXT[1-5]):S3TC压缩纹理格式
  • 本地格式 (PF_A8R8G8B8 以及其他大量的不同的类型):这意味着在内存中使用了本地储存方式(big endian或者little endian,包括16,24,32位)的整形数据。同时意味着可以把PF_A8R8G8B8格式的图片看作一个32位的整形数组,在16进制表现为0xAARRGGBB。这些字母的意义我们在稍后会提供。

1.2.3 颜色通道

颜色通道中R,G,B,A,L 以及 X 的意义是:

R:红色成分,通常范围从0.0(没有红色)到1.0(全部的红色)。

G:绿色成分,通常范围从0.0(没有绿色)到1.0(全部的绿色)。

B:蓝色成分,通常范围从0.0(没有蓝色)到1.0(全部的蓝色)。

A:alpha(不透明度)成分,通常范围从0.0(完全透明)到1.0(不透明)。

L:亮度成分,通常范围从0.0(黑暗)到1.0(全白)。最终这个成分会被分散到RGB每个中完成最终的图像效果。

X:这个是被系统忽略的成分。

对于RGBL通道来说,默认的情况下设置为0。而Alpha通道却不同,在默认的情况下被设定为1,代表不透明。

1.3 位图

A bitmap image (or sampled(采样) image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.

位图又被叫做点阵图像,也就是说位图包含了一大堆的像素点信息,这些像素点就是该图片中的点,有了图片中每个像素点的信息,就可以在屏幕上渲染整张图片了。

一堆像素点组成的二维数组,其中每个像素点都记录该点位的颜色等信息。显示出来就是一张图了。

既然像素要存储颜色数据,这里就又引出一个颜色存储格式的概念。我们就以最简单普遍的PF_BYTE_RGBA (32-bit RGBA) 像素格式为例子,一个像素点存储的色彩所需空间是32bits或是4bytes、1byte或8bit存储是一个通道,对应下来就是:

  • R = red (占1byte或8bit)
  • G = green (占1byte或8bit)
  • B = blue (占1byte或8bit)
  • A = alpha (占1byte或8bit)

这样你就知道 32-bit RGBA 格式能够显示的颜色是 2^8 * 2^8* 2^8 (256 * 256 * 256),将近一千七百多万个颜色。还有颜色空间(Color Spaces)的概念这里就不再扩展了。

而位图是装载像素点的数组,这样你大概可以理解下一张普通位图包含着多少数据!同时,这里解释颜色是为了下面计算位图大小,便于理解我们为什么要进行图片编码。

1.4 补充:矢量图

矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由点连接的线。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。

矢量图是根据几何特性来绘制图形,矢量可以是一个点或一条线,矢量图只能靠软件生成,文件占用内在空间较小,因为这种类型的图像文件包含独立的分离图像,可以自由无限制的重新组合。它的特点是放大后图像不会失真,和分辨率无关,适用于图形设计、文字设计和一些标志设计、版式设计等。

矢量图的特点

  1. 文件小,图像中保存的是线条和图块的信息,所以矢量图形文件与分辨率和图像大小无关,只与图像的复杂程度有关,图像文件所占的存储空间较小。
  2. 图像可以无级缩放,对图形进行缩放,旋转或变形操作时,图形不会产生锯齿效果。
  3. 可采取高分辨率印刷,矢量图形文件可以在任何输出设备打印机上以打印或印刷的最高分辨率进行打印输出。
  4. 最大的缺点是难以表现色彩层次丰富的逼真图像效果。
  5. 矢量图与位图的效果是天壤之别。矢量图无限放大不模糊,大部分位图都是由矢量导出来的,也可以说矢量图就是位图的源码,源码是可以编辑的。

二、图片、编解码

2.1 编码与解码

图片的编码: 在当前APP的开发中,图片是经常会使用到的,关于图片有很多种格式,例如JPEG,PNG等。其实这些各种各样的图片格式都对应了位图(bitmap)经过不同算法编码(压缩)后的图片。 苹果提供2种图片编码格式,PNG和JPEG:

  • PNG图片是无损压缩,并且支持alpha通道
  • JPEG图片则是有损压缩,可以指定0-100%的压缩比。

图片的解码:

  • app从磁盘中读入编码后的图片,需要经过解码把图片变成位图(bitmap)读入,这样才能显示在屏幕上。
  • iOS 默认会在主线程对图像进行解码,解压缩后的图片大小与原始文件大小之间没有任何关系,而只与图片的像素有关:
1
位图大小 = 图片的像素宽  * 图片的像素高  * 每个像素所占的字节数(取决于像素格式)

2.2 位图为什么要压缩编码

都知道,图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。既然如此,图片不编码也就不用解码,都使用位图可以吗?

举例:一张位图的宽和高分别都是100个像素,那这个位图的大小是多少呢?

1
2
3
4
//计算一张位图size的公式
//bytesPerPixel每个像素点所需空间
//32-bit RGBA 格式图片 bytesPerPixel = 4 (R,G,B,A各一个byte),理论看上面
size = width * height * bytesPerPixel

这样把我们100x100 的位图代入该公式,可以得到其大小:

1
size = 100 * 100 * 4 = 40000B = 39KB

正常一张PNG或JPEG格式的100x100的图片,大概只有几KB。如果更大的图,位图所占空间更大,所以位图必须进行编码进行存储。

2.3 硬解码与软解码

硬解码:由显卡核心GPU来对高清视频进行解码工作,通过解码电路实现,CPU占用率很低,画质效果比软解码略差一点,需要对播放器进行设置。(省电、对硬件要求高)

  • 优点:播放流畅、低功耗
  • 缺点:受视频格式限制、功耗大、画质没有软解码好

软解码:由CPU负责解码,通过解码算法、CPU 的通用计算等方式实现软件层面的解码,效率不如 GPU 硬解码。(省电、对硬件要求不高)

  • 优点:不受视频格式限制、画质略好于硬解
  • 缺点:会占用过高的资源、对于高清视频可能没有硬解码流畅(主要看CPU的能力)

三、图片的加载及解码流程

3.1 图片的加载

3.1.1 三种 Buffer 理念

通常 Buffer 是表示一片连续的内存空间。在这里,我们说的 Buffer 是指一系列内部结构相同、大小相同的元素组成的内存区域。有三种Buffer:Data Buffer、Image Buffer、Frame Buffer。这个理论是2018WWDC苹果上描述的概念,具体可看Image and Graphics Best Practices

  • Data Buffer 是指存储在内存中的原始数据,图像可以使用不同的格式保存,如 jpg、png。Data Buffer 的信息不能用来描述图像的位图像素信息。
  • Image Buffer 是指图像在内存中的存在方式,其中每个元素描述了一个像素点。Image Buffer 的大小和位图的大小相等。
  • Frame Buffer 和 Image Buffer 内容相同,不过其存储在 vRAM(video RAM)中,而 Image Buffer 存储在 RAM 中。

3.1.2 两种生成UIImage的方法

iOS中根据本地icon加载出一个UIImage对象有两种方法:

1
2
UIImage *img1 =  [UIImage imageNamed:@"pic1"];
UIImage *img2 = [UIImage imageWithContentsOfFile:filePath];

imageNamed方法

  • 后面的参数是icon的名字。图片可以存在项目中,也可以存在Assets中。
  • 这个方法加载完图片后会存在Cache里面,当用这个方法加载的时候,它会在系统缓存中查找并返回一个对象,如果缓存中没有找到对应的对象,这个方法会从指定的文档中加载,再返回对象。
  • 优点是提高了运行速度,缺点是消耗内存。如果是不会复用的大图,最好不要用该方法加载。

imageWithContentsOfFile方法

  1. 后面的参数是图片的路径,格式是字符串
  2. 加载图片时,会根据路径查找,直接加载,使用完后释放,不会存入内存.
  3. 优点是节省内存,缺点是消耗性能。适用于一些不常用的图片或icon,或者资源比较大图片。

总结:一些小的icon可以存在Assets里面,用imageNamed加载。一些比较大的、使用频率低的可以建立一个bundle存放图片,使用imageWithContentsOfFile 加载。

3.3 图片的解码

3.3.1 解码触发时机

UIImage 是 iOS 中处理图像的高级类。创建一个 UIImage 实例只会加载 Data Buffer,也就是说以上只是把图片转为UIImage对象,该对象存储在Data Buffer里。此时并没有对图片进行解码。

当将图像显示到屏幕上会触发隐式解码(必须同时满足图像被设置到 UIImageView 中、UIImageView 添加到视图,才会触发图像解码)。也就是说你就算实例了一个UIImageView,但是没有把他addSubview,显示到视图上,系统也是不会进行解码的。

3.3.2 主线程解码的性能问题

这个解码过程默认是发生在主线程上面的,而且非常消耗 CPU,所以到如果在 tableView 或者 collectionView 中有相当多的图片需要显示的话,这些图片在主线程的解码操作必然会影响滑动的顺畅度。所以我们是否可以在子线程强制将其解码,然后在主线程让系统渲染解码之后的图片呢?当然可以,现在基本上所有的开源图片库都会实现这个操作。例如:YYImage\SDWebImage。

3.3.3 手动解码的原理

自己手动解码的原理就是对图片进行重新绘制,得到一张新的解码后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate :

1
2
3
4
5
6
7
8
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(
void * __nullable data,
size_t width,
size_t height,
size_t bitsPerComponent,
size_t bytesPerRow,
CGColorSpaceRef cg_nullable space,
uint32_t bitmapInfo) CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

这个方法是创建一个图片处理的上下文 CGContext 对象,因为上面方法的返回值 CGContextRef 实际上就是 CGContext *。关于这个函数的详细讲解博文有很多,官方文档CGBitmapContextCreate。博客图片解码

3.3.4 开源框架解码实现

开源框架的解决方案基础也是基于这个API:

1. YYImage 中的解码
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
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
if (!imageRef) return NULL;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == 0 || height == 0) return NULL;

if (decodeForDisplay) { //decode with redraw (may lose some precision)
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return newImage;

} else {
...
}
}

实际上, 这个方法的作用是创建一个图像的拷贝,它接受一个原始的位图参数 imageRef ,最终返回一个新的解码后的位图 newImage ,中间主要经过了以下三个步骤:

  • 使用 CGBitmapContextCreate 函数创建一个位图上下文;
  • 使用 CGContextDrawImage 函数将原始位图绘制到上下文中;
  • 使用 CGBitmapContextCreateImage 函数创建一张新的解压缩后的位图。
2. SDWebImage的解码实现

事实上,SDWebImage 中对图片的解压缩过程与上述完全一致,只是传递给 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
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
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
if (![UIImage shouldDecodeImage:image]) {
return image;
}

// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{

CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];

size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
size_t bytesPerRow = kBytesPerPixel * width;

// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}

// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];

CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);

return imageWithoutAlpha;
}
}

+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}

// do not decode animated images
if (image.images != nil) {
return NO;
}

CGImageRef imageRef = image.CGImage;

CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
// do not decode images with alpha
if (anyAlpha) {
return NO;
}

return YES;
}

SDWebImage 中和其他不一样的地方,就是如果一张图片有 alpha 分量,那就直接返回原始图片,不再进行解码操作。这么做是因为alpha 分量不可知,为了保证原图完整信息故不做处理。

SDWebImage 在解码操作外面包了 autoreleasepool,这样在大量图片需要解码的时候,可以使得局部变量尽早释放掉,不会造成内存峰值过高。

四、大图显示

大的图片会占用较多的内存资源,解码和传输到 GPU 也会耗费较多时间。 因此,实际需要显示的图像尺寸可能并不是很大,如果能将大图缩小,便能达到优化的目的。

以下是WWDC给的大图显示方案,功能是缩小图像并解码:

4.1 Objective-C:

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
// 大图缩小为显示尺寸的图
- (UIImage *)downsampleImageAt:(NSURL *)imageURL to:(CGSize)pointSize scale:(CGFloat)scale {
// 利用图像文件地址创建 image source
NSDictionary *imageSourceOptions =
@{
(__bridge NSString *)kCGImageSourceShouldCache: @NO // 原始图像不要解码
};
CGImageSourceRef imageSource =
CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, (__bridge CFDictionaryRef)imageSourceOptions);

// 下采样
CGFloat maxDimensionInPixels = MAX(pointSize.width, pointSize.height) * scale;
NSDictionary *downsampleOptions =
@{
(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageAlways: @YES,
(__bridge NSString *)kCGImageSourceShouldCacheImmediately: @YES, // 缩小图像的同时进行解码
(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform: @YES,
(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize: @(maxDimensionInPixels)
};
CGImageRef downsampledImage =
CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)downsampleOptions);
UIImage *image = [[UIImage alloc] initWithCGImage:downsampledImage];
CGImageRelease(downsampledImage);
CFRelease(imageSource);

return image;
}

4.2 Swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Downsampling large images for display at smaller size
func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions =
[kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary

let downsampledImage =
CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
return UIImage(cgImage: downsampledImage)
}

参考文档

Author:Tenloy

原文链接:https://tenloy.github.io/2021/09/13/image-encode-decode.html

发表日期:2021.09.13 , 9:01 AM

更新日期:2024.04.07 , 8:02 PM

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

CATALOG
  1. 一、位图、色彩空间
    1. 1.1 色彩空间
    2. 1.2 像素格式(pixel format)
      1. 1.2.1 像素格式
      2. 1.2.2 常见的像素格式
      3. 1.2.3 颜色通道
    3. 1.3 位图
    4. 1.4 补充:矢量图
  2. 二、图片、编解码
    1. 2.1 编码与解码
    2. 2.2 位图为什么要压缩编码
    3. 2.3 硬解码与软解码
  3. 三、图片的加载及解码流程
    1. 3.1 图片的加载
      1. 3.1.1 三种 Buffer 理念
      2. 3.1.2 两种生成UIImage的方法
    2. 3.3 图片的解码
      1. 3.3.1 解码触发时机
      2. 3.3.2 主线程解码的性能问题
      3. 3.3.3 手动解码的原理
      4. 3.3.4 开源框架解码实现
        1. 1. YYImage 中的解码
        2. 2. SDWebImage的解码实现
  4. 四、大图显示
    1. 4.1 Objective-C:
    2. 4.2 Swift
  5. 参考文档