Добавление фильтров к видео с помощью AVFoundation (OSX) - как мне записать полученное изображение обратно в AVWriter?

Настройка сцены

Я работаю над приложением для обработки видео, которое запускается из командной строки для чтения, обработки и экспорта видео. Я работаю с 4 треками.

  1. Много клипов, которые я добавляю в один трек, чтобы сделать одно видео. Давайте назовем это ugcVideoComposition.
  2. Клипы с альфа-каналом, которые позиционируются на второй дорожке и с использованием инструкций слоя, устанавливаются композитными при экспорте для воспроизведения поверх верхней части ugcVideoComposition.
  3. Музыкальная звуковая дорожка.
  4. Аудиодорожка для композиции ugcVideoComposition, содержащая звук из клипов, добавленных в одну дорожку.

У меня все это работает, могу скомпоновать его и правильно экспортировать с помощью AVExportSession.

Эта проблема

Теперь я хочу применить фильтры и градиенты к ugcVideoComposition.

Мои исследования до сих пор показывают, что это делается с использованием AVReader и AVWriter, извлечения CIImage, манипулирования им с помощью фильтров и последующей записи этого.

Я еще не получил всю функциональность, с которой работал выше, но мне удалось получить чтение и запись ugcVideoComposition на диск с помощью AssetReader и AssetWriter.

    BOOL done = NO;
    while (!done)
    {
        while ([assetWriterVideoInput isReadyForMoreMediaData] && !done)
        {
            CMSampleBufferRef sampleBuffer = [videoCompositionOutput copyNextSampleBuffer];
            if (sampleBuffer)
            {
                // Let's try create an image....
                CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
                CIImage *inputImage = [CIImage imageWithCVImageBuffer:imageBuffer];

                // < Apply filters and transformations to the CIImage here

                // < HOW TO GET THE TRANSFORMED IMAGE BACK INTO SAMPLE BUFFER??? >

                // Write things back out.
                [assetWriterVideoInput appendSampleBuffer:sampleBuffer];

                CFRelease(sampleBuffer);
                sampleBuffer = NULL;
            }
            else
            {
                // Find out why we couldn't get another sample buffer....
                if (assetReader.status == AVAssetReaderStatusFailed)
                {
                    NSError *failureError = assetReader.error;
                    // Do something with this error.
                }
                else
                {
                    // Some kind of success....
                    done = YES;
                    [assetWriter finishWriting];

                }
            }
         }
      }

Как вы можете видеть, я даже могу получить CIImage из CMSampleBuffer, и я уверен, что смогу разобраться, как манипулировать изображением и применять любые эффекты и т.д., которые мне нужны. То, что я не знаю, как сделать, это поместить получившееся манипулированное изображение BACK в SampleBuffer, чтобы я мог записать его снова.

Вопрос

Учитывая CIImage, как я могу поместить это в sampleBuffer, чтобы добавить его с assetWriter?

Любая помощь приветствуется - документация AVFoundation ужасна и либо пропускает важные моменты (например, как вернуть изображение после того, как вы его извлекли, либо фокусируется на рендеринге изображений на экран iPhone, что я не хочу делать).

Высоко ценится и спасибо!

2 ответа

Решение

В конце концов я нашел решение, перекопав много полных примеров и скудную документацию AVFoundation от Apple.

Самая большая путаница заключается в том, что, хотя на высоком уровне AVFoundation "разумно" согласован между iOS и OSX, элементы более низкого уровня ведут себя по-разному, имеют разные методы и разные методы. Это решение для OSX.

Настройка вашего AssetWriter

Прежде всего, убедитесь, что при настройке средства записи ресурсов вы добавляете адаптер для чтения из CVPixelBuffer. Этот буфер будет содержать измененные кадры.

    // Create the asset writer input and add it to the asset writer.
    AVAssetWriterInput *assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:[[videoTracks objectAtIndex:0] mediaType] outputSettings:videoSettings];
    // Now create an adaptor that writes pixels too!
    AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
                                                   assetWriterInputPixelBufferAdaptorWithAssetWriterInput:assetWriterVideoInput
                                                 sourcePixelBufferAttributes:nil];
    assetWriterVideoInput.expectsMediaDataInRealTime = NO;
    [assetWriter addInput:assetWriterVideoInput];

Чтение и письмо

Проблема в том, что я не смог найти напрямую сопоставимые методы между iOS и OSX - у iOS есть возможность визуализировать контекст непосредственно в PixelBuffer, где OSX НЕ поддерживает эту опцию. Контекст также настроен по-разному между iOS и OSX.

Обратите внимание, что вы должны включить QuartzCore.Framework в свой проект XCode.

Создание контекста в OSX.

    CIContext *context = [CIContext contextWithCGContext:
                      [[NSGraphicsContext currentContext] graphicsPort]
                                             options: nil]; // We don't want to always create a context so we put it outside the loop

Теперь вы хотите выполнить цикл, считывая AssetReader и записывая в AssetWriter... но учтите, что вы пишете через адаптер, созданный ранее, а не через SampleBuffer.

    while ([adaptor.assetWriterInput isReadyForMoreMediaData] && !done)
    {
        CMSampleBufferRef sampleBuffer = [videoCompositionOutput copyNextSampleBuffer];
        if (sampleBuffer)
        {
            CMTime currentTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);

            // GRAB AN IMAGE FROM THE SAMPLE BUFFER
            CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
            NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:kCVPixelFormatType_32BGRA], kCVPixelBufferPixelFormatTypeKey,
                                     [NSNumber numberWithInt:640.0], kCVPixelBufferWidthKey,
                                     [NSNumber numberWithInt:360.0], kCVPixelBufferHeightKey,
                                     nil];

            CIImage *inputImage = [CIImage imageWithCVImageBuffer:imageBuffer options:options];

            //-----------------
            // FILTER IMAGE - APPLY ANY FILTERS IN HERE

            CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone"];
            [filter setDefaults];
            [filter setValue: inputImage forKey: kCIInputImageKey];
            [filter setValue: @1.0f forKey: kCIInputIntensityKey];

            CIImage *outputImage = [filter valueForKey: kCIOutputImageKey];


            //-----------------
            // RENDER OUTPUT IMAGE BACK TO PIXEL BUFFER
            // 1. Firstly render the image
            CGImageRef finalImage = [context createCGImage:outputImage fromRect:[outputImage extent]];

            // 2. Grab the size
            CGSize size = CGSizeMake(CGImageGetWidth(finalImage), CGImageGetHeight(finalImage));

            // 3. Convert the CGImage to a PixelBuffer
            CVPixelBufferRef pxBuffer = NULL;
            // pixelBufferFromCGImage is documented below.
            pxBuffer = [self pixelBufferFromCGImage: finalImage andSize: size];

            // 4. Write things back out.
            // Calculate the frame time
            CMTime frameTime = CMTimeMake(1, 30); // Represents 1 frame at 30 FPS
            CMTime presentTime=CMTimeAdd(currentTime, frameTime); // Note that if you actually had a sequence of images (an animation or transition perhaps), your frameTime would represent the number of images / frames, not just 1 as I've done here.

            // Finally write out using the adaptor.
            [adaptor appendPixelBuffer:pxBuffer withPresentationTime:presentTime];

            CFRelease(sampleBuffer);
            sampleBuffer = NULL;
        }
        else
        {
            // Find out why we couldn't get another sample buffer....
            if (assetReader.status == AVAssetReaderStatusFailed)
            {
                NSError *failureError = assetReader.error;
                // Do something with this error.
            }
            else
            {
                // Some kind of success....
                done = YES;
                [assetWriter finishWriting];
            }
        }
    }
}

Создание PixelBuffer

Должен быть более простой способ, однако на данный момент это работает, и я нашел единственный способ напрямую перейти от CIImage к PixelBuffer (через CGImage) в OSX. Следующий код вырезан и вставлен из AVFoundation + AssetWriter: создание фильма с изображениями и аудио

    - (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image andSize:(CGSize) size
    {
        NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                         [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                         [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
                         nil];
        CVPixelBufferRef pxbuffer = NULL;

        CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width,
                                      size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options,
                                      &pxbuffer);
        NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);

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

        CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
        CGContextRef context = CGBitmapContextCreate(pxdata, size.width,
                                             size.height, 8, 4*size.width, rgbColorSpace,
                                             kCGImageAlphaNoneSkipFirst);
        NSParameterAssert(context);
        CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
        CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
                                       CGImageGetHeight(image)), image);
        CGColorSpaceRelease(rgbColorSpace);
        CGContextRelease(context);

        CVPixelBufferUnlockBaseAddress(pxbuffer, 0);

        return pxbuffer;
    }

Попробуйте использовать: SDAVAssetExportSession

SDAVAssetExportSession на GITHub

а затем реализуя делегат для обработки пикселей

- (void)exportSession:(SDAVAssetExportSession *)exportSession renderFrame:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime toBuffer:(CVPixelBufferRef)renderBuffer

{ Do CIImage and CIFilter inside here }
Другие вопросы по тегам