iOS图片处理

388 阅读19分钟

iOS与图形图像处理相关的框架汇总

  • 界面图形框架 -- UIKit
  • 核心动画框架 -- Core Animation
  • 苹果封装的图形框架 -- Core Graphics & Quartz 2D
  • 传统跨平台图形框架 -- OpenGL ES
  • 苹果最新力推的图形框架 -- Metal
  • 适合图片的苹果滤镜框架 -- Core Image
  • 适合视频的第三方滤镜方案 -- GPUImage (第三方不属于系统,这里列出来学习)
  • 游戏引擎 -- Scene Kit (3D) 和 Sprite Kit (2D)
  • 计算机视觉在iOS的应用 -- OpenCV for iOS

接触得最多的框架是以下几个,UIKit、Core Animation,Core Graphic, Core Image。下面简要介绍这几个框架,顺便介绍下GPUImage

界面图形框架 -- UIKit(穿插使用其他图形处理框架)

  • UIKit是一组Objective-C API,为线条图形、Quartz图像和颜色操作提供Objective-C 封装,并提供2D绘制、图像处理及用户接口级别的动画。
  • UIKit包括UIBezierPath(绘制线、角度、椭圆及其它图形)、UIImage(显示图像)、UIColor(颜色操作)、UIFont和UIScreen(提供字体和屏幕信息)等类以及在位图图形环境、PDF图形环境上进行绘制和 操作的功能等, 也提供对标准视图的支持,也提供对打印功能的支持。
  • UIKit与Core Graphics的关系

    在UIKit中,UIView类本身在绘制时自动创建一个图形环境,即Core Graphics层的CGContext类型,作为当前的图形绘制环境。在绘制时可以调用 UIGraphicsGetCurrentContext 函数获得当前 //这段代码就是在UIView的子类中调用 UIGraphicsGetCurrentContext 函数获得当前的图形环境,然后向该图形环境添加路径,最后绘制。 - (void)drawRect:(CGRect)rect { //1.获取上下文 CGContextRef contextRef = UIGraphicsGetCurrentContext(); //2.描述路径 UIBezierPath * path = [UIBezierPath bezierPath]; //起点 [path moveToPoint:CGPointMake(10, 10)]; //终点 [path addLineToPoint:CGPointMake(100, 100)]; //设置颜色 [[UIColor whiteColor]setStroke]; //3.添加路径 CGContextAddPath(contextRef, path.CGPath); //显示路径 CGContextStrokePath(contextRef); }

核心动画框架 -- Core Animation

  • Core Animation 是常用的框架之一。它比 UIKit 和 AppKit 更底层。正如我们所知,UIView底下封装了一层CALayer树,Core Animation 层是真正的渲染层,我们之所以能在屏幕上看到内容,真正的渲染工作是在 Core Animation 层进行的。
  • Core Animation 是一套Objective-C API,实现了一个高性能的复合引擎,并提供一个简单易用的编程接口,给用户UI添加平滑运动和动态反馈能力。
  • Core Animation 是 UIKit 实现动画和变换的基础,也负责视图的复合功能。使用Core Animation可以实现定制动画和细粒度的动画控制,创建复杂的、支持动画和变换的layered 2D视图
  • OpenGL ES的内容也可以与Core Animation内容进行集成。
  • 为了使用Core Animation实现动画,可以修改 层的属性值 来触发一个action对象的执行,不同的action对象实现不同的动画。Core Animation 提供了一组基类及子类,提供对不同动画类型的支持:
    • CAAnimation 是一个抽象公共基类,CAAnimation采用CAMediaTiming 和CAAction协议为动画提供时间(如周期、速度、重复次数等)和action行为(启动、停止等)。
    • CAPropertyAnimation 是 CAAnimation的抽象子类,为动画提供一个由一个key路径规定的层属性的支持;
    • CABasicAnimation 是CAPropertyAnimation的具体子类,为一个层属性提供简单插入能力。
    • CAKeyframeAnimation 也是CAPropertyAnimation的具体子类,提供key帧动画支持。

苹果封装的图形框架 -- Core Graphics & Quartz 2D

  • Core Graphics(使用Quartz 2D引擎)
    • Core Graphics是一套C-based API, 支持向量图形,线、形状、图案、路径、剃度、位图图像和pdf 内容的绘制
    • Core Graphics 也是常用的框架之一。它用于运行时绘制图像。开发者们可以通过 Core Graphics 绘制路径、颜色。当开发者需要在运行时创建图像时,可以使用 Core Graphics 去绘制,运行时实时计算、绘制一系列图像帧来实现动画。与之相对的是运行前创建图像(例如从磁盘中或内存中已经创建好的UIImage图像)。
  • Quartz 2D
    • Quartz 2D是Core Graphics中的2D 绘制呈现引擎。Quartz是资源和设备无关的,提供路径绘制,anti-aliased呈现,剃度填充图案,图像,透明绘制和透明层、遮蔽和阴影、颜色管理,坐标转换,字体、offscreen呈现、pdf文档创建、显示和分析等功能。
    • Quartz 2D能够与所有的图形和动画技术(如Core Animation, OpenGL ES, 和 UIKit 等)一起使用。Quartz 2D采用paint模式进行绘制。
    • Quartz 2D提供的主要类包括:
      • CGContext:表示一个图形环境;
      • CGPath:使用向量图形来创建路径,并能够填充和stroke;
      • CGImage:用来表示位图;
      • CGLayer:用来表示一个能够用于重复绘制和offscreen绘制的绘制层;
      • CGPattern:用来表示Pattern,用于重复绘制;
      • CGShading和 CGGradient:用于绘制剃度;
      • CGColor 和 CGColorSpace;用来进行颜色和颜色空间管理;
      • CGFont, 用于绘制文本;
      • CGPDFContentStream、CGPDFScanner、CGPDFPage、CGPDFObject,CGPDFStream, * CGPDFString等用来进行pdf文件的创建、解析和显示。

适合图片的苹果滤镜框架 -- Core Image

  • Core Image 与 Core Graphics 恰恰相反,Core Graphics 用于在运行时创建图像,而 Core Image 是用来处理已经创建的图像的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。

  • Core Image 是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了180种), 这些Filter 提供了各种各样的效果, 并且还可以通过 滤镜链 将各种效果的 Filter叠加 起来形成强大的自定义效果。

  • Core Image 的优点在于十分高效。大部分情况下,它会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。如果设备支持 Metal,那么会使用 Metal 处理。这些操作会在底层完成,Apple 的工程师们已经帮助开发者们完成这些操作了。

    • 例如他可以根据需求选择 CPU 或者 GPU 来处理。
    // 创建基于 CPU 的 CIContext 对象 (默认是基于 GPU,CPU 需要额外设置参数)
    context = [CIContext contextWithOptions: [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]];
    // 创建基于 GPU 的 CIContext 对象
    context = [CIContext contextWithOptions: nil];
    // 创建基于 GPU 的 CIContext 对象
    EAGLContext *eaglctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    context = [CIContext contextWithEAGLContext:eaglctx];
    
  • Core Image 的 API 主要就是三类

    • CIImage 保存图像数据的类,可以通过UIImage,图像文件或者像素数据来创建,包括未处理的像素数据。
    • CIFilter 表示应用的滤镜,这个框架中对图片属性进行细节处理的类。它对所有的像素进行操作,用一些键-值设置来决定具体操作的程度。
    • CIContext 表示上下文,如 Core Graphics 以及 Core Data 中的上下文用于处理绘制渲染以及处理托管对象一样,Core Image 的上下文也是实现对图像处理的具体对象。可以从其中取得图片的信息。

适合视频的第三方滤镜方案 -- GPUImage

  • GPUImage是一个基于OpenGL ES 2.0的开源的图像处理库,优势:
    • 最低支持 iOS 4.0,iOS 5.0 之后就支持自定义滤镜。在低端机型上,GPUImage 有更好的表现。
    • GPUImage 在视频处理上有更好的表现。
    • GPUImage 的代码已经开源。可以根据自己的业务需求,定制更加复杂的管线操作。可定制程度高。

iOS 图形绘制框架

图片的编码解码

位图

// 生成一张位图
 func generateImage() -> UIImage? {
    let eachHeight = 10
    let size = CGSize(width: 256, height: 4*eachHeight)
    UIGraphicsBeginImageContext(size)
    
    guard let context = UIGraphicsGetCurrentContext() else {
        return nil
    }
    
    for y in 0..<4 {
        for x in 0..<256 {
            let color: UIColor
            if y == 0 {
                // 第一行从全黑到全白
                let brightness = CGFloat(x) / 255.0
                color = UIColor(red: brightness, green: brightness, blue: brightness, alpha: 1.0)
            } else {
                // 后面三行只对RGB通道递增
                let red = y == 1 ? CGFloat(x) / 255.0 : 0.0
                let green = y == 2 ? CGFloat(x) / 255.0 : 0.0
                let blue = y == 3 ? CGFloat(x) / 255.0 : 0.0
                color = UIColor(red: red, green: green, blue: blue, alpha: 1.0)
            }
            context.setFillColor(color.cgColor)
            context.fill(CGRect(x: x, y: y*eachHeight, width: 1, height: 1*eachHeight))
        }
    }
    
    let image = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return image
}


获取位图信息

if let image = imageView.image,
    let cgImage = image.cgImage,
    let rawData = cgImage.dataProvider?.data {
        print("lcm data \(rawData)")
    }

位图大小 = 图片的像素宽 256 * 图片的像素高 40 * 每个像素所占的字节数 4

位图编码

1. 使用图片编码的原因

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

//一张位图的宽和高分别都是100个像素,那这个位图的大小
size = 100 * 100 * 4 = 40000B = 39KB
// 正常一张PNG或JPEG格式的100x100的图片,大概只有几KB。如果更大的图,位图所占空间更大,所以位图必须进行编码进行存储。

2. 位图编码技术

iOS有三种位图编码,png,jpeg,heic

enum ImageCodeType: String {
    case png
    case jpeg
    case heic
}
    
func codeImageAndSave(type: ImageCodeType, image: UIImage) -> Void {
    let startTime = DispatchTime.now()
    var imageData: Data?
    switch type {
    case .png:
        imageData = image.pngData()
    case .jpeg:
        imageData = image.jpegData(compressionQuality: 1)
    case .heic:
        if #available(iOS 17.0, *) {
            imageData = image.heicData()
        }
    }
    
    let endTime = DispatchTime.now()
    let nanoseconds = endTime.uptimeNanoseconds - startTime.uptimeNanoseconds
    let milliseconds = Double(nanoseconds) / 1_000_000
    print("lcm codeImage dataSize = \(imageData?.count ?? 0) type = \(type) costTime = \(milliseconds)")
    guard let imageData else { return }
    saveBitmapAsPNGToPhotoLibrary(imageData: imageData)
    return
}

结果如下:

typedataSizecostTimealbum size
png9.6KB3.7225ms10KB
jpeg7.92KB5.2225ms13KB
heic1.17 KB150.301792ms1KB

图片解码

Image and Graphics Best Practices

图像渲染管线

从 MVC 架构的角度来说,UIImage 代表了 Model,UIImageView 代表了 View. 那么渲染的过程我们可以这样很简单的表示:

但实际上,渲染的流程还有一个很重要的步骤:解码(Decode)。
为了了解Decode,首先我们需要了解Buffer这个概念。

缓冲区 (Buffers)

Buffer 在计算机科学中,通常被定义为一段连续的内存,作为某种元素的队列来使用。
下面让我们来了解几种不同类型的 Buffer
Image Buffers: 代表了图片(Image)在内存中的表示。每个元素代表一个像素点的颜色,Buffer 大小与图像大小成正比. The frame buffer: 代表了一帧在内存中的表示。 Data Buffers:代表了图片文件(Image file)在内存中的表示。这是图片的元数据,不同格式的图片文件有不同的编码格式。Data Buffers不直接描述像素点。 因此,Decode这一流程的引入,正是为了将Data Buffers转换为真正代表像素点的Image Buffer 因此,图像渲染管线,实际上是像这样的:

解码

概括来说,从磁盘中加载一张图片,并将它显示到屏幕上,中间的主要工作流如下:

  1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  2. 然后将生成的 UIImage 赋值给 UIImageView ;
  3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  4. 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
    • 分配内存缓冲区用于管理文件 IO 和解压缩操作;
    • 将文件数据从磁盘读到内存中;
    • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
    • 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

在上面的步骤中,我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。 这些图片在主线程的解码操作必然会影响滑动的顺畅度。所以我们一般在子线程强制将其解码,然后在主线程让系统渲染解码之后的图片。现在基本上所有的开源图片库都会实现这个操作。例如:YYImage\SDWebImage。
自己手动解码的原理就是对图片进行重新绘制,得到一张新的解码后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate

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);

官方文档CGBitmapContextCreate

Pixel Format
位图其实就是一个像素数组,而像素格式则是用来描述每个像素的组成格式,它包括以下信息:
Bits per component :一个像素中每个独立的颜色分量使用的 bit 数.
Bits per pixel :一个像素使用的总 bit 数.
Bytes per row :位图中的每一行使用的字节数。
像素格式并不是随意组合的,目前只支持以下有限的 17 种特定组合

从上图可知,对于 iOS 来说,只支持 8 种像素格式。其中颜色空间为 Null 的 1 种,Gray 的 2 种,RGB 的 5 种,CMYK 的 0 种。换句话说,iOS 并不支持 CMYK(青、品红、黄、黑) 的颜色空间。另外,在表格的第 2 列中,除了像素格式外,还指定了 bitmap information constant

Bytes per row 的概念通常用于更高效地处理图像数据。特别是在涉及到内存对齐和数据访问速度时,确定每行所占用的字节数可以帮助优化内存访问和图像处理的效率。一些图像处理算法可能需要按行访问图像数据,因此了解每行的字节数可以帮助程序更有效地读取和处理图像数据,避免不必要的内存访问或计算。此外,对于某些图形库或图像处理引擎来说,提前计算好每行的字节数也可以帮助它> 们更高效地分配内存或进行数据传输。

总的来说,虽然通过图像的宽度和每个像素占用的字节数就可以计算出每行需要多少字节,但在实际应用中,明确了解每行的字节数有助于更有效地处理图像数据,提高图像处理的效率和性能。

Color and Color Spaces
在 Quartz 中,一个颜色是由一组值来表示的,比如 0, 0, 1 。而颜色空间则是用来说明如何解析这些值的,离开了颜色空间,它们将变得毫无意义。比如,下面的值都表示蓝色: 同一张图片,不同的颜色空间会有不同的效果 Color Spaces扩展

Color Spaces and Bitmap Layout
像素格式是用来描述每个像素的组成格式的,比如每个像素使用的总 bit 数。而要想确保 Quartz 能够正确地解析这些 bit 所代表的含义,我们还需要提供位图的布局信息 CGBitmapInfo

CGBitmapInfoCore Graphics 框架中的一个枚举类型,它定义了一组位图信息标志,用于描述位图的特性。这些标志可以用来控制如何解释位图中的像素数据,包括像素的存储方式(如是否包含 alpha 通道)以及像素值的解释方式(如是否使用 premultiplied alpha)。 CGBitmapInfo 枚举包含了以下标志:

  • kCGBitmapByteOrderDefault: 默认字节顺序。
  • kCGBitmapByteOrder16Little: 16 位小端字节顺序。
  • kCGBitmapByteOrder32Little: 32 位小端字节顺序。
  • kCGBitmapByteOrder16Big: 16 位大端字节顺序。
  • kCGBitmapByteOrder32Big: 32 位大端字节顺序。
  • kCGImageAlphaNone: 无 alpha 通道。
  • kCGImageAlphaPremultipliedFirst: Alpha 通道位于像素数据的最前面,并且像素值已经乘以 alpha 值(premultiplied)。
  • kCGImageAlphaPremultipliedLast: Alpha 通道位于像素数据的最后面,并且像素值已经乘以 alpha 值(premultiplied)。
  • kCGImageAlphaLast: Alpha 通道位于像素数据的最后面,但像素值未乘以 alpha 值(unpremultiplied)。
  • kCGImageAlphaFirst: Alpha 通道位于像素数据的最前面,但像素值未乘以 alpha 值(unpremultiplied)。
  • kCGBitmapFloatComponents: 浮点数组件。
  • kCGBitmapConstantAlpha: 所有像素具有相同的 alpha 值。
  • kCGImageAlphaNoneSkipFirst: 无 alpha 通道,但像素布局保留了 alpha 通道的位置。
  • kCGImageAlphaNoneSkipLast: 无 alpha 通道,但像素布局保留了 alpha 通道的位置。

这些标志可以组合使用来指定位图的具体配置。例如,一个常见的 RGB + Alpha 位图可能使用 kCGImageAlphaPremultipliedLast,这意味着它有一个 alpha 通道,并且 alpha 通道位于像素数据的最后面,并且像素值已经乘以 alpha 值。

那么我们在解压缩图片的时候应该使用哪个值呢? Which CGImageAlphaInfo should we use 当图片不包含 alpha 的时候使用 kCGImageAlphaNoneSkipFirst ,否则使用 kCGImageAlphaPremultipliedFirst 。另外,这里也提到了字节顺序应该使用 32 位的主机字节顺序 kCGBitmapByteOrder32Host

再回过头来看看 CGBitmapContextCreate 函数中每个参数所代表的具体含义:

  • widthheight :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可。
  • bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可。
  • bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化,更多信息可以查看 what is byte alignment (cache line alignment) for Core Animation? Why it matters?Why is my image’s Bytes per Row more than its Bytes per Pixel times its Width? ,亲测可用。
  • space :就是我们前面提到的颜色空间,一般使用 RGB 即可。
  • bitmapInfo :就是我们前面提到的位图的布局信息。
// YYImage 中解码的代码:
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 {
        CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
        size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
        size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
        size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
        CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
        if (bytesPerRow == 0 || width == 0 || height == 0) return NULL;
        
        CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
        if (!dataProvider) return NULL;
        CFDataRef data = CGDataProviderCopyData(dataProvider); // decode
        if (!data) return NULL;
        
        CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data);
        CFRelease(data);
        if (!newProvider) return NULL;
        
        CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault);
        CFRelease(newProvider);
        return newImage;
    }
}

它接受一个原始的位图参数 imageRef ,最终返回一个新的解压缩后的位图 newImage ,中间主要经过了以下三个步骤:

  • 使用 CGBitmapContextCreate 函数创建一个位图上下文
  • 使用 CGContextDrawImage 函数将原始位图绘制到上下文中
  • 使用 CGBitmapContextCreateImage 函数创建一张新的解压缩后的位图

SDWebImage 中对图片的解压缩过程基本一致

缩略图

Data Buffers 解码到 Image Buffers 是一个CPU密集型的操作。同时它的大小是和与原始图像大小成比例,和 View 的大小无关。
如果一个浏览照片的应用展示多张照片时,没有经过任何处理,就直接读取图片,然后来展示。那 Decode 时,将会占用极大的内存和 CPU。而我们展示的图片的 View 的大小,其实是完全用不到这么大的原始图像的。 可以使用 CGImageSourceCreateThumbnailAtIndex 减少内存的消耗

private func sampleImage() {
    let pointSize = CGSize(width: 256, height: 40)
    //生成CGImageSourceRef 时,不需要先解码。
    let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
    let imageSource = CGImageSourceCreateWithURL(imageUrl as CFURL, imageSourceOptions)!
    let maxDimensionInPixels = max(pointSize.width, pointSize.height)
    
    //kCGImageSourceShouldCacheImmediately
    //在创建Thumbnail时直接解码,这样就把解码的时机控制在这个downsample的函数内
    let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,
                                    kCGImageSourceShouldCacheImmediately: true,
                                kCGImageSourceCreateThumbnailWithTransform: true,
                                        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
    //生成
    let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
    imageView4.image = UIImage(cgImage: downsampledImage)
}

SDWebImage也有这样的处理 通过SDWebImageScaleDownLargeImage的option限制最大的内存占用实现。

// Scale down to limit bytes if need
if (limitBytes > 0) {
    // Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage
    CGSize imageSize = CGSizeMake(width, height);
    CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:limitBytes bytesPerPixel:4 frameCount:frameCount];
    // Override thumbnail size
    thumbnailSize = framePixelSize;
    preserveAspectRatio = YES;
}

使用 UIGraphicsImageRenderer 替换 UIGraphicsBeginImageContext

如果我们想要自己创建Image Buffers, 我们通常会选择使用UIGraphicsBeginImageContext, 而苹果的建议是使用UIGraphicsImageRenderer,因为它的性能更好,还支持广色域。

图片处理

很多情况我们需要对图片进行像素、滤镜处理。这里针这些常用的图片处理使用不同图形处理框架进行相关编码实践。

灰度图

  • 获取图像的宽度和高度。
  • 创建一个设备 RGB 色彩空间。
  • 初始化一个整数数组 imagePixel 用来存储图像的像素值。
  • 使用 Core Graphics 框架创建一个位图上下文,这个上下文指向 imagePixel 数组。
    • bitmapInfo: noneSkipLast, RGBX
  • 将原始图像绘制到位图上下文中。
  • 遍历每个像素,并根据不同的灰度转换方法计算新的灰度值。
  • 更新 imagePixel 数组中的像素值,使其变为灰度值。
let width = imageRef.width
let height = imageRef.height
let colorSpaceRef = CGColorSpaceCreateDeviceRGB()
var imagePixel = [UInt32](repeating: 0, count: width * height)

guard let contextRef = CGContext(data: &imagePixel, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 4 * width, space: colorSpaceRef, bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else {
    return nil
}
contextRef.draw(imageRef, in: CGRect(x: 0, y: 0, width: width, height: height))
for y in 0..<height {
    for x in 0..<width {
        let index = y * width + x
        var rgbPixel = imagePixel[index]
        let gray: UInt32
        switch type {
        case 0:
            gray = (rgbPixel >> 8) & 0xFF
        case 1:
            gray = ((rgbPixel >> 16) & 0xFF + (rgbPixel >> 8) & 0xFF + (rgbPixel & 0xFF)) / 3
        case 2:
            let b = Double((rgbPixel >> 16) & 0xFF) * 0.11
            let g = Double((rgbPixel >> 8) & 0xFF) * 0.59
            let r = Double(rgbPixel & 0xFF) * 0.3
            gray = UInt32(r + g + b)
        default:
            gray = 0
        }
        
        rgbPixel = (gray << 16) | (gray << 8) | gray
        imagePixel[index] = rgbPixel
    }
}

// Draw based on context
guard let finalRef = contextRef.makeImage() else {
    return nil
}

return UIImage(cgImage: finalRef)

或者使用 Core Image

func convertToGrayscale(image: UIImage) -> UIImage? {
    guard let ciImage = CIImage(image: image) else {
        return nil
    }
    
    let colorControlsFilter = CIFilter(name: "CIColorControls")
    colorControlsFilter?.setValue(ciImage, forKey: kCIInputImageKey)
    // 设置饱和度为0
    colorControlsFilter?.setValue(0.0, forKey: kCIInputSaturationKey)
    
    guard let outputCIImage = colorControlsFilter?.outputImage else {
        return nil
    }

    let context = CIContext(options: nil)
    guard let cgImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) else {
        return nil
    }

    return UIImage(cgImage: cgImage)
}

图片打码

func imageToMosaic(cgImage: CGImage, size: Int) -> UIImage? {
    let width = cgImage.width
    let height = cgImage.height
    
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    
    var imagePixels = [UInt32](repeating: 0, count: width * height)
    guard let contextRef = CGContext(data: &imagePixels,
                                        width: width,
                                        height: height,
                                        bitsPerComponent: 8,
                                        bytesPerRow: 4 * width,
                                        space: colorSpace,
                                        bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else {
        return nil
    }
    
    contextRef.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
    guard let bitmapPixels = contextRef.data?.assumingMemoryBound(to: UInt8.self) else {
        return nil
    }
    
    var pixels = [UInt8](repeating: 0, count: 4)
    var currentPixels = 0
    var preCurrentPixels = 0
    let mosaicSize = size
    
    if size == 0 {
        return UIImage(cgImage: cgImage)
    }
    
    for i in 0..<height - 1 {
        for j in 0..<width - 1 {
            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 {
                preCurrentPixels = (i - 1) * width + j
                memcpy(bitmapPixels + 4 * currentPixels, bitmapPixels + 4 * preCurrentPixels, 4)
            }
        }
    }
    
    guard let finalImg = contextRef.makeImage() else {
        return nil
    }
    
    let processedImage = UIImage(cgImage: finalImg)
    return processedImage
}

图片加暗水印

iOS暗水印