Metal摄像头采集图像

在iOS开发当中,我们的项目可能需要用到摄像头采集视频内容,而捕捉视频我们常用的是AVFoundation框架,然后使用AVCaptureVideoPreviewLayer来预览视频,那么我们如何使用Metal来实现视频的预览呢?也就是说使用Metal来代替AVCaptureVideoPreviewLayer实现预览功能。

我们先来了解一下音视频采集的基本流程:
首先由AVFoundation来捕获视频,然后由CMSamplerBufferRef采集视频数据,最后将音视频数据显示在屏幕上。

接下来我们来讲一下AVCaptureVideoPreviewLayer直接预览的思路:

1、采集数据->AVFoundation->CMSampleBufferRef
2、CoreVideo图像数据->Metal纹理
3、Metal纹理渲染

代码实现:
1、引入头文件和模块:

#import "ViewController.h"
#import 
@import MetalKit;
@import AVFoundation;
@import CoreMedia;

@interface ViewController ()

@property(nonatomic,strong)MTKView *mtkView;
//负责输入和输出设备之间的数据传递
@property(nonatomic,strong)AVCaptureSession *mCaptureSession;
//负责从AVCaptureDevice获得输入数据
@property(nonatomic,strong)AVCaptureDeviceInput *mCaptureDeviceInput;
//输出设备
@property(nonatomic,strong)AVCaptureVideoDataOutput *mCaptureDeviceOutput;
//处理队列
@property(nonatomic,strong)dispatch_queue_t mProgressQueue;
//处理缓存区
@property(nonatomic,assign)CVMetalTextureCacheRef textureCache;
//命令队列
@property(nonatomic,strong)id commandQueue;
//纹理
@property(nonatomic,strong)id texture;

@end

2、将MTKView显示在屏幕上

-(void)setupMetal{
    self.mtkView = [[MTKView alloc]initWithFrame:self.view.frame device:MTLCreateSystemDefaultDevice()];
    [self.view addSubview:self.mtkView];
    self.mtkView.delegate = self;
    
    //创建命令队列
    self.commandQueue = [self.mtkView.device newCommandQueue];
    
    //允许读写操作
    self.mtkView.framebufferOnly = NO;
    
    CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
}

3、使用AVFoundation捕获视频:

-(void)setupCaptureSession{
    //创建session
    self.mCaptureSession = [[AVCaptureSession alloc]init];
    //设置分辨率
    self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
    
    //创建串行队列
    self.mProgressQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);
    
    //获取摄像头设备(前置/后置摄像头)
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    AVCaptureDevice *inputCamera = nil;
    
    //循环设备数组,找到后置摄像头
    for (AVCaptureDevice *device in devices) {
        if([device position] == AVCaptureDevicePositionBack){
            inputCamera = device;
        }
    }
    
    //将AVCaptureDevice转换为AVCaptureDeviceInput
    self.mCaptureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:inputCamera error:nil];
    
    //将设备添加到session中
    if([self.mCaptureSession canAddInput:self.mCaptureDeviceInput]){
        [self.mCaptureSession addInput:self.mCaptureDeviceInput];
    }
    
    //创建AVCaptureVideoDataOutput对象
    self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc]init];
    /*设置视频帧延迟到底时是否丢弃数据.
    YES: 处理现有帧的调度队列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止时,对象会立即丢弃捕获的帧。
    NO: 在丢弃新帧之前,允许委托有更多的时间处理旧帧,但这样可能会内存增加.
    */
    [self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
    
    //这里设置格式为BGRA,而不用YUV的颜色空间,避免使用Shader转换
    //注意:这里必须和后面CVMetalTextureCacheCreateTextureFromImage 保存图像像素存储格式保持一致.否则视频会出现异常现象.
    
    [self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
    
    //设置视频捕捉输出代理方法
    [self.mCaptureDeviceOutput setSampleBufferDelegate:self queue:self.mProgressQueue];
    
    //添加输出
    if([self.mCaptureSession canAddOutput:self.mCaptureDeviceOutput]){
        [self.mCaptureSession addOutput:self.mCaptureDeviceOutput];
    }
    
    //输入与输出链接
    AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
    
    //设置视频方向
    [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
    
    //开始捕捉
    [self.mCaptureSession startRunning];
}

4、实现MTKViewDelegate,AVCaptureVideoDataOutputSampleBufferDelegate两个代理的方法

//视频采集回调方法
-(void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
    //从sampleBuffer获取视频像素缓存区对象
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    //获取捕捉视频的宽高
    size_t width = CVPixelBufferGetWidth(pixelBuffer);
    size_t height = CVPixelBufferGetHeight(pixelBuffer);
    
    //根据视频像素缓存区创建Metal纹理缓存区
    CVMetalTextureRef tmpTexture = NULL;
    CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
    
    
    //判断tmpTexture是否创建成功
    if(status == kCVReturnSuccess){
        //设置可绘制纹理的当前大小
        self.mtkView.drawableSize = CGSizeMake(width, height);
        //返回纹理缓冲区的Metal纹理对象
        self.texture = CVMetalTextureGetTexture(tmpTexture);
        //释放
        CFRelease(tmpTexture);
    }
}

//视频渲染会调用此方法
- (void)drawInMTKView:(nonnull MTKView *)view {
    //判断是否获取AVFoundation采集纹理数据
    if(self.texture){
        //创建指令缓冲
        id commandBuffer = [self.commandQueue commandBuffer];
        
        id drawingTexture = view.currentDrawable.texture;
        
        //设置滤镜
         //创建高斯滤镜处理filter
         //注意:sigma值可以修改,sigma值越高图像越模糊;
        MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc]initWithDevice:self.mtkView.device sigma:1];
        
        [filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
        
        //展示显示内容
        [commandBuffer presentDrawable:view.currentDrawable];
        
        //提交
        [commandBuffer commit];
        
        //清空当前纹理
        self.texture = NULL;
        
    }
}

这样就实现了使用Metal代替layer来预览视频的功能了。

下面补充一下图片视频解压缩的一些基本内容:

我们常见的图片格式基本上都是jpg和png,那这两种图片格式有什么区别呢?
那就是编码格式不同。

那我们为什么要对图片进行编码呢?
我们来计算一张1280 * 720位图的大小:
1280 * 720 = 2.63M

一张图片2.63M其实是比较大的,我们知道视频是通过一帧一帧来渲染的,那如果一帧大小为2.63M,1秒/60那占用的内存是相当大的,因此我们需要一些编码方式来降低图片或者视频占用内存空间的大小。

这时候就需要引入RGBA和YUV的概念了,这是两种图片、视频输出格式,我们先来了解一下RGBA这种格式:

RGBA四个字母分别代表了红,绿,蓝,透明度,其中RGB三种不同的比例可以产生很多种颜色。

我们用一张1280 * 720大小图片为例,它代表着有1280 * 720个像素点,其中每一个像素点的颜色显示都采用RGB编码方法。其中RGB每种原色都占用8bit,也就是一个字节,那么一个像素点就占用了24bit,三个字节

那么一张图片大小就占用了:1280*720*3/1024/1024 = 2.63M的存储空间

继续讲述一下YUV这种编码格式:
YUV颜色编码采用的是明亮度和色调来指定像素颜色,其中Y指的是明亮度,U、V指的是色调和饱和度。

YUV和RGB类似,每个像素点都包括YUV分量,但是它的Y和UV分量是可以分离的,如果没有UV分量,一样可以显示完整的图像,只不过是黑白的。
对于YUV图像来说,并不是每个像素点都需要包含了Y,U,V三个分量,根据不同的采样格式,可以每个y分量都对应自己的UV分量,也可以几个y分量公用UV分量。

YUV采样格式有很多种,这边只介绍3种:
第一种:4:4:4
意味着y,u,v三个分量采样比例相同。
例如:原始图像像素为:[Y0,U0,V0],[Y1,U1,V1],[Y2,U2,V2],[Y3,U3,V3]
按照4:4:4采样为Y0,U0,V0,Y1,U1,V1,Y2,U2,V2,Y3,U3,V3
最后映射还原像素点为:[Y0,U0,V0],[Y1,U1,V1],[Y2,U2,V2],[Y3,U3,V3]
采用4:4:4的方式和采用RGB模式占用内存大小是一样的,并没有节省带宽目的。

第二种:4:2:2
这种方式意味着UV分量是Y分量的一半。
例如:[Y0,U0,V0],[Y1,U1,V1],[Y2,U2,V2],[Y3,U3,V3]
按照4:2:2采样为:Y0,U0,Y1,V1,Y2,U2,Y3,V3

其中,每采样一个像素点,都会采用Y分量,而U,V会间隔一个采集一个,
最后还原为:[Y0,U0,V1],[Y1,U0,V1],[Y2,U2,V3],[Y3,U2,V3]

那么一张1280*720大小的图片,在采用4:2:2之后,内存大小为:
(1280 * 720 * 8 +1280 * 720 * 0.5 * 8 * 2)/8/1024/102 = 1.76M
达到了节省空间的目的。

第三种:4:2:0
这种方式大概意思就是采左右和上下的分量。
例如:原始像素
[Y0,U0,V0],[Y1,U1,V1],[Y2,U2,V2],[Y3,U3,V3]
[Y5,U5,V5],[Y6,U6,V6],[Y7,U7,V7],[Y8,U8,V8]

按4:2:0采样为:Y0,U0,Y1,Y2,U2,Y3,Y5,V5,Y6,Y7,V7,Y8

还原为:[Y0,U0,V5],[Y1,U0,V5],[Y2,U2,V7],[Y3,U2,V7]
[Y5,U0,V5],[Y6,U0,V5],[Y7,U2,V7],[Y8,U2,V7]

那么图片占用大小:(1280 * 720 * 8 + 1280 * 720 * 0.25 * 8 * 2)/8/1024.1024 = 1.32M
这种方式节省了接近一半的空间。

你可能感兴趣的:(Metal摄像头采集图像)