iOS端的实时人脸贴纸功能看似简单,实际做起来涉及人脸检测、关键点提取、坐标映射和渲染四个环节,任何一个环节处理不好,贴纸就会出现漂移、抖动或者镜像错位的问题。苹果官方的Vision Framework提供了VNFaceLandmarks2D,可以直接输出眉毛、眼睛、鼻子、嘴巴、脸部轮廓等区域的归一化坐标,配合Metal或Core Animation渲染即可实现低延迟的贴纸效果。本文将完整讲解检测链路搭建、坐标换算和贴纸矩阵映射的细节。

一、Vision Framework的人脸关键点能力与106点的由来
严格来说,VNFaceLandmarks2D输出的并不是一个固定的106点模型,而是按区域分组的关键点集合:faceContour提供脸部轮廓(默认9点,可配置到最多33点)、leftEye和rightEye各6点、leftEyebrow和rightEyebrow各7点、nose含鼻梁4点和鼻嵴5点(实际分布为noseCrest 4点)、outerLips 10点、innerLips 8点,此外还有leftPupil和rightPupil瞳孔点。将所有区域按最大密度配置叠加起来,总点数接近106,这也是社区里常说的106点说法的来源。
关键点返回的是相对于检测框的归一化坐标(取值0到1), VNFaceLandmarkRegion2D.pointsInImage(imageSize:)方法可以直接帮你换算成图像像素坐标,省去手动计算的麻烦。需要注意的是,这些点在图像坐标系中y轴向下,而UIKit坐标系y轴也是向下,两者方向一致但图像本身可能带有EXIF方向信息,前置摄像头拍摄的画面通常是镜像的,这是很多贴纸左右颠倒问题的根源。
与 dlib 的68点模型或商用的Face++方案相比,Vision的优势在于完全本地运算、免费、无需引入体积庞大的模型文件,而且在A系列芯片上有专门的加速支持,单帧检测耗时通常在10毫秒以内。缺点是关键点密度不如专业SDK,无法精确到脸部边缘的每一处细节,但用于贴纸锚定已经完全够用。
二、搭建实时检测链路:AVFoundation加VNDetectFaceLandmarksRequest
实时贴纸的第一步是建立视频帧到检测请求的数据流。使用AVCaptureSession采集前置摄像头画面,通过AVCaptureVideoDataOutput的代理回调拿到CVPixelBufferRef,然后交给VNImageRequestHandler执行VNDetectFaceLandmarksRequest。核心代码如下:
- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
VNImageRequestHandler *handler =
[[VNImageRequestHandler alloc] initWithCVPixelBuffer:pixelBuffer
orientation:kCGImagePropertyOrientationLeftMirrored
options:nil];
VNDetectFaceLandmarksRequest *request =
[[VNDetectFaceLandmarksRequest alloc] initWithCompletionHandler:
^(VNRequest *req, NSError *error) {
for (VNFaceObservation *face in req.results) {
// face.landmarks 包含各区域关键点
NSArray<NSValue *> *points =
[face.landmarks.outerLips pointsInImage:imageSize];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateStickerWithLandmarks:face.landmarks
boundingBox:face.boundingBox];
});
}
}];
NSError *error = nil;
[handler performRequests:@[request] error:&error];
}
这里有几个容易踩坑的地方。第一,orientation参数必须与摄像头的安装方向和连接的镜像配置匹配,前置摄像头如果设置了videoMirrored为YES,传入的EXIF方向就要相应调整,否则关键点整体会旋转或翻转。第二,检测回调默认不在主线程,更新UI必须切回主队列。第三,不要在每一帧都创建新的request对象,可以复用request并配合VNSequenceRequestHandler做人脸跟踪,既能提升性能,又能让同一个ID的人脸在不同帧之间保持关联,减少贴纸跳变。
三、归一化坐标到屏幕坐标的映射与贴纸变换
VNFaceObservation的boundingBox和landmarks坐标都基于归一化图像坐标系(原点在左下角),而UIKit的原点在左上角,两者y轴方向相反。假设视频层使用AVSampleBufferDisplayLayer或通过CALayer的内容绘制,正确的换算方式是:
// 图像坐标转为视图坐标(考虑视频填充模式的宽高比)
CGRect faceBox = VNImageRectForNormalizedRect(face.boundingBox,
imageWidth, imageHeight);
// y轴翻转:视图Y = 视图高 - (图像Y * 缩放比)
CGFloat scale = self.view.bounds.size.width / imageWidth;
CGPoint convertPoint(CGPoint p) {
return CGPointMake(p.x * scale,
self.view.bounds.size.height - p.y * scale);
}
// 瞳孔中点作为贴纸锚点
CGPoint leftEye = convertPoint(
[face.landmarks.leftPupil.pointsInImage(imageSize) firstObject].CGPointValue);
CGPoint rightEye = convertPoint(
[face.landmarks.rightPupil.pointsInImage(imageSize) firstObject].CGPointValue);
拿到锚点后,贴纸的位置、旋转和缩放都可以从关键点推导。例如额头贴纸以双瞳中点为锚,旋转角度取两眼连线与水平线的夹角 atan2(rightEye.y - leftEye.y, rightEye.x - leftEye.x),缩放比例取两眼距离除以贴纸设计时的基准距离。这样无论用户歪头还是前后移动,贴纸都能贴合脸部姿态。为每个贴纸定义一个配置对象,包含锚定区域、偏移向量、基准眼距,可以灵活扩展多套贴纸资源。
四、平滑滤波与性能优化
直接使用每帧的原始关键点会让贴纸产生明显抖动,因为检测本身存在像素级的波动。常见的做法是对锚点和角度做低通滤波,例如用上一次值和当前值做指数加权:smoothed = smoothed * 0.7 + current * 0.3,参数在0.2到0.5之间调节,数值越小贴纸越稳但跟手性越差。更讲究的方案可以用卡尔曼滤波,对位置和速度同时建模,在快速转头时表现更自然。
@interface PointSmoother : NSObject
@property (nonatomic, assign) CGPoint value;
@property (nonatomic, assign) CGFloat alpha; // 平滑系数
@end
@implementation PointSmoother
- (CGPoint)smooth:(CGPoint)input {
self.value = CGPointMake(self.value.x * (1 - self.alpha) + input.x * self.alpha,
self.value.y * (1 - self.alpha) + input.y * self.alpha);
return self.value;
}
@end
性能方面有三个建议。首先,检测分辨率不必跟随视频原分辨率,可以在输出端把videoSettings的像素格式设为kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,并将sessionPreset设为640x480档位,检测精度足够而计算量大幅下降。其次,贴纸渲染层优先使用CALayer配合transform,避免每帧重绘图片;如果要做变形贴纸(比如跟随脸颊弯曲的面具),再考虑用Metal自定义渲染。最后,在iOS 14及以上系统可以使用VNRecognizeTextRequest之外的请求并发执行,但人脸关键点检测建议串行执行,避免多帧请求排队导致延迟累积。
另外要注意多脸场景。VNDetectFaceLandmarksRequest可能返回多个VNFaceObservation,如果产品只支持单人贴纸,应选取boundingBox面积最大的人脸;多人贴纸则需要为每个observation维护独立的平滑器和贴纸层级,通过requestRevision或VNTrackObjectRequest保持身份一致。
五、总结
用Vision Framework实现人脸贴纸的整体链路是:AVFoundation采集帧、VNDetectFaceLandmarksRequest提取归一化关键点、VNImageRectForNormalizedRect换算到视图坐标、基于眼距和角度计算贴纸变换矩阵、最后用指数平滑消除抖动。整个过程不依赖任何第三方库,隐私合规和包体积都有保障。实际开发中最容易出问题的是镜像方向和坐标翻转这两处,建议先用不同机型真机验证左右眼贴纸是否对称,再逐步叠加旋转、缩放和平滑逻辑,这样定位问题会轻松很多。
Vision Framework人脸关键点检测贴纸映射修改时间:2026-09-09 08:14:43