AVAssetImageGenerator иногда возвращает одно и то же изображение из двух последовательных кадров.

В настоящее время я извлекаю каждый кадр из видео с помощью AVAssetImageGenerator, но иногда он возвращает мне последовательно 2 раза почти одно и то же изображение (у них не одинаковое «время кадра»). Самое смешное, что это всегда происходит (в моем тестовом видео) каждые 5 кадров.

здесь и вот два изображения (откройте каждое в новой вкладке, затем переключите вкладки, чтобы увидеть различия).

Вот мой код:

//setting up generator & compositor
self.generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
generator.appliesPreferredTrackTransform = YES;
self.composition = [AVVideoComposition videoCompositionWithPropertiesOfAsset:asset];

NSTimeInterval duration = CMTimeGetSeconds(asset.duration);
NSTimeInterval frameDuration = CMTimeGetSeconds(composition.frameDuration);
CGFloat totalFrames = round(duration/frameDuration);

NSMutableArray * times = [NSMutableArray array];
for (int i=0; i<totalFrames; i++) {
    NSValue * time = [NSValue valueWithCMTime:CMTimeMakeWithSeconds(i*frameDuration, composition.frameDuration.timescale)];
    [times addObject:time];
}

AVAssetImageGeneratorCompletionHandler handler = ^(CMTime requestedTime, CGImageRef im, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error){
    // If actualTime is not equal to requestedTime image is ignored
    if(CMTimeCompare(actualTime, requestedTime) == 0) {
        if (result == AVAssetImageGeneratorSucceeded) {
            NSLog(@"%.02f     %.02f", CMTimeGetSeconds(requestedTime), CMTimeGetSeconds(actualTime));
            // Each log have differents actualTimes.
            // frame extraction is here...
        }
    }
};

generator.requestedTimeToleranceBefore = kCMTimeZero;
generator.requestedTimeToleranceAfter = kCMTimeZero;
[generator generateCGImagesAsynchronouslyForTimes:times completionHandler:handler];

Есть идеи, откуда это могло взяться?


person Martin    schedule 06.03.2013    source источник
comment
Здравствуйте, дорогой Мартин, сейчас 2014 год, и у меня та же проблема, что и у вас... вам удалось найти решение? Заранее спасибо :)   -  person Cesar    schedule 12.06.2014


Ответы (3)


См. следующие свойства AVAssetImageGenerator. Вы должны установить kCMTimeZero для обоих свойств, чтобы получить точные кадры.

/* The actual time of the generated images will be within the range [requestedTime-toleranceBefore, requestedTime+toleranceAfter] and may differ from the requested time for efficiency.
   Pass kCMTimeZero for both toleranceBefore and toleranceAfter to request frame-accurate image generation; this may incur additional decoding delay.
   Default is kCMTimePositiveInfinity. */
@property (nonatomic) CMTime requestedTimeToleranceBefore NS_AVAILABLE(10_7, 5_0);
@property (nonatomic) CMTime requestedTimeToleranceAfter NS_AVAILABLE(10_7, 5_0);

Прежде чем я установил kCMTimeZero для обоих свойств, я получил несколько одинаковых изображений для разного времени запроса, как у вас. Просто попробуйте следующий код.

self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:myAsset];
self.imageGenerator.requestedTimeToleranceBefore = kCMTimeZero;
self.imageGenerator.requestedTimeToleranceAfter = kCMTimeZero;
person alones    schedule 21.06.2013
comment
Пожалуйста, прочитайте внимательнее мой вопрос перед публикацией. Ваше предложение уже является частью моего кода. - person Martin; 24.06.2013

У меня была та же проблема, что и у вас, но гораздо более очевидная, дублирование происходило, когда интервал между двумя кадрами был меньше 1,0 секунды, и я понял, что это зависит от шкалы времени, которую я использовал для генерации значений CMTime.

До

CMTime requestTime = CMTimeMakeWithSeconds(imageTime, 1);

После

CMTime requestTime = CMTimeMakeWithSeconds(imageTime, playerItem.asset.duration.timescale);

... и Бум, больше никакого дублирования :)

Так что, возможно, вы можете попытаться увеличить, возможно, вдвое, шкалу времени, используя свой код:

NSValue * time = [NSValue valueWithCMTime:CMTimeMakeWithSeconds(i*frameDuration, composition.frameDuration.timescale*2)]; // *2 at the end

Для будущих ссылок вот мой код:

    playerItem = [AVPlayerItem playerItemWithURL:item.movieUrl];
    imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:playerItem.asset];
    imageGenerator.requestedTimeToleranceAfter = kCMTimeZero;
    imageGenerator.requestedTimeToleranceBefore = kCMTimeZero;

    float duration = item.duration;
    float interval = item.interval;

    NSLog(@"\nItem info:\n%f \n%f", duration,interval);

    NSString *srcPath = nil;
    NSString *zipPath = nil;

    srcPath = [item.path stringByAppendingPathComponent:@"info.json"];
    zipPath = [NSString stringWithFormat:@"/%@/info.json",galleryID];

    [zip addFileToZip:srcPath newname:zipPath level:0];

    NSTimeInterval frameNum = item.duration / item.interval;
    for (int i=0; i<=frameNum; i++)
    {
        NSArray* cachePathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        NSString* cachePath = [cachePathArray lastObject];

        srcPath = [cachePath stringByAppendingPathComponent:@"export-tmp.jpg"];
        zipPath = [NSString stringWithFormat:@"/%@/%d.jpg",galleryID,i];

        float imageTime = ( i * interval );

        NSError *error = nil;
        CMTime requestTime = CMTimeMakeWithSeconds(imageTime, playerItem.asset.duration.timescale);
        CMTime actualTime;

        CGImageRef imageRef = [imageGenerator copyCGImageAtTime:requestTime actualTime:&actualTime error:&error];

        if (error == nil) {
            float req = ((float)requestTime.value/requestTime.timescale);
            float real = ((float)actualTime.value/actualTime.timescale);
            float diff = fabsf(req-real);

            NSLog(@"copyCGImageAtTime: %.2f, %.2f, %f",req,real,diff);
        }
        else
        {
            NSLog(@"copyCGImageAtTime: error: %@",error.localizedDescription);
        }



        // consider using CGImageDestination -> http://stackoverflow.com/questions/1320988/saving-cgimageref-to-a-png-file
        UIImage *img = [UIImage imageWithCGImage:imageRef];
        CGImageRelease(imageRef);  // CGImageRef won't be released by ARC



        [UIImageJPEGRepresentation(img, 100) writeToFile:srcPath atomically:YES];

        if (srcPath != nil && zipPath!= nil)
        {
            [zip addFileToZip:srcPath newname:zipPath level:0]; // 0 = no compression. everything is a jpg image
            unlink([srcPath UTF8String]);
        }
person Cesar    schedule 12.06.2014
comment
interval не является собственностью AVPlayerItem. Как еще можно получить это значение? - person Cbas; 30.06.2016

Я использовал немного другой способ расчета запроса CMTime, и он, похоже, работал. Вот код (при условии iOS):

-(void)extractImagesFromMovie {

// set the asset
    NSString* path = [[NSBundle mainBundle] pathForResource:@"myMovie" ofType:@"MOV"];
    NSURL* movURL = [NSURL fileURLWithPath:path];

NSMutableDictionary* myDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES] , 
                                 AVURLAssetPreferPreciseDurationAndTimingKey , 
                                [NSNumber numberWithInt:0],
                                AVURLAssetReferenceRestrictionsKey, nil];

AVURLAsset* movie = [[AVURLAsset alloc] initWithURL:movURL options:myDict];


// set the generator
AVAssetImageGenerator* generator = [[AVAssetImageGenerator assetImageGeneratorWithAsset:movie] retain];
generator.requestedTimeToleranceBefore = kCMTimeZero;
generator.requestedTimeToleranceAfter = kCMTimeZero;

// look for the video track
AVAssetTrack* videoTrack;
bool foundTrack = NO;

for (AVAssetTrack* track in movie.tracks) {

    if ([track.mediaType isEqualToString:@"vide"]) {
        if (foundTrack) {NSLog (@"Error - - - more than one video tracks"); return(-1);}
        else {
            videoTrack = track;
            foundTrack = YES;
        }
    }
}
if (foundTrack == NO) {NSLog (@"Error - - No Video Tracks at all"); return(-1);}

// set the number of frames in the movie
int frameRate = videoTrack.nominalFrameRate;
float value = movie.duration.value;
float timeScale = movie.duration.timescale;
float totalSeconds = value / timeScale;
int totalFrames = totalSeconds * frameRate;

NSLog (@"total frames %d", totalFrames);

int timeValuePerFrame = movie.duration.timescale / frameRate;

NSMutableArray* allFrames = [[NSMutableArray new] retain];

// get each frame
for (int k=0; k< totalFrames; k++) {

    int timeValue = timeValuePerFrame * k;
    CMTime frameTime;
    frameTime.value = timeValue;
    frameTime.timescale = movie.duration.timescale;
    frameTime.flags = movie.duration.flags;
    frameTime.epoch = movie.duration.epoch;

    CMTime gotTime;

    CGImageRef myRef = [generator copyCGImageAtTime:frameTime actualTime:&gotTime error:nil];
    [allFrames addObject:[UIImage imageWithCGImage:myRef]];

    if (gotTime.value != frameTime.value) NSLog (@"requested %lld got %lld for k %d", frameTime.value, gotTime.value, k)

}

NSLog (@"got %d images in the array", [allFrames count]);
// do something with images here...
}
person Chen Sokolovsky    schedule 07.03.2013
comment
Спасибо за ваш ответ и добро пожаловать на SO. Я попробую ваш ответ в следующий раз, когда буду в проекте. - person Martin; 07.03.2013
comment
Нет, так не работает (результат тот же). Пожалуйста, имейте в виду, что в вашем коде есть некоторые утечки (allFrames, myRef). Кроме того, способ получения значений CMTime и TimeScale не является рекомендуемым способом (вы должны использовать CMTimeGetSeconds) - person Martin; 12.03.2013
comment
Это происходит ровно в 1 кадре из 5 в моем тестовом ролике, снятом самим айфоном (.mov). В других фильмах это бывает иногда, но не так часто. Не пробовал другой формат видео, потому что приложение должно работать только с фильмами iphone. - person Martin; 13.03.2013