iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像

來源:互聯網
上載者:User

標籤:失敗   部分   情況   轉碼   交換   different   成功   reader   函數參數   

iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像本文檔基於H.264的解碼,介紹讀寫Video Toolbox解碼回呼函數參數CVImageBufferRef中的YUV或RGB資料的方法,並給出CVImageBufferRef產生灰階圖代碼、方便調試。同時,還介紹了Video Toolbox解碼回調中進行YUV處理時容易忽略的問題。文檔定位於iOS音視頻進階編程,致力於提供高參考價值的Core Video中文資料,最近也在StackOverflow上關注Core Video相關問題,學習並回饋社區。目錄|- 讀取CVImageBufferRef(CVPixelBufferRef)|- 寫入CVImageBufferRef(CVPixelBufferRef)|- CVPixelBufferPool記憶體池|- CVPixelBuffer通過Core Graphics建立灰階圖|- 坑|-- 直接操作解碼回調的CVImageBuffer(CVPixelBuffer)存在的問題|-- CVPixelBuffer上傳至GPU後映像垂直鏡像問題|- 參考與推薦閱讀在實現全景視頻播放器及其關聯項目過程中,我編寫了以下Video Toolbox相關文檔(因開發工作單位等原因,部分文檔處於草稿狀態,之後會進行內容修訂):iOS VideoToolbox硬編H.265(HEVC)H.264(AVC):1 概述【草稿】iOS VideoToolbox硬編H.265(HEVC)H.264(AVC):2 H264資料寫入檔案iOS VideoToolbox硬編H.265(HEVC)H.264(AVC):4 同步編碼iOS 音視頻進階編程:AVAssetReaderTrackOutput改變CMFormatDescription導致Video Toolbox解碼失敗與不解碼GPU直接顯示H.264幀iOS 音視頻進階編程:AVAsset、CoreVideo、VideoToolbox、FFmpeg與CMTimeVideo Toolbox Multi-pass Encoding擷取VideoToolbox解碼直播等H.264流的色彩轉換矩陣CVPixelBufferRef是CVImageBufferRef的別名,兩者操作幾乎一致。// CVPixelBuffer.h/* * CVPixelBufferRef * Based on the image buffer type. * The pixel buffer implements the memory storage for an image buffer. */typedef CVImageBufferRef CVPixelBufferRef;雖然文法上CVPixelBufferRef是CVImageBufferRef的別名,它們在文檔中的說明卻有區別:Core Video image buffers provides a convenient interface for managing different types of image data. Pixel buffers and Core Video OpenGL buffers derive from the Core Video image buffer.CVImageBufferRef:A reference to a Core Video image buffer. An image buffer is an abstract type representing Core Video buffers that hold images. In Core Video, pixel buffers, OpenGL buffers, and OpenGL textures all derive from the image buffer type.CVPixelBufferRef :A reference to a Core Video pixel buffer object. The pixel buffer stores an image in main memory.從上述可知,CVPixelBuffer『繼承了』CVImageBuffer,然而,由於Core Video暴露出來的是Objective-C介面,意味著若想用C語言實現『物件導向的繼承』,則CVPixelBuffer的資料成員定義位置與CVImageBuffer基本保持一致且令編譯器進行相同的位移以確保位元組對齊,猶如FFmpeg中AVFrame可強制轉換成AVPicture,以FFmpeg 3.0源碼為例。typedef struct AVFrame { uint8_t *data[AV_NUM_DATA_POINTERS]; int linesize[AV_NUM_DATA_POINTERS]; uint8_t **extended_data; // 後續還有眾多欄位}typedef struct AVPicture { ///< pointers to the image data planes uint8_t *data[AV_NUM_DATA_POINTERS]; ///< number of bytes per line int linesize[AV_NUM_DATA_POINTERS]; } AVPicture;當然,從蘋果開源的某些架構上看,Core Video內部極有可能用Objective-C++實現,可能真正用了C++式繼承,在此不作過多猜測。1、讀取CVImageBufferRef(CVPixelBufferRef)在解碼回調中,傳遞過來的幀資料由CVImageBufferRef指向。如果需取出其中像素資料作進一步處理,得訪問其中真正儲存像素的記憶體。VideoToolbox解碼後的映像資料並不能直接給CPU訪問,需先用CVPixelBufferLockBaseAddress()鎖定地址才能從主存訪問,否則調用CVPixelBufferGetBaseAddressOfPlane等函數則返回NULL或無效值。值得注意的是,CVPixelBufferLockBaseAddress自身的調用並不消耗多少效能,一般情況,鎖定之後,往CVPixelBuffer拷貝記憶體才是相對耗時的操作,比如計算記憶體位移。如果CVPixelBuffer的映像需要顯示在螢幕上,建議用GPU實現影像處理操作。下面展示讀寫左半映像時的效能損耗(請忽略記憶體計算的粗暴代碼)。讀取CVPixelBuffer映像的效能消耗寫入CVPixelBuffer映像的效能消耗然而,用CVImageBuffer -> CIImage -> UIImage則無需顯式調用鎖定基地址函數。// CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); // 可以不加CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer];CIContext *temporaryContext = [CIContext contextWithOptions:nil];CGImageRef videoImage = [temporaryContext createCGImage:ciImage fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(imageBuffer), CVPixelBufferGetHeight(imageBuffer))];UIImage *image = [[UIImage alloc] initWithCGImage:videoImage];UIImageView *imageView = [[UIImageView alloc] initWithImage:image];CGImageRelease(videoImage);// CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);CVPixelBufferIsPlanar可得到像素的儲存方式是Planar或Chunky。若是Planar,則通過CVPixelBufferGetPlaneCount擷取YUV Plane數量。通常是兩個Plane,Y為一個Plane,UV由VTDecompressionSessionCreate建立解碼會話時通過destinationImageBufferAttributes指定需要的像素格式(可不同於視頻源像素格式)決定是否同屬一個Plane,每個Plane可當作表格按行列處理,像素是行順序填充的。下面以Planar Buffer儲存方式作說明。CVPixelBufferGetPlaneCount得到像素緩衝區平面數量,然後由CVPixelBufferGetBaseAddressOfPlane(索引)得到相應的通道,一般是Y、U、V通道儲存地址,UV是否分開由解碼會話指定,如前面所述。而CVPixelBufferGetBaseAddress返回的對於Planar Buffer則是指向PlanarComponentInfo結構體的指標,相關定義如下:/* Planar pixel buffers have the following descriptor at their base address. Clients should generally use CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane, etc. instead of accessing it directly. */struct CVPlanarComponentInfo { /* offset from main base address to base address of this plane, big-endian */ int32_t offset; /* bytes per row of this plane, big-endian */ uint32_t rowBytes; };typedef struct CVPlanarComponentInfo CVPlanarComponentInfo;struct CVPlanarPixelBufferInfo { CVPlanarComponentInfo componentInfo[1]; };typedef struct CVPlanarPixelBufferInfo CVPlanarPixelBufferInfo;struct CVPlanarPixelBufferInfo_YCbCrPlanar { CVPlanarComponentInfo componentInfoY; CVPlanarComponentInfo componentInfoCb; CVPlanarComponentInfo componentInfoCr; };typedef struct CVPlanarPixelBufferInfo_YCbCrPlanar CVPlanarPixelBufferInfo_YCbCrPlanar;struct CVPlanarPixelBufferInfo_YCbCrBiPlanar { CVPlanarComponentInfo componentInfoY; CVPlanarComponentInfo componentInfoCbCr; };typedef struct CVPlanarPixelBufferInfo_YCbCrBiPlanar CVPlanarPixelBufferInfo_YCbCrBiPlanar;根據CVPixelBufferGetPixelFormatType得到像素格式,以對應的方式讀取,比如YUV420SP跨距讀取所有的U到一個緩衝區。2、寫入CVImageBufferRef(CVPixelBufferRef)下面代碼展示了以向Y、UV Planar拷貝資料的過程:NSDictionary *pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}}; CVPixelBufferRef pixelBuffer = NULL; CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, (__bridge CFDictionaryRef)pixelAttributes) &pixelBuffer); CVPixelBufferLockBaseAddress(pixelBuffer, 0);uint8_t *yDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);memcpy(yDestPlane, yPlane, width * height);uint8_t *uvDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);// numberOfElementsForChroma為UV寬高乘積memcpy(uvDestPlane, uvPlane, numberOfElementsForChroma); CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);if (result != kCVReturnSuccess) { NSLog(@"Unable to create cvpixelbuffer %d", result); } CIImage *coreImage = [CIImage imageWithCVPixelBuffer:pixelBuffer]; CVPixelBufferRelease(pixelBuffer);上述代碼通過- [CIImage imageWithCVPixelBuffer:]建立CIImage在iPad Air 2、iPhone 6p等真機上存在的問題:1、當使用kCVPixelFormatType_420YpCbCr8PlanarFullRange時提示[CIImage initWithCVPixelBuffer:options:] failed because its pixel format f420 is not supported.,即不支援由YUV420P格式的CVPixelBuffer建立CIImage。經測試,視頻源格式為yuvj420p(pc, bt709),在VTDecompressionSessionCreate不指定destinationImageBufferAttributes的kCVPixelBufferPixelFormatTypeKey值時,Video Toolbox解碼出來的CVImageBufferRef對應為f420。當指定destinationImageBufferAttributes需要kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange時,解碼出來的ImageBuffer為420v,然後建立YUV時指定PixelFormat為f420會出現上述問題。原因是,以420v方式拷貝YUV資料,其儲存布局與f420不同,導致建立CIImage失敗。2、決定CVPixelBufferCreate建立的格式是其參數pixelFormatType,而非參數pixelAttributes使用kCVPixelBufferPixelFormatTypeKey指定的像素格式。下面介紹一些簡單的影像處理辦法。原始灰階圖(一)水平鏡像水平鏡像就是映像繞映像中間垂直線交換左右像素點位置,使用矩陣運行表示為:[x, y, 1] -1 0 0 -> [x‘, y‘, 1] 0 1 0 width 0 1對於CPU而言,矩陣運行通常沒GPU快,因為GPU做2x2、3x3等矩陣運算是硬體加速實現的,很可能就是一條指令處理完,而CPU往往是逐個元素進行計算,因此,目前大家傾向於GPU做矩陣運行。樣本CPU實現代碼如下。for (int line = 0; line < 480; ++line) { for (int col = 0; col < 960; ++col) { dst_buffer[line * 960 + col] = src_buffer[line * 960 + (960 - col)]; } }iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像水平鏡像(二)垂直鏡像垂直鏡像就是映像繞映像中間水平線交換上下像素點位置,使用矩陣運行表示為:[x, y, 1] 1 0 0 -> [x‘, y‘, 1] 0 -1 0 0 height 1樣本CPU實現代碼如下。for (int line = 0; line < 480; ++line) { for (int col = 0; col < 960; ++col) { dst_buffer[(480 - line) * 960 + col] = src_buffer[line * 960 + col]; } }iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像垂直鏡像3、CVPixelBufferPool記憶體池自行建立CVPixelBufferPool且通過CVPixelBufferPool建立CVPixelBuffer,容易出現CVPixelBuffer被錯誤釋放或意外增加引用計數導致記憶體泄露,以ijkplayer為例示範CVPixelBubffer泄露的情況。iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像CVPixelBuffer泄露iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像CVPixelBuffer結束引用時引用計數不為0導致記憶體泄露而自行建立CVPixelBuffer,則容易出現記憶體暴漲問題,如建立一個960x480的YUV420SP格式的CVPixelBuffer所佔記憶體為700多M,如果是非同步解碼且沒作記憶體大小限制,將導致應用崩潰。iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像CVPixelBufferCreate佔用的記憶體如果不想自行建立CVPixelBufferPool,也不想自己建立CVPixelBuffer,取巧的辦法是,使用解碼回呼函數的CVPixelBuffer,則無需擔心記憶體消耗問題。在實踐過程中,影像處理後立即編碼,這樣使用的場合不會導致解碼器自身的緩衝隊列資料出現映像紊亂。前提是,修改後的像素資料在原資料的寬高範圍內。當然,這也會出現些問題,具體在文檔後續部分進行討論對於解碼->影像處理->編碼流程,且處理後的映像與原映像大小不同,則建立編碼器時再建立CVPixelBufferPool,讓系統管理CVPixelBuffer也是可靠的做法。另外,在影像處理過程中,Video Toolbox無論指定FullRange還是VideoRange,由此通過Core Graphics建立RGB映像是正確的,和QuickTime播放時畫面保持一致。然而,解碼出來的YUV420SP資料經過拷貝,接著進行影像處理,存在部分地區顏色有誤。通過指定Video Toolbox輸出YUV420P,再進行影像處理則無顏色異常問題。當然,使用的演算法也改變相應的YUV420P演算法,因為個人認為,這極有可能是我們團隊的YUV420SP拷貝及操作演算法有誤。4、CVPixelBuffer通過Core Graphics建立灰階圖修改完YUV資料後,如果每次都需要GPU實現YUV轉換RGB,這比較麻煩,特別是轉碼等離線計算場合。下面,介紹一種實現CVPixelBuffer產生UIImage的辦法,只使用Y平面產生映像,判斷映像成像方面的處理結果是否符合預期。// baseAddress為Y平面地址,傳遞yuv420(s)p完整資料地址,則忽略uvUIImage* yuv420ToUIImage(void *baseAddress, size_t width, size_t height, size_t bytesPerRow) { // Create a device-dependent gray color space CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); // Create a bitmap graphics context with the sample buffer data CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGImageAlphaNone); // Create a Quartz image from the pixel data in the bitmap graphics context CGImageRef quartzImage = CGBitmapContextCreateImage(context); // Free up the context and color space CGContextRelease(context); CGColorSpaceRelease(colorSpace); // Create an image object from the Quartz image UIImage *image = [UIImage imageWithCGImage:quartzImage]; return image; }上述代碼可能會引起這樣的疑問:灰階圖為何不需要U和V通道的資料。確實,此問題我最近特意查閱了些資料。建立灰階圖時,有些人還將U、V通道在偏置前(值範圍[-128, 127])設定為0,或者偏置後(值範圍[0, 255])設定為128,然而,建立灰階圖時,他們的代碼並未使用UV資料。另外,看到一種說法是:Y通道就是平時所說的灰階通道。當然,以我有限的瞭解來看,個人不太認可這種說法。原因是,Y通道是YUV的一個分量,而灰階是複合量,即使數值接近,在概念上應該也是有區別的。數值接近的意思是,以BT. 601轉換矩陣為例進行證明:Y = 0.299R + 0.587G + 0.114B GrayScale = (R + G + B) / 3可見,Y值在數值接近灰階值。下面,對建立映像的程式碼片段進行簡要分析。一些開源項目,如SDWebImage,它使用CGColorSpaceCreateDeviceRGB函數,是因為它的資料來源是RGB,而我們這裡的YUV資料需要經過色彩轉換矩陣運算才能得到RGB,簡單起見,由CGColorSpaceCreateDeviceGray函數建立灰階圖可直接看到映像發生的變化,缺點是,丟失了顏色資訊。如下所示。iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像產生灰階圖雖然,像素格式為YUV的視頻解碼後幾乎都可產生灰階圖。然而,並不是所有的映像未經處理資料都能通過Core Graphics產生可視映像,iOS支援的像素格式非常有限,如下所示。iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像產生灰階圖支援的像素格式5、坑操作CVImageBuffer(CVPixelBuffer)雖然看著沒什麼難度,然而,還是有些大大小小的問題。如果對此不作描述,那麼本文檔的標題真是太標題黨了。下面,給出我在開發過程中遇到並解決的情況。5.1、直接操作解碼回調的CVImageBuffer(CVPixelBuffer)存在的問題在解碼回呼函數中進行YUV處理,無論是否同步解碼,或者解碼與建立紋理、重新整理介面是否為同一線程。需要注意的是,解碼回調得到的CVPixelBuffer中的映像是上一次解碼回調中處理過的映像,而非視頻壓縮資料通過解碼得到的新的完整映像。換句話說,在一個主要畫面格解碼成功後,其後續P幀以前一幀為基礎,繼續解碼並將結果疊加到新畫面,然後傳遞到解碼回呼函數。簡單示意之。Decode Thread: VTDecompressionSessionDecodeFrame -> VTDecoderCallback (進行影像處理) -> 添加到待顯示隊列 Rendering Thread: 讀取待顯示隊列、得到已處理的CVPixelBuffer -> CVOpenGLESTextureCacheCreateTextureFromImage下面,詳細討論上述情況。進行YUV三個通道處理後,播放出來的畫面看著正常,相關資源佔用資訊如下所示。然而,經輸出Video Toolbox回呼函數傳遞過來的CVPixelBuffer或說CVImageBuffer,發現是之前我們處理過的映像,並在上一主要畫面格基礎上持續疊加P幀,把結果映像作為下一幀視頻。iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像CPU不超負荷的資源佔用CPU不超負荷的GPU佔用CPU不超負荷的Y通道圖CPU不超負荷的解碼回調每幀映像可見,作為一個主要畫面格間隔為15的視頻序列,src_1.jpg與src_16.jpg因主要畫面格得到一次立即重新整理,隨後的映像都在YUV處理的基礎上持續疊加。5.2、CVPixelBuffer上傳至GPU後映像垂直鏡像問題對於CMVideoFormatDescription及指定輸出的CVPixelBuffer資訊如下的解碼過程,在自行建立CVPixelBuffer後,將解碼回呼函數的CVPixelBuffer資料拷貝到新CVPixelBuffer,通常會遇到映像顛倒了,確切地說,映像出現垂直鏡像問題。不過,使用前面產生灰階圖函數得到的映像都是正的,不存在顛倒,只有上傳到GPU裡才存在此現象。原因是,電腦的映像儲存時有自己的座標,這個座標與OpenGL ES的紋理座標的Y軸正好相反,故映像在GPU中是顛倒的。CMVideoFormatDescription { CVFieldCount = 1; CVImageBufferChromaLocationBottomField = Left; CVImageBufferChromaLocationTopField = Left; FullRangeVideo = 0; SampleDescriptionExtensionAtoms = { avcC = <01640033 ffe10014 67640033 ac1b4583 c0f68400 000fa000 03a98010 01000468 e923cbfd f8f800>; }; } destinationImageBufferAttributes = { OpenGLESCompatibility = 1; PixelFormatType = 2033463856; }現在,嘗試使用Core Video介面處理此問題。首先,判斷源及靶心圖表像是否翻轉。bool isFlipped = CVImageBufferIsFlipped(pixelBuffer);if (isFlipped) { NSLog(@"pixelBuffer is %s", isFlipped ? "flipped" : "not flipped"); } isFlipped = CVImageBufferIsFlipped(imageBuffer);if (isFlipped) { NSLog(@"imageBuffer is %s", isFlipped ? "flipped" : "not flipped"); }發現映像都是翻轉的,執行結果所下。pixelBuffer is flipped imageBuffer is flipped顯然,還需要更多資訊去判斷。再擷取兩個緩衝區的ShouldNotPropagate屬性,發現都沒有值。但是,回呼函數的像素緩衝區有ShouldPropagate屬性,而我們自行建立的緩衝區則無此屬性,如下所示。CVFieldCount = 1; CVImageBufferChromaLocationBottomField = Left; CVImageBufferChromaLocationTopField = Left; CVImageBufferColorPrimaries = "SMPTE_C"; CVImageBufferTransferFunction = "ITU_R_709_2"; CVImageBufferYCbCrMatrix = "ITU_R_601_4"; ColorInfoGuessedBy = VideoToolbox;那麼,根據H.264文檔,CVFieldCount只是說明CVPixelBuffer只有一個訪問單元(Access Unit),而BottomField和TopField兩個域表達了映像緩衝區兩個色度的位置,與映像倒轉無關。其餘參數,如YCbCrMatrix只是源視頻需要的YUV轉RGB矩陣。所以,根據我對Core Video的瞭解,目前使用Core Video介面無法處理此情況,只能在GPU中通過鏡像紋理座標或者使用前面介紹的垂直鏡像方式解決

iOS面向編碼|iOSVideoToolbox:讀寫解碼回呼函數CVImageBufferRef的YUV映像

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.