基于CoreImage的图像处理

本文用于记录CoreImage框架的学习和使用,学习资源在这里
CoreImage是高性能处理分析图像和影音图像的技术,并将晦涩难懂底部实现细节封装成简单的API,开发人没有OpenGLOpenGL ESMetal这些图像处理技术背景就可以使用GPU来处理图片,也可以没有GCD编程背景完成图片的多核处理。

本文将对对CoreImage的常用功能进行介绍:

  1. 滤镜基本使用
  2. 协作AV Foundation处理视频
  3. 图片优化
  4. 内容识别(人脸、条形码、文本)

滤镜

CoreImage目前提供了两百左右种类滤镜,并对他们进行了分类, 我们可以使用filterNames(inCategory: categoryName)filterNames(inCategories: categoryArray)接口分类检索这些内置滤镜,并且获取这些内置类型的相关信息:名字、inputKey、默认参数、滤镜支持调节的参数,基于这些内置滤镜和他们的参数,我们可以实现一个基础的多效果且可调节的图片特效功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 获取所有内置的滤镜
func queryAllBiuldFilters() {

let allFilterNames = CIFilter.filterNames(inCategory: nil)
// inCategory获取指定种类的滤镜,nil为所有滤镜
print(allFilterNames.count);
for filterName in allFilterNames {
print("-----------------------------")
let filter = CIFilter(name: filterName)!
print(filterName)
print(filter.inputKeys)
print(filter.attributes)
}
}

按照滤镜效果来分的话,分为如下几类

Effect typeIndicates
kCICategoryDistortionEffectDistortion effects, such as bump, twirl, hole
kCICategoryGeometryAdjustmentGeometry adjustment, such as affine transform, crop, perspective transform
kCICategoryCompositeOperationCompositing, such as source over, minimum, source atop, color dodge blend mode
kCICategoryHalftoneEffectHalftone effects, such as screen, line screen, hatched
kCICategoryColorAdjustmentColor adjustment, such as gamma adjust, white point adjust, exposure
kCICategoryColorEffectColor effect, such as hue adjust, posterize
kCICategoryTransitionTransitions between images, such as dissolve, disintegrate with mask, swipe
kCICategoryTileEffectTile effect, such as parallelogram, triangle
kCICategoryGeneratorImage generator, such as stripes, constant color, checkerboard
kCICategoryGradientGradient, such as axial, radial, Gaussian
kCICategoryStylizeStylize, such as pixellate, crystallize
kCICategorySharpenSharpen, luminance
kCICategoryBlurBlur, such as Gaussian, zoom, motion

按照用法来分的话,分为如下几类

UseIndicates
kCICategoryStillImageCan be used for still images
kCICategoryVideoCan be used for video
kCICategoryInterlacedCan be used for interlaced images
kCICategoryNonSquarePixelsCan be used for nonsquare pixels
kCICategoryHighDynamicRangeCan be used for high-dynamic range pixels

滤镜的运用很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* CISepiaTone:降低图像质量,老照片的感觉
* kCIInputIntensityKey:强度0-1
*/
private func base() {

let ctx = CIContext()
let filter = CIFilter(name: "CISepiaTone")!
filter.setValue(0.8, forKey: kCIInputIntensityKey)
let image = CIImage(image: UIImage(named: "girl.jpg")!)
filter.setValue(image, forKey: kCIInputImageKey)
let output = filter.outputImage!
let cgImage = ctx.createCGImage(output, from: output.extent)! // 滤镜尚未生效
processedImageView.image = UIImage(cgImage: cgImage) // 滤镜生效
}

滤镜的输出.output是一个CIImage对象,它并不负责把图片展示出来,而是一个记录如何生成图片的recipe,从本地加载图片、访问滤镜(链)的.ouput都会创建一个这样的recipe,他们只有在被渲染出来的时候才会生效,这也是滤镜生效的时机。
每一个滤镜的输出(recipe)都可以作为另外一个滤镜的输入,由此便形成了滤镜链,一条滤镜链上有多个recipe,同样也只有在渲染出来的时候,滤镜链的滤镜才会生效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 滤镜链
*/
private func filterChain() {

let ctx = CIContext()
let ciImage = CIImage(image: UIImage(named: "girl.jpg")!)!
let colorFilter = CIFilter(name: "CIPhotoEffectProcess",
parameters: [kCIInputImageKey: ciImage])!
let boomImage = colorFilter.outputImage!.applyingFilter("CIBloom",
parameters: [kCIInputRadiusKey: 5, kCIInputIntensityKey: 1])
let cropRect = CGRect(x: 0, y: 0, width: 100, height: 100)
let croppedImage = boomImage.cropped(to: cropRect)
let cgImage = ctx.createCGImage(croppedImage,
from: croppedImage.extent)!
print(croppedImage.extent)
processedImageView.image = UIImage(cgImage: cgImage)

}

CoreImage不会单独运用每一个滤镜,而是把所有的滤镜组合成一次操作,这样对渲染时间和内存占用比较友好,而且渲染结果不受滤镜链上的滤镜顺序影响。CoreImage会根据滤镜的信息来优化渲染过程,比如上面的demo中滤镜使用情况为CIPhotoEffectProcess->CIBloom->剪切,如果把剪切调整到第一步,那么CoreImage就不会在被剪切的部分运用CIPhotoEffectProcessCIBloom这两个滤镜,只会运用在有效像素区域。

协作AV Foundation处理视频

前面主要讲介绍了CoreImageAVKit框架下使用,除此之外还可以运用于AV FoundationSpriteKitSceneKitMetal框架下,当然了也可以和OpenGLOpenGL ES使用。
接下来我们使用CoreImageAV Foundation给视频添加滤镜,思路很简单:

  1. 使用videoCompositionWithAsset:applyingCIFiltersWithHandler:构建一个AVVideoComposition对象,我们需要传入两个参数视频资源和视频播放的每一帧回调
  2. 在视频播放的每一帧回调中取得该帧图片,使用滤镜
  3. 使用AV Foundation框架把视频播放出来就okay了
    代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    let filter = CIFilter(name: "CIGaussianBlur")!
    filter.setValue(30, forKey: kCIInputRadiusKey)
    let url = URL(string: "http://192.168.1.101:8001/deathgod_01.mp4")!
    let asset = AVAsset(url: url)
    let composition = AVVideoComposition(asset: asset) { (request) in
    let source = request.sourceImage.clampedToExtent()
    filter.setValue(source, forKey: kCIInputImageKey)
    // 在这里可以为每一秒设置不同的滤镜参数
    let output = filter.outputImage!.cropped(to: request.sourceImage.extent)
    request.finish(with: output, context: nil)
    }
    let item = AVPlayerItem(asset: asset)
    item.videoComposition = composition
    let player = AVPlayer(playerItem: item)
    let playerLayer = AVPlayerLayer(player: player)
    playerLayer.frame = CGRect(x: 0, y: 300, width: view.frame.width,
    height: view.frame.width * 9 / 16)
    view.layer.addSublayer(playerLayer)
    player.play()
    开发文档中提到了一点,由于模糊滤镜模糊图片像素和图片边缘的透明像素,会产生一种边缘虚化的不友好现象,解决方案是虚化之前使用imageByClampingToExtent无限扩展图片边缘像素,同时会导致图片无限大小,所以需要重新裁剪。

面部特征检测

我们一般设置UIImageView.contentMode = .scaleAspectFit来防止图片被拉伸和超出边界被剪切,这样图片就可以等比例收缩且全部显示在UIImageView中,收缩比例为mini(宽收缩比例,高搜索比例)。当检测到面部特征时,我们可以拿到面部特征的数字信息,比如面部位置bounds、左眼位置leftEyePosition、右眼位置rightEyePosition、嘴巴位置mouthPosition,这些数字都以原图片(非UIImageView或者收缩后的图片)的坐标系来的,且坐标原点在左下角(mac os的坐标系),所以使用这些数据之前,需要先对这些数据进行如下转换:

  1. 按照图片收缩的比例进行收缩
  2. 由于这些数据参考的坐标原点在左下角,所以实际的Y是boundsAfterScale.height - boundsAfterScale.maxY
  3. 由于图片进行等比例缩小,所以一般情况下在垂直或者水平的方向和UIImageView有空白,在转换x/y的时候要加上这部分的空白 滤镜部分代码如下,详细代码在这里
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /// 面部特征检测
    let ctx = CIContext()
    let detector = CIDetector(ofType: CIDetectorTypeFace,
    context: ctx,
    options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])!
    let image = UIImage(named: "girl.jpg")!
    let ciImage = CIImage(image: image)!
    var ops = [String: Any]()
    if let orientation = ciImage.properties[kCGImagePropertyOrientation as String] {
    ops[CIDetectorImageOrientation] = orientation
    } else {
    // kCGImagePropertyOrientation 1 - 8 默认是1,分析是从左上角开始,即第一行在top + 第一列在left
    ops[CIDetectorImageOrientation] = 1
    }

    let features = detector.features(in: ciImage, options: ops ) as! [CIFaceFeature]
    同样,我们可以使用CIDetectorTypeQRCode进入二维码检测
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let image = UIImage(named: "qrcode_text.png")!
    let ciImage = CIImage(image: image)!
    let ctx = CIContext()
    let detector = CIDetector(ofType: CIDetectorTypeQRCode,
    context: ctx,
    options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
    let features = detector?.features(in: ciImage) as? [CIQRCodeFeature] ?? []
    print(features.count)
    for feature in features {
    // bounds的处理参考面部检测
    print("位置在 \(feature.bounds) 二维码的内容是\(feature.messageString ?? "")")
    }
    使用CIDetectorTypeText进行文本检测,默认按行为单位进行扫描检测,返回该行的区域和该行每个字符的四个角的位置,但是很遗憾,不能识别文字内容,而且容易受干扰,比如上图(带二维码)中的文字就没有检测到,下图中的弯曲文字也没有校测到。滤镜代码如下,全部代码在这里
    1
    2
    3
    4
    5
    6
    let ctx = CIContext()
    let detector = CIDetector(ofType: CIDetectorTypeText,
    context: ctx,
    options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
    // CIDetectorReturnSubFeatures可以返回每个字符的四个角
    let features = detector?.features(in: ciImage, options: [CIDetectorReturnSubFeatures: true]) as? [CITextFeature] ?? []

图片自动优化

CoreImage分析图片的柱状图?、面部区域内容、元数据,并根据这些情况返回一个优化该图片所需要使用的滤镜组,总共有5中滤镜:

CIRedEyeCorrection修复强光导致的红眼(眼底血管导致)、白眼(眼球反光导致)、眼球模糊
CIFaceBalance自动调色
CIVibrance调整色彩饱和度
CIToneCurve<对比度调整
CIHighlightShadowAdjust调整阴影

最后我们只需将返回的滤镜组运用到图片上即可完成优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@IBAction func enhanceBtnDidClick(_ sender: UIButton) {

let ctx = CIContext()
let originalImage = UIImage(named: "girl.jpg")!
var ciImage = CIImage(image: originalImage)!
let filters = ciImage.autoAdjustmentFilters()

for filter in filters {
print(filter.name)
filter.setValue(ciImage, forKey: kCIInputImageKey)
ciImage = filter.outputImage!
}

let cgImage = ctx.createCGImage(ciImage, from: ciImage.extent)!
processedImageView.image = UIImage(cgImage: cgImage)
}

从输出结果中,发现使用了4个滤镜:CIFaceBalance、CIVibrance、CIToneCurve、CIHighlightShadowAdjust,因为图片本身已经很好了,所以效果看着不是太明显,但还是能看出来饱和度和对比度稍微提升一些
同时也发现了一点,前面提到的filter.output的时候,滤镜还没有发生作用,所以别直接将filter.ouput转化成UIimage显示到UIImageView上,而是使用ctx.createCGImage(),可能会得到莫名其妙图片,比如图片被拉伸了
注!请不要过于信赖的CoreImage的自动优化,因为有时候不能识别出所有优化点,比如这个,虽然人物面部肤色更加饱和红润了,但是从返回的滤镜组发现,没有去红眼滤镜,说明没有识别出图片的红眼问题。

显示 Gitment 评论