# VisionDemo **Repository Path**: zymITsky/VisionDemo ## Basic Information - **Project Name**: VisionDemo - **Description**: iOS 11新特性之人脸检测 - **Primary Language**: Objective-C - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 21 - **Forks**: 1 - **Created**: 2019-07-24 - **Last Updated**: 2023-12-19 ## Categories & Tags **Categories**: cv, ios-modules **Tags**: None ## README >大道如青天,我独不得出 ##### 前言 在上一篇[iOS Core ML与Vision初识](http://www.jianshu.com/p/b0e5f2944b3d)中,初步了解到了`vision`的作用,并在文章最后留了个疑问,就是类似下面的一些函数有什么用 ``` - (instancetype)initWithCIImage:(CIImage *)image options:(NSDictionary *)options; - (instancetype)initWithCVPixelBuffer:(CVPixelBufferRef)pixelBuffer options:(NSDictionary *)options; ``` 在查阅一些资料后,最终通过这些函数得到了如下的效果 ![face.gif](http://upload-images.jianshu.io/upload_images/2525768-c45ee7c16299c64a.gif?imageMogr2/auto-orient/strip) 对,没错,这就是通过`initWithCVPixelBuffer`函数来实现的。当然`vision`的作用远不于此,还有如下的效果 1、图像匹配(上篇文章中的效果) 2、矩形检测 3、二维码、条码检测 4、目标跟踪 5、文字检测 6、人脸检测 7、人脸面部特征检测 由于对人脸识别比较感兴趣,所以这里就主要简单了解了下人脸部分,下面就针对人脸检测和面部检测写写 ##### Vision支持的图片类型 通过查看`VNRequestHandler.h`文件,我们可以看到里面的所有初始化函数,通过这些初始化函数,我们可以了解到支持的类型有: 1、`CVPixelBufferRef` 2、`CGImageRef` 3、`CIImage` 4、`NSURL` 5、`NSData` ##### Vision使用 在使用`vision`的时候,我们首先需要明确自己需要什么效果,然后根据想要的效果来选择不同的类,最后实现自己的效果 1、需要一个`RequestHandler`,在创建`RequestHandler`的时候,需要一个合适的输入源,及`图片`类型 2、需要一个`Request `,在创建`Request `的时候,也需要根据实际情况来选择,`Request `大概有如下这么些 ![request.jpeg](https://camo.githubusercontent.com/cf235b91f509afb1966509336f7e3fd3b740af55/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f323532353736382d636333636432323236333636366561312e6a7065673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430) 3、通过`requestHandler`将`request`联系起来,然后得到结果 ``` [handler performRequests:@[requset] error:&error]; ``` 4、处理结果`VNObservation`,在`VNRequest`的`results`数组中,包含了`VNObservation`结果,`VNObservation`也分很多种,这和你`Request`的类型是相关联的 ![Vision结果继承关系.png](https://camo.githubusercontent.com/8fecf09837cd078e101de55aee404cbfa8ce733c/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f323532353736382d646563656432323134356336303965662e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430) 在完成上述步骤后,我们就可以根据结果来实现一些我们想要的效果 ##### 人脸矩形检测 这里我们需要用到`VNDetectFaceRectanglesRequest` ``` requset = [[VNDetectFaceRectanglesRequest alloc] initWithCompletionHandler:completionHandler]; ``` 在得到结果后,我们需要处理下坐标 ``` for (VNFaceObservation *faceObservation in observations) { //boundingBox CGRect transFrame = [self convertRect:faceObservation.boundingBox imageSize:image.size]; [rects addObject:[NSValue valueWithCGRect:transFrame]]; } ``` ``` // 转换Rect - (CGRect)convertRect:(CGRect)boundingBox imageSize:(CGSize)imageSize{ CGFloat w = boundingBox.size.width * imageSize.width; CGFloat h = boundingBox.size.height * imageSize.height; CGFloat x = boundingBox.origin.x * imageSize.width; CGFloat y = imageSize.height * (1 - boundingBox.origin.y - boundingBox.size.height);//- (boundingBox.origin.y * imageSize.height) - h; return CGRectMake(x, y, w, h); } ``` 在返回结果中的`boundingBox `中的坐标,我们并不能立即使用,而是需要进行转换,因为这里是相对于`image`的一个比例,这里需要注意的是`y`坐标的转换,因为坐标系的`y`轴和`UIView`的`y`轴是相反的。 最后就是通过返回的坐标进行矩形的绘制 ``` + (UIImage *)gl_drawImage:(UIImage *)image withRects:(NSArray *)rects { UIImage *newImage = nil; UIGraphicsBeginImageContextWithOptions(image.size, NO, [UIScreen mainScreen].scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetLineCap(context,kCGLineCapRound); //边缘样式 CGContextSetLineJoin(context, kCGLineJoinRound); CGContextSetLineWidth(context,2); //线宽 CGContextSetAllowsAntialiasing(context,YES); //打开抗锯齿 CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor); CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor); //绘制图片 [image drawInRect:CGRectMake(0, 0,image.size.width, image.size.height)]; CGContextBeginPath(context); for (int i = 0; i < rects.count; i ++) { CGRect rect = [rects[i] CGRectValue]; CGPoint sPoints[4];//坐标点 sPoints[0] = CGPointMake(rect.origin.x, rect.origin.y);//坐标1 sPoints[1] = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y);//坐标2 sPoints[2] = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);//坐标3 sPoints[3] = CGPointMake(rect.origin.x , rect.origin.y + rect.size.height); CGContextAddLines(context, sPoints, 4);//添加线 CGContextClosePath(context); //封闭 } CGContextDrawPath(context, kCGPathFillStroke); //根据坐标绘制路径 newImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return newImage; } ``` 效果如下 ![faceRect.jpg](https://camo.githubusercontent.com/81b06af0faa8b13c2d02f6e8cb08e8272c8beb59/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f323532353736382d613566656265663332316166626361662e6a70673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430) ##### 人脸特征识别 这里我们需要用到`VNDetectFaceLandmarksRequest` ``` requset = [[VNDetectFaceLandmarksRequest alloc] initWithCompletionHandler:completionHandler]; ``` 处理结果 ``` for (VNFaceObservation *faceObservation in observations) { //boundingBox CGRect transFrame = [self convertRect:faceObservation.boundingBox imageSize:image.size]; [rects addObject:[NSValue valueWithCGRect:transFrame]]; } pointModel.faceRectPoints = rects; return pointModel; } - (GLDiscernPointModel *)handlerFaceLandMark:(NSArray *)observations image:(UIImage *)image { GLDiscernPointModel *pointModel = [[GLDiscernPointModel alloc] init]; NSMutableArray *rects = @[].mutableCopy; for (VNFaceObservation *faceObservation in observations) { VNFaceLandmarks2D *faceLandMarks2D = faceObservation.landmarks; [self getKeysWithClass:[VNFaceLandmarks2D class] block:^(NSString *key) { if ([key isEqualToString:@"allPoints"]) { return ; } VNFaceLandmarkRegion2D *faceLandMarkRegion2D = [faceLandMarks2D valueForKey:key]; NSMutableArray *sPoints = [[NSMutableArray alloc] initWithCapacity:faceLandMarkRegion2D.pointCount]; for (int i = 0; i < faceLandMarkRegion2D.pointCount; i ++) { CGPoint point = faceLandMarkRegion2D.normalizedPoints[i]; CGFloat rectWidth = image.size.width * faceObservation.boundingBox.size.width; CGFloat rectHeight = image.size.height * faceObservation.boundingBox.size.height; CGPoint p = CGPointMake(point.x * rectWidth + faceObservation.boundingBox.origin.x * image.size.width, faceObservation.boundingBox.origin.y * image.size.height + point.y * rectHeight); [sPoints addObject:[NSValue valueWithCGPoint:p]]; } [rects addObject:sPoints]; }]; } ``` 在这里,我们需要注意到`landmarks `这个属性,这是一个`VNFaceLandmarks2D`类型的对象,里面包含着许多面部特征的`VNFaceLandmarkRegion2D`对象,如:`faceContour`,`leftEye`,`nose`....分别表示面部轮廓、左眼、鼻子。这些对象中,又包含下面这么一个属性 ``` @property (readonly, assign, nullable) const CGPoint* normalizedPoints ``` 这是一个包含该面部特征的的数组,所以我们可以通过下面的方式取出里面的坐标 ``` CGPoint point = faceLandMarkRegion2D.normalizedPoints[i]; ``` 当然这里面也存在坐标的转换,见上面代码 最后也是画线,代码如下 ``` + (UIImage *)gl_drawImage:(UIImage *)image faceLandMarkPoints:(NSArray *)landMarkPoints { UIImage * newImage = image; for (NSMutableArray *points in landMarkPoints) { CGPoint sPoints [points.count]; for (int i = 0;i value2) { return NSOrderedDescending; }else if (value1 == value2){ return NSOrderedSame; }else{ return NSOrderedAscending; } }]; NSArray *sortPointYs = [pointYs sortedArrayWithOptions:NSSortStable usingComparator: ^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { int value1 = [obj1 floatValue]; int value2 = [obj2 floatValue]; if (value1 > value2) { return NSOrderedDescending; }else if (value1 == value2){ return NSOrderedSame; }else{ return NSOrderedAscending; } }]; UIImage *image =[UIImage imageNamed:@"eyes"]; CGFloat imageWidth = [sortPointXs.lastObject floatValue] - [sortPointXs.firstObject floatValue] + 40; CGFloat imageHeight = (imageWidth * image.size.height)/image.size.width; self.glassesImageView.frame = CGRectMake([sortPointXs.firstObject floatValue]-20, [sortPointYs.firstObject floatValue]-5, imageWidth, imageHeight); }); } }]; ``` 由于时间关系,代码有点乱,将就将就 先说说思路,我是想动态添加一个眼镜的,所以我必须先得到两个眼睛的位置,然后在计算出两个眼睛的宽高,最后适当的调整眼镜的大小,再动态的添加上去 这里必须要说的一个问题,就是我在实现过程中遇到的---`坐标` 首先是`y`坐标,如果还是按照静态图片的那种获取方式,那么得到的结果将会是完全相反的。 ``` faceObservation.boundingBox.origin.y * image.size.height + point.y * rectHeight ``` 这里我做了 一个假设,估计是由于摄像机成像的原因造成的,所以必须反其道而行,于是我如下改造了下 ``` CGFloat boundingBoxY = self.view.bounds.size.height * (1 - faceObservation.boundingBox.origin.y - faceObservation.boundingBox.size.height); p = CGPointMake(point.x * rectWidth + faceObservation.boundingBox.origin.x * self.view.bounds.size.width, boundingBoxY + (1-point.y) * rectHeight); ``` 从中可以看到,所有的`point.y`都用`1`减去了,这个试验的过程有点恼火,我还没怎么相通,若有知道的,希望可以告诉我下,当然我也会再研究研究。 再说完`y`坐标后,就是`x`坐标了,`x`坐标在`前置摄像头`的时候一切正常,然而在切换成`后置摄像头`的时候,又反了。😔!心累啊,所以没办法,我就只要加判断,然后进行测试,有了如下代码 ``` CGFloat boundingX = self.view.frame.size.width - faceObservation.boundingBox.origin.x * self.view.bounds.size.width - rectWidth; ``` 最后终于大功告成! 效果就是文章最顶的那个效果 ##### 注意 1、在使用过程中,我发现当检测图片的时候内存和`cpu`的消耗还是很高的,比如我的`5s`就成功的崩溃过..... 2、图片方向是有要求的.... ``` - (instancetype)initWithCVPixelBuffer:(CVPixelBufferRef)pixelBuffer options:(NSDictionary *)options; /*! @brief initWithCVPixelBuffer:options creates a VNImageRequestHandler to be used for performing requests against the image passed in as buffer. @param pixelBuffer A CVPixelBuffer containing the image to be used for performing the requests. The content of the buffer cannot be modified for the lifetime of the VNImageRequestHandler. @param orientation The orientation of the image/buffer based on the EXIF specification. For details see kCGImagePropertyOrientation. The value has to be an integer from 1 to 8. This superceeds every other orientation information. @param options A dictionary with options specifying auxilary information for the buffer/image like VNImageOptionCameraIntrinsics */ - (instancetype)initWithCVPixelBuffer:(CVPixelBufferRef)pixelBuffer orientation:(CGImagePropertyOrientation)orientation options:(NSDictionary *)options; ``` 通过对比上面两个函数,我们可以发现,多了一个`CGImagePropertyOrientation `类型的参数,没错,这就是指定传入图片的方向,如果指定了方向,而图片方向却不一致,那么恭喜你,检测不出来....这里我用的都是第一个方法,及没有参数,好像默认是`up`的。 ##### 最后 还是附上[Demo](https://gitee.com/zymITsky/VisionDemo),如果觉得还行的话,欢迎大家给个`star`!有什么问题,可以多多沟通