如何在Objective-C和iOS中缩放/调整CVPixelBufferRef大小

16

我试图将一个CVPixelBufferRef中的图像调整为299x299大小,并且最好还能裁剪图像。原始的像素缓冲区是640x320,目标是在不失去纵横比的情况下缩放/裁剪到299x299(居中裁剪)。

我发现了用objective c调整UIImage大小的代码,但没有发现调整CVPixelBufferRef大小的代码。我找到了许多针对不同图像类型的复杂的object C示例,但没有专门针对调整CVPixelBufferRef大小的代码。

请问最简单/最好的方法是什么,包括精确的代码。

...我尝试了selton的答案,但这并不起作用,因为在缩放缓冲区中生成的类型是不正确的(进入断言代码)。

OSType sourcePixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
  int doReverseChannels;
  if (kCVPixelFormatType_32ARGB == sourcePixelFormat) {
    doReverseChannels = 1;
  } else if (kCVPixelFormatType_32BGRA == sourcePixelFormat) {
    doReverseChannels = 0;
  } else {
    assert(false);  // Unknown source format
  }

不清楚为什么您发布了if-else语句。如果您包含更多关于如何尝试selton的答案的信息,他也许可以更好地帮助您。紧接着的代码是像素格式要求是什么? - allenh
像素缓冲区的原始格式是kCVPixelFormatType_32ARGB,新的缩放缓冲区也必须是kCVPixelFormatType_32ARGB。Selton代码正在更改格式,从而触发assert(false)异常。如何保持相同的格式或成为kCVPixelFormatType_32ARGB? - James
谢谢@James,我发了一个答案。希望能有所帮助。 - allenh
4个回答

22

借鉴 CoreMLHelpers 的灵感,我们可以创建一个C函数来完成你的需求。根据你的像素格式要求,我认为这个解决方案将是最高效的选项。 我在测试中使用了AVCaptureVideoDataOutput

希望这能有所帮助!

AVCaptureVideoDataOutputSampleBufferDelegate 实现 。这里的大部分工作是创建一个居中裁剪矩形。利用AVMakeRectWithAspectRatioInsideRect 是关键(它正好做你想做的事情)。

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; {

    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    if (pixelBuffer == NULL) { return; }

    size_t height = CVPixelBufferGetHeight(pixelBuffer);
    size_t width = CVPixelBufferGetWidth(pixelBuffer);

    CGRect videoRect = CGRectMake(0, 0, width, height);
    CGSize scaledSize = CGSizeMake(299, 299);

    // Create a rectangle that meets the output size's aspect ratio, centered in the original video frame
    CGRect centerCroppingRect = AVMakeRectWithAspectRatioInsideRect(scaledSize, videoRect);

    CVPixelBufferRef croppedAndScaled = createCroppedPixelBuffer(pixelBuffer, centerCroppingRect, scaledSize);

    // Do other things here
    // For example
    CIImage *image = [CIImage imageWithCVImageBuffer:croppedAndScaled];
    // End example

    CVPixelBufferRelease(croppedAndScaled);
}

方法1:数据操作和加速

此函数的基本原理是先将图像裁剪到指定的矩形,然后缩放到最终需要的大小。裁剪是通过简单地忽略矩形外的数据来实现的。缩放是通过加速的vImageScale_ARGB8888函数实现的。再次感谢CoreMLHelpers提供的见解。

void assertCropAndScaleValid(CVPixelBufferRef pixelBuffer, CGRect cropRect, CGSize scaleSize) {
    CGFloat originalWidth = (CGFloat)CVPixelBufferGetWidth(pixelBuffer);
    CGFloat originalHeight = (CGFloat)CVPixelBufferGetHeight(pixelBuffer);

    assert(CGRectContainsRect(CGRectMake(0, 0, originalWidth, originalHeight), cropRect));
    assert(scaleSize.width > 0 && scaleSize.height > 0);
}

void pixelBufferReleaseCallBack(void *releaseRefCon, const void *baseAddress) {
    if (baseAddress != NULL) {
        free((void *)baseAddress);
    }
}

// Returns a CVPixelBufferRef with +1 retain count
CVPixelBufferRef createCroppedPixelBuffer(CVPixelBufferRef sourcePixelBuffer, CGRect croppingRect, CGSize scaledSize) {

    OSType inputPixelFormat = CVPixelBufferGetPixelFormatType(sourcePixelBuffer);
    assert(inputPixelFormat == kCVPixelFormatType_32BGRA
           || inputPixelFormat == kCVPixelFormatType_32ABGR
           || inputPixelFormat == kCVPixelFormatType_32ARGB
           || inputPixelFormat == kCVPixelFormatType_32RGBA);

    assertCropAndScaleValid(sourcePixelBuffer, croppingRect, scaledSize);

    if (CVPixelBufferLockBaseAddress(sourcePixelBuffer, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) {
        NSLog(@"Could not lock base address");
        return nil;
    }

    void *sourceData = CVPixelBufferGetBaseAddress(sourcePixelBuffer);
    if (sourceData == NULL) {
        NSLog(@"Error: could not get pixel buffer base address");
        CVPixelBufferUnlockBaseAddress(sourcePixelBuffer, kCVPixelBufferLock_ReadOnly);
        return nil;
    }

    size_t sourceBytesPerRow = CVPixelBufferGetBytesPerRow(sourcePixelBuffer);
    size_t offset = CGRectGetMinY(croppingRect) * sourceBytesPerRow + CGRectGetMinX(croppingRect) * 4;

    vImage_Buffer croppedvImageBuffer = {
        .data = ((char *)sourceData) + offset,
        .height = (vImagePixelCount)CGRectGetHeight(croppingRect),
        .width = (vImagePixelCount)CGRectGetWidth(croppingRect),
        .rowBytes = sourceBytesPerRow
    };

    size_t scaledBytesPerRow = scaledSize.width * 4;
    void *scaledData = malloc(scaledSize.height * scaledBytesPerRow);
    if (scaledData == NULL) {
        NSLog(@"Error: out of memory");
        CVPixelBufferUnlockBaseAddress(sourcePixelBuffer, kCVPixelBufferLock_ReadOnly);
        return nil;
    }

    vImage_Buffer scaledvImageBuffer = {
        .data = scaledData,
        .height = (vImagePixelCount)scaledSize.height,
        .width = (vImagePixelCount)scaledSize.width,
        .rowBytes = scaledBytesPerRow
    };

    /* The ARGB8888, ARGB16U, ARGB16S and ARGBFFFF functions work equally well on
     * other channel orderings of 4-channel images, such as RGBA or BGRA.*/
    vImage_Error error = vImageScale_ARGB8888(&croppedvImageBuffer, &scaledvImageBuffer, nil, 0);
    CVPixelBufferUnlockBaseAddress(sourcePixelBuffer, kCVPixelBufferLock_ReadOnly);

    if (error != kvImageNoError) {
        NSLog(@"Error: %ld", error);
        free(scaledData);
        return nil;
    }

    OSType pixelFormat = CVPixelBufferGetPixelFormatType(sourcePixelBuffer);
    CVPixelBufferRef outputPixelBuffer = NULL;
    CVReturn status = CVPixelBufferCreateWithBytes(nil, scaledSize.width, scaledSize.height, pixelFormat, scaledData, scaledBytesPerRow, pixelBufferReleaseCallBack, nil, nil, &outputPixelBuffer);

    if (status != kCVReturnSuccess) {
        NSLog(@"Error: could not create new pixel buffer");
        free(scaledData);
        return nil;
    }

    return outputPixelBuffer;
}

方法二:CoreImage

这种方法更容易理解,并且具有传递的像素缓冲区格式相当不可知性的优点,对于某些使用情况来说是一个加分项。但是需要注意,您只能使用CoreImage支持的格式。

CVPixelBufferRef createCroppedPixelBufferCoreImage(CVPixelBufferRef pixelBuffer,
                                                   CGRect cropRect,
                                                   CGSize scaleSize,
                                                   CIContext *context) {

    assertCropAndScaleValid(pixelBuffer, cropRect, scaleSize);

    CIImage *image = [CIImage imageWithCVImageBuffer:pixelBuffer];
    image = [image imageByCroppingToRect:cropRect];

    CGFloat scaleX = scaleSize.width / CGRectGetWidth(image.extent);
    CGFloat scaleY = scaleSize.height / CGRectGetHeight(image.extent);

    image = [image imageByApplyingTransform:CGAffineTransformMakeScale(scaleX, scaleY)];

    // Due to the way [CIContext:render:toCVPixelBuffer] works, we need to translate the image so the cropped section is at the origin
    image = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-image.extent.origin.x, -image.extent.origin.y)];

    CVPixelBufferRef output = NULL;

    CVPixelBufferCreate(nil,
                        CGRectGetWidth(image.extent),
                        CGRectGetHeight(image.extent),
                        CVPixelBufferGetPixelFormatType(pixelBuffer),
                        nil,
                        &output);

    if (output != NULL) {
        [context render:image toCVPixelBuffer:output];
    }

    return output;
}

在调用处可以创建CIContext,也可以创建并存储在属性中。有关选项的信息,请参见文档

// Create a CIContext using default settings, this will
// typically use the GPU and Metal by default if supported
if (self.context == nil) {
    self.context = [CIContext context];
}

#1的代码在Xcode中无法编译,".data = sourceData + offset"会出现"对void指针进行算术运算"的错误,同时".width = scaledSize.width"会出现"类型'CGFloat'(又名'double')无法缩小为'vImagePixelCount'"的错误。 - James
另外对于问题#2,如何创建CIContext *context。 - James
@James 我更新了我的答案,希望能解决你的两个问题。关于编译错误,看起来你启用了更严格的编译器警告或处于 C++ 或 Obj-C++ 上下文中。 - allenh

4
    func assertCropAndScaleValid(_ pixelBuffer: CVPixelBuffer, _ cropRect: CGRect, _ scaleSize: CGSize) {
        let originalWidth: CGFloat = CGFloat(CVPixelBufferGetWidth(pixelBuffer))
        let originalHeight: CGFloat = CGFloat(CVPixelBufferGetHeight(pixelBuffer))

        assert(CGRect(x: 0, y: 0, width: originalWidth, height: originalHeight).contains(cropRect))
        assert(scaleSize.width > 0 && scaleSize.height > 0)
    }

    func createCroppedPixelBufferCoreImage(pixelBuffer: CVPixelBuffer,
                                           cropRect: CGRect,
                                           scaleSize: CGSize,
                                           context: inout CIContext
    ) -> CVPixelBuffer {
        assertCropAndScaleValid(pixelBuffer, cropRect, scaleSize)
        var image = CIImage(cvImageBuffer: pixelBuffer)
        image = image.cropped(to: cropRect)

        let scaleX = scaleSize.width / image.extent.width
        let scaleY = scaleSize.height / image.extent.height

        image = image.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
        image = image.transformed(by: CGAffineTransform(translationX: -image.extent.origin.x, y: -image.extent.origin.y))

        var output: CVPixelBuffer? = nil

        CVPixelBufferCreate(nil, Int(image.extent.width), Int(image.extent.height), CVPixelBufferGetPixelFormatType(pixelBuffer), nil, &output)

        if output != nil {
            context.render(image, to: output!)
        } else {
            fatalError("Error")
        }
        return output!
    }

@allenh 的答案的Swift版本


如果我不想指定上下文怎么办?我对Swift还很陌生... - sickerin

0

步骤一

通过使用[CIImage imageWithCVPixelBuffer:将CVPixelBuffer转换为UIImage,然后使用标准方法将该CIImage转换为CGImage,再将该CGImage转换为UIImage。

CIImage *ciimage = [CIImage imageWithCVPixelBuffer:pixelBuffer];

CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgimage = [context
                   createCGImage:ciimage
                   fromRect:CGRectMake(0, 0, 
                          CVPixelBufferGetWidth(pixelBuffer),
                          CVPixelBufferGetHeight(pixelBuffer))];

UIImage *uiimage = [UIImage imageWithCGImage:cgimage];
CGImageRelease(cgimage);

步骤二

通过将图像放置在UIImageView中,将其缩放到所需的大小/裁剪。

UIImageView *imageView = [[UIImageView alloc] initWithFrame:/*CGRect with new dimensions*/];
imageView.contentMode = /*UIViewContentMode with desired scaling/clipping style*/;
imageView.image = uiimage;

步骤三

使用类似以下代码的方式对所述imageView的CALayer进行快照:

#define snapshotOfView(__view) (\
(^UIImage *(void) {\
CGRect __rect = [__view bounds];\
UIGraphicsBeginImageContextWithOptions(__rect.size, /*(BOOL)Opaque*/, /*(float)scaleResolution*/);\
CGContextRef __context = UIGraphicsGetCurrentContext();\
[__view.layer renderInContext:__context];\
UIImage *__image = UIGraphicsGetImageFromCurrentImageContext();\
UIGraphicsEndImageContext();\
return __image;\
})()\
)

正在使用中:

uiimage = snapshotOfView(imageView);

步骤4

使用类似于此方法https://dev59.com/K2865IYBdhLWcg3wZNrT#34990820将裁剪/缩放后的UIImage快照图像转换回CVPixelBuffer。

也就是说,

- (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image
{
    NSDictionary *options = @{
                              (NSString*)kCVPixelBufferCGImageCompatibilityKey : @YES,
                              (NSString*)kCVPixelBufferCGBitmapContextCompatibilityKey : @YES,
                              };

    CVPixelBufferRef pxbuffer = NULL;
    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, CGImageGetWidth(image),
                        CGImageGetHeight(image), kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options,
                        &pxbuffer);
    if (status!=kCVReturnSuccess) {
        NSLog(@"Operation failed");
    }
    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);

    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);

    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(pxdata, CGImageGetWidth(image),
                                                 CGImageGetHeight(image), 8, 4*CGImageGetWidth(image), rgbColorSpace,
                                                 kCGImageAlphaNoneSkipFirst);
    NSParameterAssert(context);

    CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
    CGAffineTransform flipVertical = CGAffineTransformMake( 1, 0, 0, -1, 0, CGImageGetHeight(image) );
    CGContextConcatCTM(context, flipVertical);
    CGAffineTransform flipHorizontal = CGAffineTransformMake( -1.0, 0.0, 0.0, 1.0, CGImageGetWidth(image), 0.0 );
    CGContextConcatCTM(context, flipHorizontal);

    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
                                           CGImageGetHeight(image)), image);
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);

    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
    return pxbuffer;
}

正在使用中:

pixelBuffer = [self pixelBufferFromCGImage:uiimage];

需要创建CIImage、CGImage、UIImage和UIImageView来缩放CVPixelBuffer吗? - James
@James 我不确定是否必须,但这可能是最简单的方法。我不知道有没有内置的方法可以直接缩放它,但重新创建一个新尺寸的图像也很简单。 - Albert Renshaw
@James 包含所有必要的代码。请确保包括所有正确的框架,如CoreGraphics、CoreImage等。 - Albert Renshaw
这种方法有几个缺点。1.浪费了相当多的CPU时间。2.浪费了相当多的内存带宽。3.需要使用主线程(UIImageView)。4.按照步骤进行导致图像混乱。 - allenh
@AllenHumphreys 1和2我们在谈论千分之一秒和额外的一兆字节左右的RAM,只持续了一瞬间。3 这都可以通过GCD异步运行。4 我的图片看起来很好,请验证一下所有输入是否正确,因为所有使用的方法都是标准的,所以没有理由图片会出现“乱码”。 - Albert Renshaw

-1
你可以考虑使用 CIImage:
CIImage *image = [CIImage imageWithCVPixelBuffer:pxbuffer];
CIImage *scaledImage = [image imageByApplyingTransform:(CGAffineTransformMakeScale(0.1, 0.1))];
CVPixelBufferRef scaledBuf = [scaledImage pixelBuffer];

你应该调整比例以适应你的目标尺寸。


1
这个不起作用。在缩放的缓冲区中,类型似乎是错误的, OSType sourcePixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer); int doReverseChannels; if (kCVPixelFormatType_32ARGB == sourcePixelFormat) { doReverseChannels = 1; } else if (kCVPixelFormatType_32BGRA == sourcePixelFormat) { doReverseChannels = 0; } else { assert(false); // 未知的源格式 } - James
1
[scaledImage pixelBuffer] 返回 nil,而 CVPixelBufferGetPixelFormatType(nil) 返回 0。 - allenh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接