5

長年のstackoverflowリーダー、初めてのポスター。

CloudWriterという iPad アプリを作成しようとしています。アプリのコンセプトは、雲の中に見える形を描くことです。アプリをダウンロードした後、CloudWriterを起動すると、ユーザーには (背面カメラからの) ライブ ビデオの背景が表示され、その上に OpenGL 描画レイヤーが表示されます。ユーザーはアプリを開き、空の雲に iPad を向けて、ディスプレイに表示されているものを描くことができます。

このアプリケーションの主な機能は、ユーザーがセッション中にディスプレイ上で起こっていることのビデオ スクリーン キャプチャを記録することです。ライブ ビデオ フィードと「描画」ビューは、フラットな (マージされた) ビデオになります。

これが現在どのように機能するかについてのいくつかの仮定と背景情報。

  • AVCamサンプル プロジェクトの一部である Apple の AVCamCaptureManagerを、カメラ関連のコードの多くの基盤として使用します。
  • AVCaptureSessionPresetMediumをプリセットとして使用して、AVCamCapture セッションを開始します。
  • videoPreviewLayer を介して背景としてカメラ フィードをパイプアウトし始めます。
  • そのライブ videoPreviewLayer を、openGL を使用して「描画」(フィンガー ペイント スタイル) できるビューでオーバーレイします。「描画」ビューの背景は [UIColor clearColor] です。

この時点で、ユーザーは iPad3 のカメラを空の雲に向けて、見える形を描くことができるという考えです。この機能は問題なく動作します。ユーザー セッションの「フラットな」ビデオ スクリーン キャプチャを作成しようとすると、パフォーマンスの問題が発生し始めます。結果として得られる「フラットな」ビデオでは、カメラ入力がユーザーの描画にリアルタイムでオーバーレイされます。

私たちが探しているものと同様の機能を持つアプリの良い例は、App Store で入手できるBoard Camです。

プロセスを開始するために、ビューには常に「記録」ボタンが表示されます。ユーザーが記録ボタンをタップすると、記録ボタンが再度タップされるまで、セッションが「フラット」ビデオ画面キャプチャとして記録されることが期待されます。

ユーザーが「記録」ボタンをタップすると、コードで次のことが起こります

  • AVCaptureSessionPresetがAVCaptureSessionPresetMediumからAVCaptureSessionPresetPhotoに変更され、

    - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
    
  • isRecording値がYESに設定されています。
  • didOutputSampleBuffer はデータの取得を開始し、現在のビデオ バッファー データから画像を作成します。への呼び出しでこれを行います

    - (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer
    
    • self.currentImage がこれに設定されます
  • アプリケーションのルート ビュー コントローラーは、drawRect のオーバーライドを開始して、最終的なビデオで個別のフレームとして使用される平坦化されたイメージを作成します。

  • そのフレームはフラットビデオに書き込まれます

個別のフレームとして使用されるフラット イメージを作成するには、ルート ViewController の drawRect 関数で、AVCamCaptureManager のdidOutputSampleBufferコードによって受信された最後のフレームを取得します。それが下です

- (void) drawRect:(CGRect)rect {


    NSDate* start = [NSDate date];
    CGContextRef context = [self createBitmapContextOfSize:self.frame.size];

    //not sure why this is necessary...image renders upside-down and mirrored
    CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, self.frame.size.height);
    CGContextConcatCTM(context, flipVertical);

    if( isRecording)
        [[self.layer presentationLayer] renderInContext:context];

    CGImageRef cgImage = CGBitmapContextCreateImage(context);
    UIImage* background = [UIImage imageWithCGImage: cgImage];
    CGImageRelease(cgImage);

    UIImage *bottomImage = background;


    if(((AVCamCaptureManager *)self.captureManager).currentImage != nil && isVideoBGActive )
    {

        UIImage *image = [((AVCamCaptureManager *)self.mainContentScreen.captureManager).currentImage retain];//[UIImage
        CGSize newSize = background.size;
        UIGraphicsBeginImageContext( newSize );
        // Use existing opacity as is
        if( isRecording )
        {
            if( [self.mainContentScreen isVideoBGActive] && _recording)
            {
                [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
            }
            // Apply supplied opacity

            [bottomImage drawInRect:CGRectMake(0,0,newSize.width,newSize.height) blendMode:kCGBlendModeNormal alpha:1.0];

        }
        UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

        UIGraphicsEndImageContext();

        self.currentScreen = newImage;



        [image release];
    }

    if (isRecording) {
        float millisElapsed = [[NSDate date] timeIntervalSinceDate:startedAt] * 1000.0;
        [self writeVideoFrameAtTime:CMTimeMake((int)millisElapsed, 1000)];
    }

    float processingSeconds = [[NSDate date] timeIntervalSinceDate:start];
    float delayRemaining = (1.0 / self.frameRate) - processingSeconds;

    CGContextRelease(context);

    //redraw at the specified framerate
    [self performSelector:@selector(setNeedsDisplay) withObject:nil afterDelay:delayRemaining > 0.0 ? delayRemaining : 0.01];  
}

createBitmapContextOfSizeは以下です

- (CGContextRef) createBitmapContextOfSize:(CGSize) size {
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace = nil;
    int             bitmapByteCount;
    int             bitmapBytesPerRow;

    bitmapBytesPerRow   = (size.width * 4);
    bitmapByteCount     = (bitmapBytesPerRow * size.height);
    colorSpace = CGColorSpaceCreateDeviceRGB();
    if (bitmapData != NULL) {
        free(bitmapData);
    }
    bitmapData = malloc( bitmapByteCount );
    if (bitmapData == NULL) {
        fprintf (stderr, "Memory not allocated!");
        CGColorSpaceRelease( colorSpace );
        return NULL;
    }

    context = CGBitmapContextCreate (bitmapData,
                                     size.width ,
                                     size.height,
                                     8,      // bits per component
                                     bitmapBytesPerRow,
                                     colorSpace,
                                     kCGImageAlphaPremultipliedFirst);

    CGContextSetAllowsAntialiasing(context,NO);
    if (context== NULL) {
        free (bitmapData);
        fprintf (stderr, "Context not created!");
        CGColorSpaceRelease( colorSpace );
        return NULL;
    }

    //CGAffineTransform transform = CGAffineTransformIdentity;
    //transform = CGAffineTransformScale(transform, size.width * .25, size.height * .25);
    //CGAffineTransformScale(transform, 1024, 768);

    CGColorSpaceRelease( colorSpace );

    return context;
}

- (void)captureOutput:didOutputSampleBuffer fromConnection

// Delegate routine that is called when a sample buffer was written
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection
{

    // Create a UIImage from the sample buffer data
    [self imageFromSampleBuffer:sampleBuffer];
}

- (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef)以下のsampleBuffer

// Create a UIImage from sample buffer data - modifed not to return a UIImage *, rather store it in self.currentImage
- (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer
{

    // unlock the memory, do other stuff, but don't forget:

    // Get a CMSampleBuffer's Core Video image buffer for the media data
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    // Lock the base address of the pixel buffer
    CVPixelBufferLockBaseAddress(imageBuffer, 0);

   // uint8_t *tmp = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer);
    int bytes = CVPixelBufferGetBytesPerRow(imageBuffer); // determine number of bytes from height * bytes per row
    //void *baseAddress = malloc(bytes);
    size_t height = CVPixelBufferGetHeight(imageBuffer);     
    uint8_t *baseAddress = malloc( bytes * height );
    memcpy( baseAddress, CVPixelBufferGetBaseAddress(imageBuffer), bytes * height );
    size_t width = CVPixelBufferGetWidth(imageBuffer);

    // Create a device-dependent RGB color space
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    // Create a bitmap graphics context with the sample buffer data
    CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8,
                                                 bytes, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);


    // CGContextScaleCTM(context, 0.25, 0.25); //scale down to size
    // Create a Quartz image from the pixel data in the bitmap graphics context
    CGImageRef quartzImage = CGBitmapContextCreateImage(context);
    // Unlock the pixel buffer
    CVPixelBufferUnlockBaseAddress(imageBuffer,0);

    // Free up the context and color space
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    free(baseAddress);

    self.currentImage = [UIImage imageWithCGImage:quartzImage scale:0.25 orientation:UIImageOrientationUp];

    // Release the Quartz image
    CGImageRelease(quartzImage);


    return nil; 
}

最後に、以下のコードでwriteVideoFrameAtTime:CMTimeMakeを使用して、これをディスクに書き出します。

-(void) writeVideoFrameAtTime:(CMTime)time {
    if (![videoWriterInput isReadyForMoreMediaData]) {
        NSLog(@"Not ready for video data");
    }
    else {
        @synchronized (self) {
            UIImage* newFrame = [self.currentScreen retain];
            CVPixelBufferRef pixelBuffer = NULL;
            CGImageRef cgImage = CGImageCreateCopy([newFrame CGImage]);
            CFDataRef image = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));

            if( image == nil )
            {
                [newFrame release];
                CVPixelBufferRelease( pixelBuffer );
                CGImageRelease(cgImage);
                return;
            }

            int status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, avAdaptor.pixelBufferPool, &pixelBuffer);
            if(status != 0){
                //could not get a buffer from the pool
                NSLog(@"Error creating pixel buffer:  status=%d", status);
            }

            // set image data into pixel buffer
            CVPixelBufferLockBaseAddress( pixelBuffer, 0 );
            uint8_t* destPixels = CVPixelBufferGetBaseAddress(pixelBuffer);
            CFDataGetBytes(image, CFRangeMake(0, CFDataGetLength(image)), destPixels);  //XXX:  will work if the pixel buffer is contiguous and has the same bytesPerRow as the input data

            if(status == 0){
                BOOL success = [avAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:time];
                if (!success)
                    NSLog(@"Warning:  Unable to write buffer to video");
            }

            //clean up
            [newFrame release];
            CVPixelBufferUnlockBaseAddress( pixelBuffer, 0 );
            CVPixelBufferRelease( pixelBuffer );
            CFRelease(image);
            CGImageRelease(cgImage);
        }

    }

}

isRecording が YES に設定されるとすぐに、iPad 3 のパフォーマンスは約 20FPS からおそらく 5FPS に上がります。Insturmentsを使用すると、次のコードのチャンク ( drawRect:から) が原因で、パフォーマンスが使用できないレベルに低下することがわかります。

 if( _recording )
        {
            if( [self.mainContentScreen isVideoBGActive] && _recording)
            {
                [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
            }
            // Apply supplied opacity

            [bottomImage drawInRect:CGRectMake(0,0,newSize.width,newSize.height) blendMode:kCGBlendModeNormal alpha:1.0];

        }

全画面をキャプチャしているため、「drawInRect」が提供するはずのすべての利点が失われることを理解しています。具体的には、再描画の高速化について話しているのは、理論的には表示のごく一部 (CGRect で渡されたもの) のみを更新するためです。繰り返しますが、フルスクリーンをキャプチャすると、drawInRectがほぼ同じくらいのメリットを提供できるかどうかはわかりません。

パフォーマンスを向上させるために、imageFromSampleBufferが提供する画像と描画ビューの現在のコンテキストを縮小すると、フレーム レートが向上するのではないかと考えています。残念ながら、CoreGrapics.Framework は私が過去に使用したことがないため、パフォーマンスを許容レベルまで効果的に調整できるかどうかはわかりません。

CoreGraphics Guru の入力がありますか?

また、一部のコードでは ARC がオフになっています。アナライザーは 1 つのリークを示していますが、これは誤検出であると考えています。

空が限界のCloudWriter ™ が近日公開されます!

4

1 に答える 1

1

まともな記録パフォーマンスが必要な場合は、Core Graphics を使用して再描画することを避ける必要があります。純粋な OpenGL ES に固執します。

あなたはすでに OpenGL ES でフィンガー ペインティングを行っていると言っているので、それをテクスチャにレンダリングできるはずです。ライブ ビデオ フィードをテクスチャに送信することもできます。そこから、フィンガー ペインティング テクスチャのアルファ チャネルに基づいて 2 つのオーバーレイ ブレンドを行うことができます。

これは、OpenGL ES 2.0 シェーダーを使用すると非常に簡単に実行できます。実際、ペイント コードからレンダリングされたテクスチャを提供する場合、私のGPUImageオープン ソース フレームワークは、ビデオ キャプチャとこの部分のブレンドを処理できます (ビデオにオーバーレイされた画像の例については、FilterShowcase サンプル アプリケーションを参照してください)。ペイントが OpenGL ES 1.1 ではなく 2.0 を使用していること、および GPUImage OpenGL ES コンテキストと同じ共有グループを持っていることを確認する必要がありますが、CubeExample アプリケーションでその方法を示します。

また、利用可能な場合はテクスチャ キャッシュを使用して (iOS 5.0 以降)、GPUImage でのビデオ録画を高性能な方法で処理します。

私のフレームワークのようなものを使用し、OpenGL ES 内に留まることで、720p ビデオ (iPad 2) または 1080p ビデオ (iPad 3) の 30 FPS でこのブレンディングを記録できるはずです。

于 2012-10-17T19:55:01.813 に答える