在iOS开发中,实时美颜一直是社交类App的核心功能之一。Core Image作为系统级图像处理框架,提供了丰富的内置滤镜和高效的GPU加速能力,但如果只是简单地叠加几个CIFilter,往往会出现效果不均匀、面部区域被整体涂抹等问题。要实现真正可用的实时美颜,需要把磨皮、美白这些全局操作限定在人脸区域,并且还要根据眼睛、嘴巴等特征点做避让处理,避免五官边缘被模糊掉。

Core Image美颜滤镜的基础架构
Core Image的核心概念是滤镜链(Filter Chain)。一个CIImage通过一个或多个CIFilter处理,最终输出到CIContext渲染。实时美颜链路通常包含人脸检测、区域蒙版生成、磨皮美白滤镜、混合叠加等环节。CIContext是渲染上下文,它管理着GPU资源,创建成本很高,必须复用一个实例。对于实时视频流,还需要使用CIContext的渲染方法将结果输出到Metal或OpenGL纹理。
美颜的本质是图像编辑,核心是让皮肤区域的细节变得平滑,同时保持五官和边缘清晰。Core Image中的CIGaussianBlur、CIBilateralFilter等滤镜可以用来做平滑处理,但双边滤波在Core Image中性能并不理想。实践中更常用的方案是使用高反差保留(High Pass)配合模糊来模拟磨皮,或者直接使用CIFilter自定义子类封装核心图像内核(CIKernel)来实现局部自适应模糊。下面是一个典型的滤镜链构建方式:
import CoreImage
import CoreImage.CIFilterBuiltins
func makeBeautyFilter() -> CIFilter? {
// 使用系统的双边滤镜做基础磨皮
let filter = CIFilter.bilateralFilter()
filter.inputImage = sourceImage
filter.radius = 8.0
filter.edgeDetail = 0.5
return filter
}
以上代码展示的是CIFilterBuiltins中的双边滤镜,它通过输入图像和半径参数控制模糊程度。但直接使用这种滤镜会让人脸的毛发、眉毛也模糊掉,所以更需要结合蒙版来进行区域限制。在Core Image中,蒙版本质上也是一张灰度图,白色区域表示完全生效,黑色区域表示不生效。我们可以通过CIMaskToAlpha等滤镜将人脸掩膜转换成alpha通道,再与原始图像混合。
磨皮与美白的滤镜组合实现
磨皮常用的方案是“高反差保留+模糊+混合”。首先复制原图,对复制图进行高斯模糊得到低频图像,然后用原图减去低频图像得到高频细节层,最后将高频细节层以柔和模式叠加回模糊图,从而在保留细节的同时平滑皮肤。这种算法比双边滤波可控性更强。美白则相对简单,可以通过调节亮度、饱和度以及色彩平衡来实现。Core Image中的CIColorControls可以调整亮度(brightness)、对比度(contrast)、饱和度(saturation),而CITemperatureAndTint可以调整色温,让肤色显得更通透。
可以封装一个自定义的CIFilter子类,把磨皮和美白组合在一起。滤镜参数包括强度(strength)、半径(radius)、亮度(brightness)等。下面是一个使用CIKernel编写的自定义磨皮滤镜的代码框架,它通过对皮肤色区域进行选择性平滑来保持细节:
class SkinSmoothFilter: CIFilter {
var inputImage: CIImage?
var inputStrength: Float = 0.6
private lazy var kernel: CIKernel = {
let code = """
kernel vec4 skinSmooth(__sample image, float strength) {
// 简单的邻近像素平均
vec2 offset = destCoord() - samplerCoord(image);
// 这里只做示意,实际需要做多次采样
vec4 pixel = sample(image, samplerCoord(image));
return mix(pixel, vec4(1.0), strength * 0.1);
}
"""
return CIKernel(source: code)!
}()
// 实际实现需要更多采样逻辑,此处省略
}
这里展示了自定义CIKernel的基本结构。CIKernel用OpenGL着色语言编写,可以逐像素处理图像。与系统内置滤镜相比,自定义kernel的优势是可以针对美颜算法做更精细的控制,例如检测肤色范围、计算局部反差等。不过要注意,CIKernel的编译是在运行时完成的,所以需要把kernel字符串定义为静态常量,避免频繁编译。
美白与磨皮的组合顺序也会影响最终效果。通常先做美白,再做磨皮,因为美白会改变像素亮度,如果磨皮在前,亮度调整可能会把已经平滑的皮肤重新引入噪声。同时,美白不宜过度,否则会丢失高光细节。可以将美白强度与肤色亮度关联起来,越暗的区域美白幅度越大,这样能让皮肤更自然。
使用Vision框架精准定位人脸特征点
实现区域美颜的前提是知道人脸在哪里,以及眼睛、鼻子、嘴巴等关键点的精确位置。Vision框架提供的VNDetectFaceLandmarksRequest可以检测人脸以及面部轮廓、眼睛、眉毛、鼻子、嘴唇等特征点。它的检测精度在A12芯片以后有显著提升,而且可以处理实时视频帧。使用时需要将CIImage或CGImage转换为VNImageRequestHandler来处理。
特征点坐标是相对于人脸boundingBox的归一化坐标,因此需要转换成图像坐标系中的像素位置。Vision的坐标系原点在左下角,并且y轴向上,而UIKit的坐标系原点在左上角,转换时需要注意。以下代码演示了如何从CIImage中检测人脸并获取左侧眼睛的中心位置:
import Vision
func detectFaceLandmarks(in ciImage: CIImage) -> VNFaceObservation? {
let handler = VNImageRequestHandler(ciImage: ciImage, options: [:])
let request = VNDetectFaceLandmarksRequest()
try? handler.perform([request])
return request.results?.first
}
// 获取左眼中心点(归一化坐标转换)
if let landmarks = faceObservation.landmarks,
let leftEye = landmarks.leftEye {
let x = faceObservation.boundingBox.origin.x + leftEye.normalizedPoints[0].x * faceObservation.boundingBox.width
let y = faceObservation.boundingBox.origin.y + leftEye.normalizedPoints[0].y * faceObservation.boundingBox.height
// 注意在UIKit中显示时,需要做y坐标翻转
}
注意特征点数组normalizedPoints是相对于整个人脸boundingBox的,不是整张图像。同时,因为视频帧在实时处理时往往是YUV格式,我们需要先转为RGBA的CIImage,人脸检测才能正确处理。通常的做法是使用AVCaptureVideoDataOutput获取CVPixelBuffer,然后直接创建CIImage,再传给VNImageRequestHandler。为了兼顾性能,可以设置检测请求的usesCPUOnly为false,让GPU参与计算。
人脸检测本身比较耗时,如果每一帧都做完整的检测,会占用不少CPU。实践中可以降低检测频率,比如每隔几帧检测一次,中间帧沿用上一帧的特征点位置,并根据运动平滑算法进行插值。这样既能保证实时性,又能避免特征点抖动。
特征点与滤镜的融合处理
拿到人脸特征点后,下一步是生成局部蒙版。Core Image没有直接提供从点集生成蒙版的滤镜,但我们可以使用CIRadialGradient或CIBumpDistortion等滤镜来生成圆形渐变,也可以借助Core Graphics的路径绘制生成区域蒙版。一个简单有效的办法是:以左右两眼的中心为半径,生成一个覆盖脸部的椭圆渐变蒙版,椭圆中心大约在下巴附近,这样可以让脸颊和额头区域获得最强效果,而五官区域由于处于椭圆边缘或外部会自然避让。
另一种更精确的做法是使用脸部轮廓特征点构建路径,然后通过Core Image的CIPerspectiveCorrection或CIRoundedRectangle等生成软边蒙版。实际上,我们可以将特征点数组传入自定义CIKernel,利用距离计算来控制每个像素的参与权重。例如,对于一个像素位置,计算它到左眼、右眼、嘴巴中心的距离,如果距离大于某个阈值则提高磨皮权重,如果靠近眼睛则降低权重。这样就能做到真正的特征点驱动融合。
// 使用CIImage的filteredImage方法组合处理链
let maskImage = CIImage(color: CIColor.white)
.cropped(to: inputImage.extent)
.applyingFilter("CIRadialGradient", parameters: [
kCIInputCenterKey: CIVector(x: faceCenter.x, y: faceCenter.y),
"inputRadius0": 100,
"inputRadius1": 200,
"inputColor0": CIColor.white,
"inputColor1": CIColor.black
])
let smoothedImage = inputImage.applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: 10])
let blendedImage = smoothedImage.applyingFilter("CIBlendWithMask", parameters: [
kCIInputBackgroundImageKey: inputImage,
kCIInputMaskImageKey: maskImage
])
上面的代码使用了CIRadialGradient生成一个中心到边缘渐变的蒙版,然后用CIBlendWithMask将模糊图与原图混合。这样做出的磨皮效果会在边缘产生柔和过渡,不会出现明显的断层。需要注意的是,CIRadialGradient生成的蒙版范围是固定的,对于不同大小的人脸需要动态调整半径参数。可以通过人脸boundingBox的宽度来估算蒙版半径,例如半径设置为宽度的一半。
当需要更精细的五官保护时,我们可以组合多个蒙版。比如先生成一张全脸蒙版,再生成一张眼睛和嘴部的避让蒙版,然后用CIMultiplyCompositing将两张蒙版相乘,得到最终蒙版。避让蒙版可以通过将眼睛和嘴部区域设置为黑色来实现。这样,磨皮效果会自然地绕开这些区域,保留了眼睛的锐利度和嘴部轮廓。
实时性能优化策略
实时美颜对性能要求很高,要让处理速度跟得上视频帧率,必须在几个方面做优化。首先,CIContext要全局复用,不要每帧创建。其次,要尽量降低输入图像的采样分辨率。如果视频是1080p,可以先将CVPixelBuffer通过CIImage的transformed(by:)缩放到一个较小的尺寸,例如720p,处理完成后再缩放回原尺寸输出。由于美颜本身是一种柔和处理,降低分辨率不会对画质造成明显影响,反而能大幅提升处理速度。
其次,要避免在滤镜链中出现不必要的中间图像传递。例如,不要重复调用filteredImage方法创建临时CIImage,而是直接使用CIFilter的inputImage/outputImage串联。Core Image的惰性求值机制会在最终渲染时自动合并操作,但前提是代码写成一个连续链。另外,可以使用CIContext的render(_:to:commandBuffer:bounds:colorSpace:)方法配合Metal command buffer,将结果直接渲染到Metal纹理中,避免CPU和GPU之间的数据拷贝。
还有一点是控制人脸检测的频率。前面提到过,可以每3帧检测一次,或者对连续帧使用相同的检测结果。如果使用AVFoundation采集视频,可以结合帧的时间戳来判断是否需要进行新的人脸检测。特征点的小幅移动可以通过带低通滤波的平滑来修正,例如使用指数移动平均来更新特征点位置,这样既省去了每帧检测的开销,又能让美颜区域跟随人脸移动时显得平滑自然。
最后,还可以利用Metal Performance Shaders中的一些滤镜作为替代,但如果项目已经基于Core Image,那么通过自定义CIKernel来替代部分内置滤镜也能提升性能。例如,将磨皮的高斯模糊换成CIBokehBlur可能更快,但具体效果需要测试。建议在真机上用Instruments的Core Animation工具监控帧耗时,针对瓶颈做优化。
Core Image实时美颜人脸特征点修改时间:2026-08-25 22:49:02