由CMSampleBufferRef创建的UIImage无法在UIImageView中显示?

23

我正在尝试实时显示来自相机的UIImage,但似乎我的UIImageView不能正确地显示图像。这是AVCaptureVideoDataOutputSampleBufferDelegate必须实现的方法。

- (void)captureOutput:(AVCaptureOutput *)captureOutput 
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer 
       fromConnection:(AVCaptureConnection *)connection
{ 
    // Create a UIImage from the sample buffer data
    UIImage *theImage = [self imageFromSampleBuffer:sampleBuffer];
//    NSLog(@"Got an image! %f %f", theImage.size.width, theImage.size.height);
//    NSLog(@"The image view is %@", imageView);
//    UIImage *theImage = [[UIImage alloc] initWithData:[NSData 
//        dataWithContentsOfURL:[NSURL 
//        URLWithString:@"http://farm4.static.flickr.com/3092/2915896504_a88b69c9de.jpg"]]];
    [self.session stopRunning];
    [imageView setImage: theImage];
}

为了先解决简单的问题:

  • 使用UIImagePickerController不是一个选项(最终我们将实际处理图像)
  • 我知道处理程序被调用了(NSLog调用已经执行,我看到了输出)
  • 如果我使用上述注释掉的代码从Web上加载任意图片而不是仅仅向imageView发送setImage:theImage,则可以正确加载该图片(并且第二次NSLog呈现非空对象)。
  • 至少在基本层面上,我从imageFromSampleBuffer:获取的图像是好的,因为NSLog报告的尺寸为360x480,这是我预期的大小。

我正在使用最近发布的苹果AVFoundation片段中的代码,可在此处获取。

特别地,这是我使用的设置AVCaptureSession对象及其相关内容(我很少理解),并从Core Video缓冲区创建UIImage对象的代码(即imageFromSampleBuffer方法)。

最后,如果我尝试使用从imageFromSamplerBuffer返回的UIImage向普通UIView子类发送drawInRect:,应用程序会崩溃,而使用上面的URL中的UIImage则不会崩溃。下面是来自调试器内崩溃的堆栈跟踪(我收到一个EXC_BAD_ACCESS信号):

#0  0x34a977ee in decode_swap ()
#1  0x34a8f80e in decode_data ()
#2  0x34a8f674 in img_decode_read ()
#3  0x34a8a76e in img_interpolate_read ()
#4  0x34a63b46 in img_data_lock ()
#5  0x34a62302 in CGSImageDataLock ()
#6  0x351ab812 in ripc_AcquireImage ()
#7  0x351a8f28 in ripc_DrawImage ()
#8  0x34a620f6 in CGContextDelegateDrawImage ()
#9  0x34a61fb4 in CGContextDrawImage ()
#10 0x321fd0d0 in -[UIImage drawInRect:blendMode:alpha:] ()
#11 0x321fcc38 in -[UIImage drawInRect:] ()

编辑:以下是关于那段代码返回的UIImage的更多信息。

使用这里描述的方法,我可以提取像素并将它们打印出来,在第一眼看起来它们看起来还好(例如,alpha通道中的每个值均为255)。然而,缓冲区大小似乎有些不对。我从那个URL获取的Flickr图像的大小为375x500,其[pixelData length]给出750000 = 375*500*4,这是期望的值。然而,从imageFromSampleBuffer:返回的图像的像素数据大小为691208 = 360*480*4 + 8,因此像素数据中有8个额外的字节。CVPixelBufferGetDataSize本身也返回了这个少了8个字节的值。我曾经想过可能是在内存中分配缓冲区时采用了对齐位置,但是691200是256的倍数,所以这也无法解释。这种大小差异是我能够发现的仅有的两个UIImages之间的区别,并且它可能会引起问题。尽管如此,为缓冲区分配额外的内存不应该导致EXC_BAD_ACCESS违规。

非常感谢任何的帮助,如果您需要更多信息,请告诉我。


我遇到了同样的问题。令人烦恼的是,提供给我们自定义“imageFromSampleBuffer”函数的正是苹果公司本身。 - Daniel Amitay
更奇怪的是,图像内容本身是正确的。 我通过套接字将原始像素推送到另一台机器上,它与以正确格式捕获的摄像机视频帧完全相同。 - Carlos Scheidegger
5个回答

41

我曾经遇到过同样的问题,但是我发现了这篇旧帖子,并且它提供的创建CGImageRef的方法可行!

http://forum.unity3d.com/viewtopic.php?p=300819

下面是一个可行的示例:

app has a member UIImage theImage;

// Delegate routine that is called when a sample buffer was written
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer  fromConnection:(AVCaptureConnection *)connection
{
        //... just an example of how to get an image out of this ...

    CGImageRef cgImage = [self imageFromSampleBuffer:sampleBuffer];
    theImage.image =     [UIImage imageWithCGImage: cgImage ];
    CGImageRelease( cgImage );
}

- (CGImageRef) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer // Create a CGImageRef from sample buffer data
{
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); 
    CVPixelBufferLockBaseAddress(imageBuffer,0);        // Lock the image buffer 

    uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);   // Get information of the image 
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer); 
    size_t width = CVPixelBufferGetWidth(imageBuffer); 
    size_t height = CVPixelBufferGetHeight(imageBuffer); 
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 

    CGContextRef newContext = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); 
    CGImageRef newImage = CGBitmapContextCreateImage(newContext); 
    CGContextRelease(newContext); 

    CGColorSpaceRelease(colorSpace); 
    CVPixelBufferUnlockBaseAddress(imageBuffer,0); 
    /* CVBufferRelease(imageBuffer); */  // do not call this!

    return newImage;
}

1
所以,当涉及到iOS API时,我完全是个白痴,不知道如何将像素推入实际的CGImage中。这似乎可以通过memcpy(mImageData, ...)调用来实现,但是在那段代码片段中mImageData实际上没有声明。你知道我如何使用iOS API获取这样的指针吗? - Carlos Scheidegger
13
因某种原因,我遇到了<Error>: CGBitmapContextCreateImage: invalid context 0x0的问题。有什么想法吗? - Tomas Andrle
@TomA,你找到答案了吗? - onmyway133
这在iPhone5、iPad3和iPad2上运行正常,但在iPhone4(黑白)上不行。我的意思是图片显示不连续。 - Muruganandham K
同样的问题,我也收到了错误提示 "<Error>: CGBitmapContextCreateImage: invalid context 0x0。这是一个严重的错误。该应用程序或其使用的库正在使用无效的上下文,从而对系统稳定性和可靠性造成贡献。这是一种礼貌:请解决此问题。在即将发布的更新中,这将成为致命错误。”大家有什么想法吗? - sudoExclaimationExclaimation
显示剩余8条评论

10

1
这是一个很好的例子。然而,在iPad3上构建图像需要大约两分钟,非常奇怪。 - Oh Danny Boy
2
好的,我承认我说谎了。使用 performSelectorOnMainThread 来设置 UIImageView 的话,可以立即得到结果。 - Oh Danny Boy

7

设定正确的输出格式同样很重要。使用默认的格式设置时,我遇到了图像捕捉的问题。应该将其设置为:

[videoDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA] forKey:(NSString*)kCVPixelBufferPixelFormatTypeKey]];

5

Ben Loulier写了一篇很好的文章,介绍如何实现这个功能。

我使用他的示例应用程序作为起点,并且它对我有效。除了用创建CGBitmapContextCreate的CGImageRef替换imageFromSamplerBuffer函数外,他还在设置输出采样缓冲区委托时使用主调度队列(通过dispatch_get_main_queue())。这不是最好的解决方案,因为它需要一个串行队列,而据我所知,主队列不是一个串行队列。所以虽然你不能保证按正确顺序获得帧,但它似乎对我有效 :)


这个代码可以正常工作,即使没有不同的队列。我现在怀疑苹果在他们的代码片段中调用CGDataProviderCreateWithData,这是代码中唯一剩下的区别。 - Carlos Scheidegger

1
另一件需要注意的事情是,你是否在主线程上更新了UIImageView:如果没有,很可能不会反映任何更改。 captureOutput:didOutputSampleBuffer:fromConnection委托方法通常在后台线程上调用。因此,你需要这样做:
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection
{   
    CFRetain(sampleBuffer);

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{

        //Now we're definitely on the main thread, so update the imageView:
        UIImage *capturedImage = [self imageFromSampleBuffer:sampleBuffer];

        //Display the image currently being captured:
        imageView.image = capturedImage;

        CFRelease(sampleBuffer);
    }];
}

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