前言
Metal入门教程(一)图片绘制
Metal入门教程(二)三维变换
前面的教程介绍了如何绘制一张图片和如何把图片显示到3D物体上并进行三维变换,这次介绍如何用Metal渲染摄像头采集到的图像。
Metal系列教程的代码地址;
OpenGL ES系列教程在这里;
你的star和fork是我的源动力,你的意见能让我走得更远。
正文
核心思路
用AVFoundation
采集摄像头数据得到CMSampleBufferRef
,用CoreVideo
提供的方法将图像数据转为Metal的纹理,再用MetalPerformanceShaders
的高斯模糊滤镜对图像进行处理,结果展示到屏幕上。
效果展示
具体步骤
1、Metal相关设置
- (void)setupMetal {
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
self.mtkView.device = MTLCreateSystemDefaultDevice();
[self.view insertSubview:self.mtkView atIndex:0];
self.mtkView.delegate = self;
self.mtkView.framebufferOnly = NO;
self.commandQueue = [self.mtkView.device newCommandQueue];
CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
}
除了正常创建和初始化MTKView
之外,这里还多两行代码:
- 设置
MTKView
的dramwable纹理是可读写的;(默认是只读) - 创建
CVMetalTextureCacheRef _textureCache
,这是Core Video的Metal纹理缓存;
2、摄像头采集设置
- (void)setupCaptureSession {
self.mCaptureSession = [[AVCaptureSession alloc] init];
self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL); // 串行队列
AVCaptureDevice *inputCamera = nil;
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if ([device position] == AVCaptureDevicePositionBack) {
inputCamera = device;
}
}
self.mCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil];
if ([self.mCaptureSession canAddInput:self.mCaptureDeviceInput]) {
[self.mCaptureSession addInput:self.mCaptureDeviceInput];
}
self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
[self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
// 这里设置格式为BGRA,而不用YUV的颜色空间,避免使用Shader转换
[self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
[self.mCaptureDeviceOutput setSampleBufferDelegate:self queue:self.mProcessQueue];
if ([self.mCaptureSession canAddOutput:self.mCaptureDeviceOutput]) {
[self.mCaptureSession addOutput:self.mCaptureDeviceOutput];
}
AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait]; // 设置方向
[self.mCaptureSession startRunning];
}
创建AVCaptureSession
、AVCaptureDeviceInput
和AVCaptureVideoDataOutput
,注意在创建AVCaptureVideoDataOutput
时,需要指定内容格式,这里使用的是BGRA的格式;
同时需要设定采集的方向,否则图像会出现旋转;
3、摄像头采集回调
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
CVMetalTextureRef tmpTexture = NULL;
// 如果MTLPixelFormatBGRA8Unorm和摄像头采集时设置的颜色格式不一致,则会出现图像异常的情况;
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
if(status == kCVReturnSuccess)
{
self.mtkView.drawableSize = CGSizeMake(width, height);
self.texture = CVMetalTextureGetTexture(tmpTexture);
CFRelease(tmpTexture);
}
}
这是demo的核心内容,摄像头回传CMSampleBufferRef
数据,找到CVPixelBufferRef
,用CVMetalTextureCacheCreateTextureFromImage
创建CoreVideo的Metal纹理缓存CVMetalTextureRef
,最后通过CVMetalTextureGetTexture
得到Metal的纹理;
这个过程与Metal入门教程(一)图片绘制使用device newTextureWithDescriptor
创建纹理,再通过texture replaceRegion
的方式上传纹理数据类似,但是性能上有提升。
4、渲染处理
- (void)drawInMTKView:(MTKView *)view {
if (self.texture) {
id commandBuffer = [self.commandQueue commandBuffer]; // 创建指令缓冲
id drawingTexture = view.currentDrawable.texture; // 把MKTView作为目标纹理
MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:1]; // 这里的sigma值可以修改,sigma值越高图像越模糊
[filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture]; // 把摄像头返回图像数据的原始数据
[commandBuffer presentDrawable:view.currentDrawable]; // 展示数据
[commandBuffer commit];
self.texture = NULL;
}
}
这也是demo的核心内容,MetalPerformanceShaders
是Metal的一个集成库,有一些滤镜处理的Metal实现,demo选用其中的高斯模糊处理MPSImageGaussianBlur
;
MPSImageGaussianBlur
以一个Metal纹理作为输入,以一个Metal纹理作为输出;
这里的输入是从摄像头采集的图像,也即是第三步创建的纹理;输出的纹理是MTKView
的currentDrawable.texture
;
在绘制完之后调用presentDrawable:
展示渲染结果。
注意事项
1、运行后Crash,提示frameBufferOnly texture not supported for compute
这是因为MTKView
的drawable
纹理默认是只用来展示渲染结果,只允许作为framebuffer attachments
,需要设置framebufferOnly
为NO;
self.mtkView.framebufferOnly = NO;
2、图像显示异常,偏绿or偏蓝
如果MTLPixelFormatBGRA8Unorm和摄像头采集时设置的颜色格式不一致,则会出现图像异常的情况,以下两行代码需要设置同样的格式:
[self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
3、图像显示异常,倒置or旋转
如果出现图像朝向异常的情况,可以通过下面的两种方式进行修改:
- 修改
AVCaptureConnection
的朝向:
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
- 或者给MTKView增加旋转变换:
self.mtkView.transform = CGAffineTransformMakeRotation(M_PI / 2);
总结
本文有两个核心点:从CVPixelBufferRef
创建Metal纹理以及MetalPerformanceShaders
的使用和理解,这两个点也引入后续Metal更复杂的能力,分别是视频渲染和自定义Shader计算。
同时从这个demo可以看到相对OpenGL,Metal对图像的处理更为方便,代码也更为精简。
代码的地址在这里,欢迎交流。