CVPixelBuffer的建立資料填充以及資料讀取
CVPixelBuffer 在音視訊編解碼以及影象處理過程中應用廣泛,有時需要讀取內部資料,很少的時候需要自行建立並填充資料,下面簡單敘述。
建立
建立時呼叫的方法主要是這個:
CVReturn CVPixelBufferCreate(CFAllocatorRef allocator,
size_t width,
size_t height,
OSType pixelFormatType,
CFDictionaryRef pixelBufferAttributes,
CVPixelBufferRef _Nullable *pixelBufferOut);
提供必須的引數即可,
XX pixelFormatType 常用的這幾個:
/* NV12 */
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange = '420v',
/* Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]).
baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct */
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange = '420f',
/* Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255]).
baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct */
/* YUV420P */
kCVPixelFormatType_420YpCbCr8Planar = 'y420',
/* Planar Component Y'CbCr 8-bit 4:2:0.
baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct */
因為我想要建立NV12格式的buffer,所以沒有使用那個直接提供資料的建立函式,後續提供。如果資料格式為420P的話,直接指定資料地址也可以。
XX pixelBufferAttributes
這個引數是optinal的,提供所有額外的資訊。Core Video根據提供的引數來建立合適的資料,我看到網上的程式碼往往是這樣提供的:
NSDictionary *pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}};
說明如下:
Provide a value for this key if you want Core Video to use the
IOSurface framework to allocate the pixel buffer.
(See IOSurface.)
Provide an empty dictionary to use default IOSurface options.
資料填充
以 NV12 格式的資料填充舉例說明。
在訪問buffer內部裸資料的地址時(讀或寫都一樣),需要先將其鎖上,用完了再放開,如下:
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
// To touch the address of pixel...
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
Y通道(Luminance)與 UV通道(Chrominance)分開填充資料,而且需要注意後者是UV交錯排列的。在填充資料時還需要考慮到資料對齊的問題,當視訊幀的寬高並不是某個對齊基數的倍數時(比如16),內部具體如何分配記憶體是不確定的,保險的做法就是逐行資料填充。這裡我放上填充Chrominance通道資料的例子:
size_t bytesPerRowChrominance = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
long chrominanceWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
long chrominanceHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
// Chrominance
uint8_t *uvDestPlane = (uint8_t *) CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
memset(uvDestPlane, 0x80, chrominanceHeight * bytesPerRowChrominance);
for (int row = 0; row < chrominanceHeight; ++row) {
memcpy(uvDestPlane + row * bytesPerRowChrominance,
uvDataPtr + row * _outVideoWidth,
_outVideoWidth);
}
free(uvDataPtr);
在逐行copy資料的時候,pixel內部地址每個迴圈步進 current_row * bytesPerRowChrominance
的大小,這是pixelbuffer內部的記憶體排列。然後我的資料來源記憶體排列是緊密排列不考慮記憶體多少位對齊的問題的,所以每次的步進是 current_row * _outVideoWidth
也就是真正的視訊幀的寬度。每次copy的大小也應該是真正的寬度。對於這個通道來說,寬度和高度都是亮度通道的一半,每個元素有UV兩個資訊,所以這個通道每一行佔用空間和亮度通道應該是一樣的。也就是每一行copy資料的大小是這樣算出來的:_outVideoWidth / 2 * 2
.
資料讀取
資料讀取和資料填充正好是相反的操作,操作流程相似,先獲取pixelBuffer的一些具體資訊,判斷資訊無誤後繼續讀取資料。
unsigned long planes = CVPixelBufferGetPlaneCount(pixelRef);
若通道數目錯誤顯然邏輯已經錯誤,無需繼續。同樣是先鎖住BaseAddress,然後獲取其bytesPerRowChrominance等資訊,然後按行讀取資料即可。切記,仍然需要按行讀取資料。
補充:從 Image 建立 PixelBuffer
直接附上可執行的程式碼:
size_t height;
size_t width;
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, width,
height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options,
&pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata, width,
height, 8, 4 * width, rgbColorSpace,
kCGImageAlphaNoneSkipFirst);
NSParameterAssert(context);
CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
CGImageGetHeight(image)), image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
建立格式為 kCVPixelFormatType_32ARGB
的 pixelBuffer,建立一個CGContextRef 物件,並將其內部地址設定為pixelBuffer的內部地址。使用 CGContextDrawImage()
函式將原始圖片的資料繪製到我們建立的context上面,完成。
參考資料:
大神的文章,很詳細:讀寫CVPixelBufferRef
Create CVPixelBuffer from YUV with IOSurface backed
How to convert from YUV to CIImage for iOS
How do I export UIImage array as a movie?