Last active
May 3, 2024 06:58
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
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
@allenhumphreys
Thank you very much for sharing this code! Could you please tell me how to resize the pixel buffer with keeping aspect ratio?