Skip to content

Instantly share code, notes, and snippets.

@allenhumphreys
Last active May 3, 2024 06:58
Show Gist options
  • Select an option

  • Save allenhumphreys/bada805df947d1c95e69850068192c55 to your computer and use it in GitHub Desktop.

Select an option

Save allenhumphreys/bada805df947d1c95e69850068192c55 to your computer and use it in GitHub Desktop.
Functions in Swift/C to Crop and Scale CVPixelBuffers in the BiPlanar formats
CVPixelBufferRef createCroppedPixelBufferBiPlanar(CVPixelBufferRef srcPixelBuffer, CGRect croppingRect, CGSize scaleSize) {
OSType inputPixelFormat = CVPixelBufferGetPixelFormatType(srcPixelBuffer);
assert(inputPixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
|| inputPixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange);
size_t inputWidth = CVPixelBufferGetWidth(srcPixelBuffer);
size_t inputHeight = CVPixelBufferGetHeight(srcPixelBuffer);
assert(CGRectGetMinX(croppingRect) >= 0);
assert(CGRectGetMinY(croppingRect) >= 0);
assert(CGRectGetMaxX(croppingRect) <= inputWidth);
assert(CGRectGetMaxY(croppingRect) <= inputHeight);
int cropX = CGRectGetMinX(croppingRect);
int cropY = CGRectGetMinY(croppingRect);
int cropWidth = CGRectGetWidth(croppingRect);
int cropHeight = CGRectGetHeight(croppingRect);
if (CVPixelBufferLockBaseAddress(srcPixelBuffer, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) {
NSLog(@"Could not lock base address");
return nil;
}
// Do Luminance Plane Setup
void *sourceLuminanceData = CVPixelBufferGetBaseAddressOfPlane(srcPixelBuffer, 0);
if (sourceLuminanceData == NULL) {
NSLog(@"Error: could not get pixel buffer base address");
return nil;
}
size_t bytesPerRowLuminance = CVPixelBufferGetBytesPerRowOfPlane(srcPixelBuffer, 0);
size_t bytesPerPixelLuminance = 1; // bytesPerRowLuminance / widthLuminance
size_t croppingOffsetLuminance = cropY * bytesPerRowLuminance + cropX * bytesPerPixelLuminance;
vImage_Buffer sourceBufferLuminance = {
.data = sourceLuminanceData + croppingOffsetLuminance,
.height = (vImagePixelCount)cropHeight,
.width = (vImagePixelCount)cropWidth,
.rowBytes = bytesPerRowLuminance
};
size_t destinationBytesPerRowLuminance = scaleSize.width * bytesPerPixelLuminance;
size_t outputBufferSizeLuminance = scaleSize.height * destinationBytesPerRowLuminance;
// End Luminance Plane Setup
// Do CbCr Plane Setup
void *sourceCbCrData = CVPixelBufferGetBaseAddressOfPlane(srcPixelBuffer, 1);
if (sourceCbCrData == NULL) {
NSLog(@"Error: could not get pixel buffer base address");
return nil;
}
size_t bytesPerRowCbCr = CVPixelBufferGetBytesPerRowOfPlane(srcPixelBuffer, 1);
size_t bytesPerPixelCbCr = 2; // bytesPerRowCbCr / widthCbCr
size_t croppingOffsetCbCr = cropY / 2 * bytesPerRowCbCr + cropX / 2 * bytesPerPixelCbCr;
vImage_Buffer sourceBufferCbCr = {
.data = sourceCbCrData + croppingOffsetCbCr,
.height = cropHeight / 2,
.width = cropWidth / 2,
.rowBytes = bytesPerRowCbCr
};
size_t destinationBytesPerRowCbCr = scaleSize.width / 2 * bytesPerPixelCbCr;
size_t outputBufferSizeCbCr = scaleSize.height / 2 * destinationBytesPerRowCbCr;
// End CbCr Plance Setup
size_t outputBufferSize = outputBufferSizeLuminance + outputBufferSizeCbCr;
void *finalPixelData = malloc(outputBufferSize);
if (finalPixelData == NULL) {
NSLog(@"Error: out of memory");
return nil;
}
// Do Luminace Plane Crop and Scale
vImage_Buffer luminanceDestBuffer = {
.data = finalPixelData,
.height = scaleSize.height,
.width = scaleSize.width,
.rowBytes = destinationBytesPerRowLuminance
};
vImage_Error error = vImageScale_Planar8(&sourceBufferLuminance, &luminanceDestBuffer, nil, 0);
if (error != kvImageNoError) {
NSLog(@"Error: %ld", error);
free(finalPixelData);
return nil;
}
// End Luminance Plane Crop and Scale
// Do CbCr Plane Crop and Scale
void *pixelDataCbCr = finalPixelData + outputBufferSizeLuminance;
vImage_Buffer cbCrDestBuffer = {
.data = pixelDataCbCr,
.height = scaleSize.height / 2,
.width = scaleSize.width / 2,
.rowBytes = destinationBytesPerRowCbCr
};
vImage_Error errorCbCr = vImageScale_CbCr8(&sourceBufferCbCr, &cbCrDestBuffer, nil, 0);
if (errorCbCr != kvImageNoError) {
NSLog(@"Error: %ld", errorCbCr);
free(finalPixelData);
return nil;
}
// End CbCr Plane
OSType pixelFormat = CVPixelBufferGetPixelFormatType(srcPixelBuffer);
CVPixelBufferRef outputPixelBuffer;
size_t planeWidths[2] = {scaleSize.width, scaleSize.width/2};
size_t planeHeights[2] = {scaleSize.height, scaleSize.height/2};
size_t bytesPerRows[2] = {destinationBytesPerRowLuminance, destinationBytesPerRowCbCr};
void *baseAddresses[2] = {finalPixelData, finalPixelData + outputBufferSizeLuminance};
OSStatus status = CVPixelBufferCreateWithPlanarBytes(nil, // 1
scaleSize.width, // 2
scaleSize.height, // 3
pixelFormat, // 4
finalPixelData, // 5
outputBufferSize, // 6
2, // 7
baseAddresses, // 8
planeWidths, // 9
planeHeights, // 10
bytesPerRows, // 11
pixelBufferReleaseCallBackBiPlanar, // 12
nil, // 13
nil, // 14
&outputPixelBuffer // 16
);
if (status != kCVReturnSuccess) {
NSLog(@"Error: could not create new pixel buffer");
free(finalPixelData);
return nil;
}
return outputPixelBuffer;
}
// Works with BiPlanar types kCVPixelFormatType_420YpCbCr8BiPlanarFullRange and kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
// Planar video would only be a matter of repeating the steps for each plane as the Cb and Cr planes would be separate. But the iOS
// camera can't output planar video so I have no way to test that
// Based on https://github.com/hollance/CoreMLHelpers/blob/master/CoreMLHelpers/CVPixelBuffer%2BHelpers.swift#L45
public func resizePixelBufferBiPlanar(_ srcPixelBuffer: CVPixelBuffer,
cropRect: CGRect,
scaleWidth: Int,
scaleHeight: Int) -> CVPixelBuffer? {
let cropX = Int(cropRect.minX)
let cropY = Int(cropRect.minY)
let cropHeight = Int(cropRect.height)
let cropWidth = Int(cropRect.width)
assert(CVPixelBufferGetPlaneCount(srcPixelBuffer) == 2)
guard CVPixelBufferLockBaseAddress(srcPixelBuffer, [.readOnly]) == kCVReturnSuccess else { return nil }
defer { CVPixelBufferUnlockBaseAddress(srcPixelBuffer, [.readOnly]) }
// Do Luminance Plane Setup
guard let sourceLuminanceData = CVPixelBufferGetBaseAddressOfPlane(srcPixelBuffer, 0) else {
print("Error: could not get pixel buffer base address")
return nil
}
let bytesPerRowLuminance = CVPixelBufferGetBytesPerRowOfPlane(srcPixelBuffer, 0)
let bytesPerPixelLuminance = 1 // bytesPerRowLuminance / widthLuminance
let croppingOffsetLuminance = cropY * bytesPerRowLuminance + cropX * bytesPerPixelLuminance
var sourceBufferLuminance = vImage_Buffer(data: sourceLuminanceData.advanced(by: croppingOffsetLuminance),
height: vImagePixelCount(cropHeight),
width: vImagePixelCount(cropWidth),
rowBytes: bytesPerRowLuminance)
let destinationBytesPerRowLuminance = scaleWidth * bytesPerPixelLuminance
let outputBufferSizeLuminance = scaleHeight * destinationBytesPerRowLuminance
// End Luminance Plane Setup
// Do CbCr Plane Setup
guard let sourceCbCrData = CVPixelBufferGetBaseAddressOfPlane(srcPixelBuffer, 1) else {
print("Error: could not get pixel buffer base address")
return nil
}
let bytesPerRowCbCr = CVPixelBufferGetBytesPerRowOfPlane(srcPixelBuffer, 1)
let bytesPerPixelCbCr = 2 // bytesPerRowCbCr / widthCbCr
let croppingOffsetCbCr = cropY / 2 * bytesPerRowCbCr + cropX / 2 * bytesPerPixelCbCr
var sourceBufferCbCr = vImage_Buffer(data: sourceCbCrData.advanced(by: croppingOffsetCbCr),
height: vImagePixelCount(cropHeight / 2),
width: vImagePixelCount(cropWidth / 2),
rowBytes: bytesPerRowCbCr)
let destinationBytesPerRowCbCr = scaleWidth / 2 * bytesPerPixelCbCr
let outputBufferSizeCbCr = scaleHeight / 2 * destinationBytesPerRowCbCr
// End CbCr Plance Setup
let outputBufferSize = outputBufferSizeLuminance + outputBufferSizeCbCr
guard let finalPixelData = malloc(outputBufferSize) else {
print("Error: out of memory")
return nil
}
// Do Luminace Plane Crop and Scale
var luminanceDestBuffer = vImage_Buffer(data: finalPixelData,
height: vImagePixelCount(scaleHeight),
width: vImagePixelCount(scaleWidth),
rowBytes: destinationBytesPerRowLuminance)
let error = vImageScale_Planar8(&sourceBufferLuminance, &luminanceDestBuffer, nil, vImage_Flags(0))
if error != kvImageNoError {
print("Error:", error)
free(finalPixelData)
return nil
}
// End Luminance Plane Crop and Scale
// Do CbCr Plane Crop and Scale
let pixelDataCbCr = finalPixelData.advanced(by: outputBufferSizeLuminance)
var cbCrDestBuffer = vImage_Buffer(data: pixelDataCbCr,
height: vImagePixelCount(scaleHeight / 2),
width: vImagePixelCount(scaleWidth / 2),
rowBytes: destinationBytesPerRowCbCr)
let errorCbCr = vImageScale_CbCr8(&sourceBufferCbCr, &cbCrDestBuffer, nil, vImage_Flags(0))
if errorCbCr != kvImageNoError {
print("Error:", errorCbCr)
free(finalPixelData)
return nil
}
// End CbCr Plane
let releaseCallbackBiPlanar: CVPixelBufferReleasePlanarBytesCallback = { releaseRefCon, dataPtr, dataSize, numberOfPlanes, planeAddresses in
if let dataPtr = dataPtr {
free(UnsafeMutableRawPointer(mutating: dataPtr))
}
}
let pixelFormat = CVPixelBufferGetPixelFormatType(srcPixelBuffer)
var outputPixelBuffer: CVPixelBuffer?
var planeWidths = [scaleWidth, scaleWidth/2]
var planeHeights = [scaleWidth, scaleWidth/2]
var bytesPerRows = [destinationBytesPerRowLuminance, destinationBytesPerRowCbCr]
var baseAddresses: [UnsafeMutableRawPointer?] = [finalPixelData, finalPixelData.advanced(by: outputBufferSizeLuminance)]
let status = CVPixelBufferCreateWithPlanarBytes(
nil, // 1
scaleWidth, // 2
scaleHeight, // 3
pixelFormat, // 4
finalPixelData, // 5
outputBufferSize, // 6
2, // 7
&baseAddresses, // 8
&planeWidths, // 9
&planeHeights, // 10
&bytesPerRows, // 11
releaseCallbackBiPlanar, // 12
nil, // 13
nil, // 14
&outputPixelBuffer // 16
)
if status != kCVReturnSuccess {
print("Error: could not create new pixel buffer")
free(finalPixelData)
return nil
}
return outputPixelBuffer
}
@ViktoriiaKh

Copy link
Copy Markdown

@allenhumphreys
Thank you very much for sharing this code! Could you please tell me how to resize the pixel buffer with keeping aspect ratio?

@soundflix

Copy link
Copy Markdown

This is great!
Need to import Cocoa and Accelerate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment