Skip to content

Instantly share code, notes, and snippets.

@allenhumphreys
Last active May 3, 2024 06:58
Show Gist options
  • Save allenhumphreys/bada805df947d1c95e69850068192c55 to your computer and use it in GitHub Desktop.
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

@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

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