Metal 四、視頻采集預覽渲染+YUV


一、Metal 實現視頻預覽

首先我們知道視頻其實就是一幀幀的圖片。

渲染業務流程:

(注:AVFoundation 有提供的預覽圖層: AVCaptureVideoPreviewLayer)

 

0、初始化工作

    // 1.獲取 MTKView 預覽圖層
    self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
    self.mtkView.device = MTLCreateSystemDefaultDevice();
    [self.view insertSubview:self.mtkView atIndex:0];
    self.mtkView.delegate = self;
    
    // 2.創建命令隊列
    self.commandQueue = [self.mtkView.device newCommandQueue];
    
    // 3.注意: 在初始化MTKView 的基本操作以外. 還需要多下面2行代碼.
    /*
     1. 設置MTKView 的drawable 紋理是可讀寫的(默認是只讀);
     2. 創建CVMetalTextureCacheRef _textureCache; 這是Core Video的Metal紋理緩存
     */
    // 允許讀寫操作
    self.mtkView.framebufferOnly = NO;
    /*
     CVMetalTextureCacheCreate(CFAllocatorRef  allocator,
     CFDictionaryRef cacheAttributes,
     id <MTLDevice>  metalDevice,
     CFDictionaryRef  textureAttributes,
     CVMetalTextureCacheRef * CV_NONNULL cacheOut )
     功能: 創建紋理緩存區

     參數1: allocator 內存分配器.默認即可.NULL
     參數2: cacheAttributes 緩存區行為字典.默認為NULL
     參數3: metalDevice
     參數4: textureAttributes 緩存創建紋理選項的字典. 使用默認選項NULL
     參數5: cacheOut 返回時,包含新創建的紋理緩存。
     */
    CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);

1、通過 AVFoundation 進行視頻采集

 1     // 1.創建mCaptureSession
 2     self.mCaptureSession = [[AVCaptureSession alloc] init];
 3     // 設置視頻采集的分辨率
 4     self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
 5   
 6     // 2.創建串行隊列
 7     self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);
 8    
 9     // 3.獲取攝像頭設備(前置/后置攝像頭)
10     NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
11     AVCaptureDevice *inputCamera = nil;
12     // 循環設備數組,找到后置攝像頭.設置為當前inputCamera
13     for (AVCaptureDevice *device in devices) {
14         if ([device position] == AVCaptureDevicePositionBack) {
15             inputCamera = device;
16         }
17     }
18     
19     // 4.將 AVCaptureDevice 轉換為 AVCaptureDeviceInput
20     self.mCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil];
21     
22     // 5. 將設備添加到 mCaptureSession 中
23     if ([self.mCaptureSession canAddInput:self.mCaptureDeviceInput]) {
24         [self.mCaptureSession addInput:self.mCaptureDeviceInput];
25     }
26     
27     // 6.創建 AVCaptureVideoDataOutput 對象 --> 輸出
28     self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
29     
30     /*設置視頻幀延遲到底時是否丟棄數據.
31      YES: 處理現有幀的調度隊列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止時,對象會立即丟棄捕獲的幀。
32      NO: 在丟棄新幀之前,允許委托有更多的時間處理舊幀,但這樣可能會內存增加.
33      */
34     [self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
35     
36     // 這里設置格式為 BGRA,而不用YUV的顏色空間,避免使用Shader轉換
37     // 注意: 這里必須和 CVMetalTextureCacheCreateTextureFromImage 保存圖像像素存儲格式保持一致,否則視頻會出現異常現象
38     [self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
39     
40     // 設置視頻捕捉輸出的代理方法
41     [self.mCaptureDeviceOutput setSampleBufferDelegate:self queue:self.mProcessQueue];
42     
43     // 7.添加輸出
44     if ([self.mCaptureSession canAddOutput:self.mCaptureDeviceOutput]) {
45         [self.mCaptureSession addOutput:self.mCaptureDeviceOutput];
46     }
47     
48     // 8.輸入與輸出鏈接
49     AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
50     
51     // 9.設置視頻方向
52     // 注意: 一定要設置視頻方向,否則視頻會是朝向異常的
53     [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
54     
55     // 10.開始捕捉
56     [self.mCaptureSession startRunning];

2、Metal 進行視圖的渲染 -- 2個 delegate 方法

#pragma mark - AVFoundation Delegate -
// AVFoundation 視頻采集回調方法
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    
    // 1.從sampleBuffer 獲取視頻像素緩存區對象
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
   
    // 2.獲取捕捉視頻的寬和高
    size_t width = CVPixelBufferGetWidth(pixelBuffer);
    size_t height = CVPixelBufferGetHeight(pixelBuffer);
    
    /* 3. 根據視頻像素緩存區 創建 Metal 紋理緩存區
     CVReturn CVMetalTextureCacheCreateTextureFromImage(CFAllocatorRef allocator,                         CVMetalTextureCacheRef textureCache,
     CVImageBufferRef sourceImage,
     CFDictionaryRef textureAttributes,
     MTLPixelFormat pixelFormat,
     size_t width,
     size_t height,
     size_t planeIndex,
     CVMetalTextureRef  *textureOut);
     
     功能: 從現有圖像緩沖區創建核心視頻Metal紋理緩沖區。
     參數1: allocator 內存分配器,默認kCFAllocatorDefault
     參數2: textureCache 紋理緩存區對象
     參數3: sourceImage 視頻圖像緩沖區
     參數4: textureAttributes 紋理參數字典.默認為NULL
     參數5: pixelFormat 圖像緩存區數據的Metal 像素格式常量.注意如果MTLPixelFormatBGRA8Unorm和攝像頭采集時設置的顏色格式不一致,則會出現圖像異常的情況;
     參數6: width,紋理圖像的寬度(像素)
     參數7: height,紋理圖像的高度(像素)
     參數8: planeIndex.如果圖像緩沖區是平面的,則為映射紋理數據的平面索引。對於非平面圖像緩沖區忽略。
     參數9: textureOut,返回時,返回創建的Metal紋理緩沖區。
     
     // Mapping a BGRA buffer:
     CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &outTexture);
     
     // Mapping the luma plane of a 420v buffer:
     CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatR8Unorm, width, height, 0, &outTexture);
     
     // Mapping the chroma plane of a 420v buffer as a source texture:
     CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatRG8Unorm width/2, height/2, 1, &outTexture);
     
     // Mapping a yuvs buffer as a source texture (note: yuvs/f and 2vuy are unpacked and resampled -- not colorspace converted)
     CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, NULL, MTLPixelFormatGBGR422, width, height, 1, &outTexture);
     */
    CVMetalTextureRef tmpTexture = NULL;
    CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
    
    // 4.判斷tmpTexture 是否創建成功
    if(status == kCVReturnSuccess) {

        // 5.設置可繪制紋理的當前大小。
        self.mtkView.drawableSize = CGSizeMake(width, height);
        // 6.返回紋理緩沖區的Metal紋理對象。
        self.texture = CVMetalTextureGetTexture(tmpTexture);
        // 7.使用完畢,則釋放tmpTexture
        CFRelease(tmpTexture);
    }
}

#pragma mark - MTKView Delegate -
// 視圖渲染則會調用此方法
- (void)drawInMTKView:(MTKView *)view {
  
    // 1.判斷是否獲取了AVFoundation 采集的紋理數據
    if (self.texture) {
        
        // 2.創建指令緩沖
        id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
        
        // 3.將MTKView 作為目標渲染紋理
        id<MTLTexture> drawingTexture = view.currentDrawable.texture;
        
        // 4.設置濾鏡
        /*
         MetalPerformanceShaders是Metal的一個集成庫,有一些濾鏡處理的Metal實現;
         MPSImageGaussianBlur 高斯模糊處理;
         */
        // 創建高斯濾鏡處理 filter
        // sigma 值越高圖像越模糊
        MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:1];
        
        // 5.MPSImageGaussianBlur 以一個 Metal 紋理作為輸入,以一個Metal 紋理作為輸出;
        // 輸入:攝像頭采集的圖像 self.texture
        // 輸出:創建的紋理 drawingTexture (其實就是view.currentDrawable.texture)
        [filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
        
        // 6.展示顯示的內容
        [commandBuffer presentDrawable:view.currentDrawable];
        
        // 7.提交命令
        [commandBuffer commit];
        
        // 8.清空當前紋理,准備下一次的紋理數據讀取.
        self.texture = NULL;
    }
}
// MTKView - 視圖大小發生改變時.會調用此方法
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {}

二、視頻處理

1、一張圖片的大小

圖片的渲染:jpg/png文件 --> 解壓成位圖 --> 片元着色器  對每個像素處理 --> 渲染顯示

視頻的渲染:視頻文件 --> 解碼(解壓縮) --> 視頻 60幀/s, 60張圖片的處理 --> 片元着色器

舉例:RGB 編碼的一張 1280*720 的圖片(這里忽略透明度A):

  有1280*720個像素點,每個像素點紅綠藍3個原色,每個原色占 8bit(1字節),那么一個像素即 3*8=24bit,即 3字節

  --> so 一張圖片占用存儲空間:1280*720 * 3 /1024/1024 = 2.63MB.

對視頻來講,這種空間占用率太高,消耗太大了。

如何解決此問題?

1)視頻壓縮(編碼) --> H264 格式視頻(視頻壓縮) --> 壓縮比可達到 102:1

2)采集時,降低數據量 --> YUV 格式保存視頻

2、YUV

2.1)YUV是什么

YUV 顏色編碼和 RGB顏色編碼的紅綠藍三原色組合 不同,YUV采用的是 明亮度 和 色度 來指定像素的顏色。

Y:明亮度(Luminance、Luma)

U、V:色度(Chrominance、Chroma) -->

    色度又定義了顏色的:色調飽和度

為什么用 YUV  -->  1、節省帶寬 2、比 RGBA 占用 內存空間 更少

2.2)YUV 采樣格式

和 RGB 表示圖像類似,每個像素點都包含 YUV 三個分量。但它的Y分量和UV分量是可以拆分開的,沒有UV 分量也是可以顯示一張完整圖片的,不過圖片會是沒有色彩的黑白的。

對於 YUV 格式圖像來說,並非每個像素點都包含了完整的 YUV 三個分量,根據不同的采樣格式,每個 Y 分量可以對應自身的 UV 分量,也可以幾個 Y 共用 UV 分量。

a、采樣格式 YUV4:4:4 

YUV4:4:4 采樣,YUV 三個分量的采樣比例相同,在生成的圖像里,每個像素點的三分量信息都是完整的8bit。

采樣碼流 為:Y0 U0 V0 Y1 U2 V1 Y2 U2 V2 Y3 U3 V3

映射還原的像素點:[Y0 U0 V0] [Y1 U2 V1] [Y2 U2 V2] [Y3 U3 V3]

圖片所占空間: 1280*720 * 3 /1024/1024 = 2.63MB,與RGB格式相同。此種采樣格式實際業務中並不會使用。

b、采樣格式 YUV4:2:2

YUV4:2:2 即,UV 分量是 Y 分量的一半,Y 和 UV 按照 2:1 的比例采樣。如果水平方向有10個像素點,那么采樣了 10 個 Y,5個UV。采樣方式如上圖,UV分量隔一個采一個,相當於前后2個像素 每次都借用彼此的V/U分量。

采樣碼流:Y0 U0 Y1 V1 Y2 U2 Y3 V3

映射還原像素點:[Y0 U0 V1] [Y1 U0 V1] [Y2 U2 V3] [Y3 U2 V3]

此時,圖片所占空間: 1280*720 * (8bit + 0.5*8 * 2) / 8  /1024/1024 = 1.76MB。相較RGB模型圖像節省了 1/3 的存儲空間,傳遞中占用帶寬也隨之減少。

c、采樣格式 YUV4:2:0

YUV4:2:0 采樣,在每一行掃描時,只掃一種色度分量(U 或 V),和Y 按 2:1 的方式采樣。比如,第一行 YU 按 2:1 的方式采樣,那么第二行則 YV 按 2:1 的方式采樣。 對於每個色度分量來說,水平和豎直方向上的采樣 和 Y 相比 都是 2:1。此種方式必粗要掃2行才能組成完整的UV分量。如上圖,也可以理解為,4個一組,UV 分量共用。

采樣碼流:Y0 U0 Y1 Y2 U2 Y3

    :   Y4 V4 Y5 Y6 V6 Y7

映射還原像素點:[Y0 U0 V4] [Y1 U0 V4]  [Y2 U2 V6] [Y3 U2 V6]

        [Y4 U0 V4] [Y5 U0 V4]  [Y6 U2 V6] [Y7 U2 V6]

此時,圖片占用空間大小:1280*720 * (8 + 0.25*8 *2) / 8 /1024/1024 = 1.32M,進一步的縮小了占據中間,傳輸時帶寬占用更少。

YUV4:2:0 采樣是目前業務開發中最常用的方式。

視頻捕獲(采集)過程中,只需在片元着色器中,將RGBA轉化成 YUV 即可。

3、YUV 與 RGBA 轉換

對於圖像顯示器來說,是通過 RGB 模型來顯示圖像的,而在傳輸圖像數據時,我們使用的是 YUV 模型,因為 YUV 可以節省帶寬。因此,采集圖像時,我們會將RGB 轉換到YUV,但是,顯示時我們要將 YUV 轉回 RGB

RGB 到 YUV 的轉換,即 將圖像所有的像素點的 RGB 分量轉換到 YUV 分量。

RGB ==> YUV:

Y  = 0.299 * R + 0.587 * G + 0.114 * B

U = -0.147 * R - 0.289 * G + 0.436 * B

V = 0.615 * R - 0.515 * G - 0.100 * B

YUV ==> RGB:

R = Y + 1.14 * V

G = Y - 0.39 * U - 0.58 * V

B = Y + 2.03 * U

以上。

 

YUV 處理后,雖然圖片占用空間已相對縮小,但,仍需要繼續 對視頻壓縮處理,2者結合,進一步優化。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM