iOS相机简单封装及理解(tl:tr)(下篇)

猪小花1号2018-08-27 09:16

4.3 人脸跟踪

FaceAlignment是个基于openCV 的人脸特征识别框架库,支通过iOS相机获取的32BGRA图像获取面部特征点;支持各个相机姿态(camera angle)识别人脸特征点。

传入通过相机获取的UIImage和rotationAngleDegrees

CGFloat degress = [[LOCMotionManager sharedMotionManager]rotationAngleDegrees];
NSString *result = [[MMFaceAlignmentor sharedInstance] detectAlignmentor:image camAngle:degress frontCamera:YES];

对应的结果为:

degress is 4.880134 result is {
     "status":   1,
     "faceNum":  1,
     "faces":    [{
     "rects":    "169 62 272 272 ",
     "pts":    "294,267;333,263;370,249;401,224;413,190;404,148;374,113;327,99;277,100;259,251;261,225;258,191;249,157;282,207;322,207;340,219;342,202;338,185;288,251;281,244;281,234;289,223;292,232;292,244;285,180;275,171;274,158;279,146;284,156;285,169;372,223;361,201;369,166;381,199;369,201;368,201"
}]}

即完成了对视频流一帧的处理 ,返回参数:

字段 说明
status 检测的返回结果状态,正常状态为1,出错状态为-1
faceNum 检测到的人脸的个数
faces 用于存储检测到的各个人脸相关信息的数组
face 人脸框的位置,分别为x,y,width,height,其中x表示距离图像左边框的像素点数量,y表示距离图像上方便框的像素点数量,width表示人脸框的宽度,height表示人脸框的高度。
pts 用于存放人脸中检测到的关键点,共36个关键点,按照xyxy的方式排列,x和y的含义同上。

各个参数以及关键点排列方式

4.4 文档矫正

MMTextDetector 是基于openCVboost的文档检测框架,支持传入 RGB色彩空间的 UIImage来返回特征点(四个角点)或者矫正后的UIImage。包含以下功能:

  • 实时文档检测(速度较快,可以实时调用)
  • 精确文档检测(速度较慢)
  • 黑白文档图像增强
  • 彩色文档图像增强
  • 长宽比估计,文档Deskew

以下为SDK使用案例:

原图

 /**
  给定MMTextDetectorResult(四个角点),对图像做矫正,精度高。该方法运算耗时,处理连续帧会消耗大量资源

  @param origRGBImage 输入图像,需要为RGB色彩空间
  @param enhance 是否增强
  @param detectorResult MMTextDetectorResult
  @return 返回矫正后的图片
  */
 - (UIImage *)deskew:(UIImage *)origRGBImage
        enhance:(BOOL)enhance
 detectorResult:(MMTextDetectorResult *)detectorResult;

deskew

 /**
  实时点位返回,精度比单张矫正低,但是运算效率高

  @param image origRGBImage
  @param maxLostFocus 最大丢帧参数:数值越大越不容易抖动;越小越敏感;默认为5
  @return MMTextDetectorResult
  */
 - (MMTextDetectorResult *)realTimeDetect:(UIImage *)image
                        maxLostFocus:(unsigned int)maxLostFocus;

实时检测和返回点位

点击此处参考Demo代码

4.5 ARKit

    ARKit使用视觉惯性里程计(VIO)追踪四周的环境。VIO结合相机传感器与CoreMotion数据,以高精度来跟踪对象的移动。ARKit封装了ARFrame、ARSession、ARCamera等类,将相机初始化和数据采集等做了进一步封装。ARCamera表示虚拟摄像头,用来虚拟设备的角度和位置。而ARSession 则是 AVCaptureSession 和 CMMotionManager 的封装。只要设定 ARSession 的ARSessionConfiguration 调用 run 方法即可。

换角度

4.6 CoreML + Vision

    CoreML 框架极大地降低了简化了iOS平台机器学习相关代码的搭建。一个很显然的对比就是CoreMLDemo 和 VGGNet-Metal 的实现,两者的实现结果一致,但是用Metal中的矩阵运算相关的代码比起以下简单几行核心代码就能集成VGG16模型的预测。

  1. 初始化代码 VNCoreMLRequest

     request = VNCoreMLRequest(model: visionModel, completionHandler: requestDidComplete)
  2. 对捕捉的CVPixelBuffer进行预测

    func predict(pixelBuffer: CVPixelBuffer) {
        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
        try? handler.perform([request])
    }
  3. 获取结果

    func requestDidComplete(request: VNRequest, error: Error?) {
        if let observations = request.results as? [VNClassificationObservation] {
            // 显示五种概率最高的结果
            let top5 = observations.prefix(through: 4)
            .map { ($0.identifier, Double($0.confidence)) }
            DispatchQueue.main.async {
                self.show(results: top5)
            }
        }
    }

  4. 简单几行代码实现VGG16模型预测

4.7 Google Translate

     Google Translate 通过摄像头取词照片扫描等功能,实现了照片或实景翻译的功能,技术上看,是翻译+跟踪算法的结合。

原图

使用效果

5 相机使用Tips

     在使用相机的时候,我们经常需要在iOS中处理各种格式的图片数据。比如从相机获取的bitmap可能是封装在CMSampleBuffer结构体中的,展示图片需要的图片需要转换成UIImage,而在OpenCV中处理又需要转换成mat矩阵。以下有一些简单的代码提示,可能对未太多接触iOS图像格式转换的开发者有些许帮助。一下对色彩空间YUV和RGB以及相机的源数据CMSampleBuffer以及openCV中的cvMat做一些简单介绍和格式转换处理。

5.1 CMSampleBuffer 和色彩空间

CMSampleBuffer

CMSampleBuffer 是一个Core Foundation 对象,CMSampleBuffer包含了多媒体采样的一种封装(音频、视频、音视频混合等等)。用于传输媒体信号。该结构体位于 CoreVideo 框架中的 CVPixelBuffer.h 中。在iOS相机中,CMSampleBuffer 通常用来包含连续的多媒体流。在使用相机的时候,我们可以认为通过它可以获取到相机的每一帧源数据。需要注意的是CMSampleBuffer 获取的数据和相机启动时候设置相关,一个比较重要的参数是色彩空间。 iOS相机常用的色彩空间有 kCVPixelFormatType32BGRA(32 bit BGRA) 和kCVPixelFormatType420YpCbCr8Planar ( Planar Component Y'CbCr 8-bit 4:2:0. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct )

YUV和RGB:
    我们来看一看YUV在维基百科中的介绍 
    YUV,是一种颜色编码方法。常使用在各个影像处理元件中。YUV在对照片或影片编码时,考虑到人类的感知能力,允许降低色度的带宽。具体如何呢?
    原来用RGB(R,G,B 都是 8bit unsigned) 模型,1个点需要8x3=24bits,(全采样后, YUV 仍各占 8bit )。YUV在不牺牲画质的情况下,按 4:2:0 采样后,仅需要 8+(8/2)+(8/0)=12bits,这样就把图像的数据压缩了一半。当然,以上格式如果是带alpha通道,还会再增加额外的开销。
     RGB 是从颜色发光的原理来设计定的,通俗点说它的颜色混合方式就好像有红、绿、蓝三盏灯,当它们的光相互叠合的时候,色彩相混,而亮度却等于两者亮度之总和(两盏灯的亮度嘛!),越混合亮度越高,即加法混合。
YUV 与 RGB 转换公式 :

Y = 0.257R + 0.504G + 0.098B + 16
U = 0.148R - 0.291G + 0.439B + 128
V = 0.439R - 0.368G - 0.071B + 128
B = 1.164(Y - 16) + 2.018(U - 128)
G = 1.164(Y - 16) - 0.813(V - 128) - 0.391(U - 128)
R = 1.164(Y - 16) + 1.596(V - 128)

想了解更多YUV可以参考维基百科的和这篇文章

cvMAT:

OpenCV中,保存图像像素信息的是数据结构Mat阵,Mat即Matrix,Mat的基本参数有长、宽、像素形态、像素深度、通道数量,iOS开发者可能需要了解这些,以及他和CMSampleBuffer以及UIImage之间的转换,具体到数据在openCV中的处理可能需要交给图像处理工程师。

5.2 UIImage和OpenCV中的mat转换

如果需要将色彩空间为32BGRA的CMSampleBufferRef转成mat

 -(void)processBuffer:(CMSampleBufferRef)buffer
{
    //1 获取CVImageBufferRef,锁住当前帧的地址
    CVImageBufferRef imgBuf = CMSampleBufferGetImageBuffer(buffer);
    CVPixelBufferLockBaseAddress(imgBuf, 0);

    // 获取地址
    void *imgBufAddr = CVPixelBufferGetBaseAddressOfPlane(imgBuf, 0);

    // 获取width和height
    int w = (int)CVPixelBufferGetWidth(imgBuf);
    int h = (int)CVPixelBufferGetHeight(imgBuf);

    // 创建mat
    cv::Mat image;
    image.create(h, w, CV_8UC4);
    memcpy(image.data, imgBufAddr, w * h);

    // 解锁
    CVPixelBufferUnlockBaseAddress(imgBuf, 0);

    //Use Mat here
}

YUV通道可以通过设置CV传入的参数CV_8UC4设置传出的Mat类型。

将UIImage转成cvMat

- (cv::Mat)cvMatFromUIImage:(UIImage *)image
{
  CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
  CGFloat cols = image.size.width;
  CGFloat rows = image.size.height;

  cv::Mat cvMat(rows, cols, CV_8UC4); // 8 bits per component, 4 channels (color channels + alpha)

  CGContextRef contextRef = CGBitmapContextCreate(cvMat.data,                 // Pointer to  data
                                             cols,                       // Width of bitmap
                                             rows,                       // Height of bitmap
                                             8,                          // Bits per component
                                             cvMat.step[0],              // Bytes per row
                                             colorSpace,                 // Colorspace
                                             kCGImageAlphaNoneSkipLast |
                                             kCGBitmapByteOrderDefault); // Bitmap info flags

  CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
  CGContextRelease(contextRef);

  return cvMat;
}

cvMat转UIImage

-(UIImage *)UIImageFromCVMat:(cv::Mat)cvMat
{
  NSData *data = [NSData dataWithBytes:cvMat.data length:cvMat.elemSize()*cvMat.total()];
  CGColorSpaceRef colorSpace;

  if (cvMat.elemSize() == 1) {
      colorSpace = CGColorSpaceCreateDeviceGray();
  } else {
      colorSpace = CGColorSpaceCreateDeviceRGB();
  }

  CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);

  // Creating CGImage from cv::Mat
  CGImageRef imageRef = CGImageCreate(cvMat.cols,                                 //width
                                 cvMat.rows,                                 //height
                                 8,                                          //bits per component
                                 8 * cvMat.elemSize(),                       //bits per pixel
                                 cvMat.step[0],                            //bytesPerRow
                                 colorSpace,                                 //colorspace
                                 kCGImageAlphaNone|kCGBitmapByteOrderDefault,// bitmap info
                                 provider,                                   //CGDataProviderRef
                                 NULL,                                       //decode
                                 false,                                      //should interpolate
                                 kCGRenderingIntentDefault                   //intent
                                 );


  // Getting UIImage from CGImage
  UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
  CGImageRelease(imageRef);
  CGDataProviderRelease(provider);
  CGColorSpaceRelease(colorSpace);

  return finalImage;
 }

CMSampleBufferRef转UIImage

+ (UIImage *)imageFromSampleBuffer32BGRA:(CMSampleBufferRef)sampleBuffer
{
    //1 获取CVImageBufferRef,锁住当前帧的地址
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); 
    CVPixelBufferLockBaseAddress(imageBuffer,0); 

    //2 获取第1个通道的数据、BytesPerRow、width以及height
    uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0); 
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);

    //3 设置色彩空间,创建CGContextRef,并从上下文中获取CGImageRef
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef newContext = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    CGImageRef newImage = CGBitmapContextCreateImage(newContext);

    //4. 释放操作
    CGContextRelease(newContext);
    CGColorSpaceRelease(colorSpace);
    CVPixelBufferUnlockBaseAddress(imageBuffer,0);

    //5. 获取色彩空间为RGB的UIImage
    UIImage *result = [UIImage imageWithCGImage:newImage];
    return result;
}

如果是YUV通道的CMSampleBufferRef,我们需要先将YUV的数据转成RGB,该过程在现在的iOS设备上需要耗费毫秒级别的时间,如果是对性能敏感的连续帧之类的地方(如相机30*3ms = 90ms,将让画面变得卡顿),应当尽量避免此类操作,而从相机硬件直接采集RGB数据。具体的转换代码可以参考如下:

+(UIImage *)imageFromSampleBufferY420:(CMSampleBufferRef )sampleBuffer
{
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CVPixelBufferLockBaseAddress(imageBuffer,0);

    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);
    uint8_t *yBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
    size_t yPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
    uint8_t *cbCrBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
    size_t cbCrPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);

    //转换代码
    int bytesPerPixel = 4;
    uint8_t *rgbBuffer = malloc(width * height * bytesPerPixel);

    for(int y = 0; y < height; y++) {
        uint8_t *rgbBufferLine = &rgbBuffer[y * width * bytesPerPixel];
        uint8_t *yBufferLine = &yBuffer[y * yPitch];
        uint8_t *cbCrBufferLine = &cbCrBuffer[(y >> 1) * cbCrPitch];

        for(int x = 0; x < width; x++) {
            int16_t y = yBufferLine[x];
            int16_t cb = cbCrBufferLine[x & ~1] - 128;
            int16_t cr = cbCrBufferLine[x | 1] - 128;

            uint8_t *rgbOutput = &rgbBufferLine[x*bytesPerPixel];

            int16_t r = (int16_t)roundf( y + cr *  1.4 );
            int16_t g = (int16_t)roundf( y + cb * -0.343 + cr * -0.711 );
            int16_t b = (int16_t)roundf( y + cb *  1.765);

            rgbOutput[0] = 0xff;
            rgbOutput[1] = clamp(b);
            rgbOutput[2] = clamp(g);
            rgbOutput[3] = clamp(r);
        }
    }

    //获取到rgbBuffer数据之后
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(rgbBuffer, width, height, 8, width * bytesPerPixel, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
    CGImageRef quartzImage = CGBitmapContextCreateImage(context);
    UIImage *image = [UIImage imageWithCGImage:quartzImage];

    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    CGImageRelease(quartzImage);
    free(rgbBuffer);

    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);

    return image;
}

6 参考资料


    本文整体介绍了相机封装、AVFoundation、以及GPUImage、Core Image以及相机和OpenCV等相关结合的应用。以及介绍了CMSampleBuffer、mat和UIImage之间的转换,对于从事相机相关开发的同学应该有所帮助。

相关阅读:

iOS相机简单封装及理解(tl:tr)(上篇)

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者金立涨授权发布。