理解图像(一)- 基础

1,355 阅读11分钟

理解图像(一)- 基础
理解图像(二)- 解码、编码(基于ImageIO)

位图(Bitmap)

1.什么是位图?

位图是由一系列连续的像素点组成的二位数组,又被叫做点阵图像。位图中每个像素点的数据记录了图像中该点位的颜色等信息,最终将所有点位的颜色渲染出来就组成一张图像了。

2.位图的类型

存储位图的磁盘文件通常包括一个或多个信息块,用于记录数组的行数、每行的像素数、每个像素的位数等。

  • 普通位图(直接存储颜色本身)
    位图中的每个像素存储的是颜色本身,如下图右侧部分,为每个像素点对应的颜色放大图。

  • 调色板索引位图
    在位图文件的信息块中,还有可能会包含一个颜色表(也成调色板),而位图中每个元素存储的是颜色表中的索引,在解码时会进行替换。

3.位图的大小

位图中每个像素点所占的位数决定了能够分配给该像素的颜色的数量,示例如下:

每像素位数可以分配给像素的颜色数
12^1 = 2
22^2 = 4
42^4 = 16
82^8 = 256
162^16 = 65,536
242^24 = 16,777,216

因此每一位分配的颜色数量越多则占用的内存空间越大,计算公式如下:
位图大小 = 图片的像素宽 * 图片的像素高 * 每个像素所占的字节数(取决于像素格式)

例:一张 RGBA 格式图像的宽、高分别是 100 像素,它的位图大小是多少?

//公式:size = width * height * bytesPerPixel 
// RGBA 格式图像 bytesPerPixel = 4 byte,即 32 bit,R、G、B、A 各一个 byte
size = 100 * 100 * 4 = 40000B ≈ 39KB

从上得知,一张100x100 像素的图像的位图有几十K;如果更大的图像,位图所占的空间会更大,所以位图必须进行压缩(编码)存储。

图像格式

1.栅格图像文件格式

网络上最常见的光栅图像格式包括:JPEG、GIF、PNG 三种。
栅格图形文件类型为静态图像,无法随意调整图像的大小,否则原始设计和像素会被简单的拉伸来填充额外的空间,导致产生模糊、像素化、失真的图像。

  • JPEG(或JPG)
    全称为 Joint Photographic Experts Group(联合图像专家组),是一种有损压缩的光栅图像文件格式,文件的扩展名是 jpg。所谓“有损的” 图像,也就是当压缩文件大小时,在一定程度上会减低图像的质量。JPEG 压缩不适用于线条图、纯色块和清晰边界。

  • GIF
    全称为Graphics Interchange Format(图形交换格式),GIF 格式是最常用的动画图像,支持无损压缩,并且将图像的每个像素约束最大为 8 bits,因此被限制为 256 种颜色。8-bit的限制保证了动画体积更小,但缺点是图像的质量受限。

  • PNG
    全称为Portable Network Graphics PNG(便携式网络图形),支持无损压缩,相比 JPEG 具有更好的文本可读性,成为信息图形、文本图形等的最佳表达方式。PNG 文件还可以为每个像素存储一个 Alpha 值。

2.矢量图像文件格式

常见的矢量图像文件类型有:SVG、EPS、AI、PDF等。
与静态栅格图像文件格式(每个设计形状和颜色都与一个像素相关联)不同,矢量图像格式更加灵活。可以无限扩大原始图像分辨率,又不会损失质量或失真。有关矢量图相关类型此处不再详细展开介绍。

3.小结

矢量图像:一般称为图形,即它是一种几何形状,而不是单纯的像素图。

光栅图像:是位图,则称为图像,在固定的尺寸内包含一定数量的像素。

图像显示的过程

主要介绍一下图像从磁盘中读入到显示到屏幕过程中,都经历了什么,以及一些基础概念。

1.三种 Buffer 的概念

通常 Buffer 表示一片连续的内存空间。在这里,Buffer 可以理解为图像数据在不同时机所处的内存区域,有三种:Data Buffer、Image Buffer、Frame Buffer。

  • Data Buffer
    Data Buffer是指存在磁盘中的原始数据,图像可以使用不同格式存储,如 jpg、png 等。是图像被压缩后存储的方式。

  • Image Buffer
    Image Buffer 是指图像在内存中存在的方式,其中每个元素描述了一个像素点。Image Buffer 的大小和位图的大小相等。

  • Frame Buffer
    Frame Buffer 和 Image Buffer 内容相同,是即将渲染的最终形态,它存储在 vRAM 中(video RAM),而 Image Buffer 存储在 RAM 中。

2.加载过程

图像的加载过程,可简单理解为图像数据在内存中以不同形式的Buffer 流转的过程,如下图:

加载的过程依次是:

  • 创建 UIImage 对象 (Data Buffer -> Image Buffer)
    加载磁盘中的图像共有两种方式(优缺点此处不再展开),分别是:
    1)imageNamed:
    2)imageWithContentsOfFile:

此时,只是将图像转换为 UIImage 对象,将对象存储在Data Buffer 中,并没有对图像进行解码。

  • 赋值给 UIImageView  (Frame Buffer)
    将 UImage 对象赋值给 UIImageView 后,会发生的变化有:
    1)隐式 的 CATransaction 捕获到 UIImageView 图层树的变化;
    2)在主线程下一个 RunLoop 到来时,Core Animation 会提交这个 transaction,这个过程可能会对图像进行 copy 操作,这个 copy 操作可能会涉及以下步骤:
    • 分配内存缓冲区(用于管理文件 IO、解压缩等)
    • 将图像解压缩成位图形式(CPU 耗时操作)
    • 最后,Core Animation 使用解压缩的位图数据渲染 UIImageView 的图层

注意:必须同时满足图像被设置到 UIImageView 中、UIImageView 添加到视图,才会触发图像解码。

3.渲染的大致过程

  1. 当接收Vsync 信号后,主线程开始在 CPU 上做计算
  2. CPU 计算:视图的创建、布局计算、图像解码等
  3. GPU 渲染:CPU 将计算结果提交给 GPU,GPU 进行变换、合成、渲染
  4. GPU 提交:GPU 将渲染结果提交到帧缓冲区,等待下次 VSync 信号到来后上屏显示

常见的图像压缩方案

为什么要压缩图像呢?
在开发过程中,图像可以说是最大的对象了,考虑到手机性能、网络带宽、以及服务器压力等不同场景下的各种限制。比如当我们需要上传一张 20M 的大图时,在网速比较慢时,用户可能需要等待很久,甚至直接就退出了应用,非常影响用户体验。因此,在尽可能保证图像质量的情况下,对图像进行压缩处理就显得尤为必要了。
图像的压缩可以通过多种方式实现,一是压图像质量,不改变像素数、不改变尺寸;另一种方式是缩图像尺寸,也减少了像素数,同时也减少了体积。下面来看一下这两种方案的代码实现。

压质量(Quality)

对图像体积的直接压缩,一遍通过系统提供的 API 实现,如下:

// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
UIKIT_EXTERN NSData *UIImagePNGRepresentation(UIImage *image); 

 // return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)                              
UIKIT_EXTERN NSData *UIImageJPEGRepresentation(UIImage *image, CGFloat compressionQuality); 
  • UIImagePNGRepresentation
    • 无损压缩
    • 比较耗时,可能造成卡顿
  • UIImageJPEGRepresentation
    • 有损压缩
    • 耗时少,会剔除图像中无用的信息
    • 可以配置压缩系数(范围:0.0~1.0),系数越小,压缩后的图像越小,自然图像质量越差需要注意的是,当图像质量低于一定程度时,继续压缩没有效果。也就是说,compression继续减小,data 也不再继续减小。

总之,以上两个函数虽然可以直接对图像进行压缩,但无法确定是否压缩到阈值范围内,无法实现精确压缩。

缩尺寸(Size)

通过上面压缩图像质量的方式无法实现精确压缩,有时候无法满足我们的需求,因此还可以通过压缩图像的尺寸的方式来进一步压缩图像。原理是通过系统 API 对图像进行重绘,从而得到尺寸更小的图像,源码如下:

//压缩到指定 Size
- (UIImage*)compressImage:(UIImage*)sourceImage toTargetSize:(CGFloat)size {
    UIGraphicsBeginImageContext(size);
    [sourceImage drawInRect:CGRectMake(0,0, size.width, size.height)];
    UIImage*newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}

如上这种压缩方式,可将图像压缩到指定的大小尺寸内,改变了原图的尺寸,无法保证图像的质量,会使图像明显模糊(比压缩图像质量模糊)。

综上:

  • 如果要保证图像清晰度,建议选择压缩图像质量。
  • 如果要使图像一定小于指定大小,压缩图像尺寸可以满足。

压质量 + 缩尺寸结合

如果需求不仅要保证图像质量,同时对图像的尺寸大小也有限制,我们可以优先压质量,如果还达不到要求,可以在此基础之后,继续采取缩尺寸的方式处理,直到达到要求为止。可参考下面代码:

/*!
 *  @brief 使图像压缩后刚好小于指定大小
 *
 *  @param image 当前要压缩的图 maxLength 压缩后的大小
 *
 *  @return 图像对象
 */
//图像质量压缩到某一范围内
- (UIImage *)compressImageSize:(UIImage *)image toByte:(NSUInteger)maxLength{
    //首先判断原图大小是否在要求内,如果满足要求则不进行压缩 
    CGFloat compression = 1;
    NSData *data = UIImageJPEGRepresentation(image, compression);
    if (data.length < maxLength) 
      return image;
    //原图大小超过范围,先进行“压处理”,这里压缩比 采用二分法进行处理,6次二分后的最小压缩比是0.015625,已经够小了
    CGFloat max = 1;
    CGFloat min = 0;
    for (int i = 0; i < 6; ++i) {
        compression = (max + min) / 2;
        data = UIImageJPEGRepresentation(image, 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;
    
    //缩尺寸
    //直接用大小的比例作为缩处理的比例进行处理,因为有取整处理,所以一般是需要两次处理
    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)));
        UIGraphicsBeginImageContext(size);
        [resultImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
        resultImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        data = UIImageJPEGRepresentation(resultImage, compression);
    }
    
    return resultImage;
}

大图显示 - 下采样

上面介绍图像显示过程的时得知,图像需要先被解码成位图后才能被渲染,解码是在主线程执行的,也是耗时的操作。当项目中加载到遇到很大的网络(本地)图像时,解码过程很容易造成 ANR 或 OOM。因此,在保证图像质量的前提下,尽可能的减少图像的尺寸和分辨率,以此来达到优化应用性能的目的。
下面分别是 OC 和 Swift 两种语言实现的“下采样” 的源码,缩小图像并解码

OC

// 大图缩小为显示尺寸的图
- (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];
    CFRelease(imageSource);

    return image;
}

Swift

/// Creates a downsampled image from given data to a certain size and scale.
  func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? {
    let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
    guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else {
      return nil
    }
    
    let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
    let downsampleOptions = [
      kCGImageSourceCreateThumbnailFromImageAlways: true,
      kCGImageSourceShouldCacheImmediately: true,
      kCGImageSourceCreateThumbnailWithTransform: true,
      kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
    guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
      return nil
    }
    return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil)
  }

总结

本篇简要介绍了图像相关的基础知识,以及一些在开发过程常见的图像压缩、下采样等方案。在理解了图像的基础之后,再去深入学习一些高阶 API 就会驾轻就熟了。


参考:

位图类型 - Win32 apps | Microsoft Learn
深入探讨15种主流图像格式及其优缺点
图形处理(一) - 图像的加载与编解码
图像压缩到指定大小以内
Apple Image/IO