klc*_*r89 25 core-graphics objective-c gif cgimage ios
我正在尝试在创建包含大量帧的GIF时修复性能问题.例如,某些GIF可能包含> 1200帧.使用我当前的代码,我的内存不足.我正在试图弄清楚如何解决这个问题; 这可以分批完成吗?我的第一个想法是,是否可以将图像附加在一起,但我认为没有一种方法可以用于ImageIO框架创建GIF或如何创建GIF .如果有一个复数CGImageDestinationAddImages方法但没有,那将是很好的,所以我迷失了如何尝试解决这个问题.我感谢任何提供的帮助.对于冗长的代码提前抱歉,但我觉得有必要逐步创建GIF.
只要视频中可能存在不同的GIF帧延迟,并且录制时间与每帧中所有动画的总和不一样,我就可以制作视频文件而不是GIF.
注意:跳转到下面的最新更新标题以跳过背景故事.
更新1 - 6:使用固定的线程锁定GCD,但内存问题仍然存在.这里不需要100%的CPU利用率,因为我展示了一段UIActivityIndicatorView时间的工作.使用该drawViewHierarchyInRect方法可能比renderInContext方法更有效/更快,但是我发现您不能drawViewHierarchyInRect在后台线程上使用该方法,并将afterScreenUpdates属性设置为YES; 它锁定了线程.
必须有一些方法可以分批编写GIF.我相信我已经将内存问题缩小到:CGImageDestinationFinalize这种方法对于制作具有大量帧的图像来说效率非常低,因为所有内容都必须在内存中写出整个图像.我已经证实了这一点,因为我在抓取渲染的containerView图层图像和调用时使用了很少的内存CGImageDestinationAddImage.我打电话CGImageDestinationFinalize给记忆仪的那一刻立即飙升; 有时最高可达2GB,具体取决于帧数.所需的内存量似乎很疯狂,可以制作~20-1000KB的GIF.
更新2:我发现有一种方法可能会带来一些希望.它是:
CGImageDestinationCopyImageSource(CGImageDestinationRef idst,
CGImageSourceRef isrc, CFDictionaryRef options,CFErrorRef* err)
Run Code Online (Sandbox Code Playgroud)
我的新想法是,对于每10帧或其他任意帧数,我会将它们写入目的地,然后在下一个循环中,先前完成的10帧目标将成为我的新源.但是有一个问题; 阅读文档,它说明了这一点:
Losslessly copies the contents of the image source, 'isrc', to the * destination, 'idst'.
The image data will not be modified. No other images should be added to the image destination.
* CGImageDestinationFinalize() should not be called afterward -
* the result is saved to the destination when this function returns.
Run Code Online (Sandbox Code Playgroud)
这让我觉得我的想法不起作用,但我试过了.继续更新3.
更新3:我一直在尝试CGImageDestinationCopyImageSource使用下面的更新代码来测试方法,但是我总是只用一帧来获取图像; 这是因为上面更新2中所述的文档很有可能.还有一种方法可能尝试:CGImageSourceCreateIncremental但我怀疑这是我需要的.
看起来我需要一些方法来逐步将GIF帧写入/附加到磁盘上,这样我就可以清除每个新块的内存.也许 CGImageDestinationCreateWithDataConsumer用适当的回调来逐步保存数据会是理想的吗?
更新4:我开始尝试使用CGImageDestinationCreateWithDataConsumer方法来查看是否可以管理字节,因为它们使用了一个NSFileHandle,但问题是调用CGImageDestinationFinalize一次性发送所有字节,这与以前相同 - 我运行记忆.我真的需要帮助才能解决这个问题并提供大笔奖金.
更新5:我发布了一笔巨额奖金.我希望看到一些没有第三方库或框架的精彩解决方案,可以将原始NSDataGIF字节相互附加并逐步将其写入磁盘NSFileHandle- 实际上是手动创建GIF.或者,如果您认为有一个解决方案可以ImageIO像我尝试过的那样使用,那将会很棒.Swizzling,subclassing等.
更新6:我一直在研究如何在最低级别制作GIF,并且我写了一个小测试,这与我在获得赏金帮助时的目标一致.我需要抓取渲染的UIImage,从中获取字节,使用LZW压缩它,并附加字节以及其他一些工作,如确定全局颜色表.信息来源:http://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html.
我花了整整一个星期的时间从各个角度研究这个问题,看看究竟是为了建立基于限制(例如最多256色)的优质GIF.我相信并假设ImageIO正在做的是在引擎盖下创建单个位图上下文,所有图像帧合并为一个,并且在该位图上执行颜色量化以生成要在GIF中使用的单个全局颜色表.在一些成功的GIF上使用十六进制编辑器ImageIO确认它们有一个全局颜色表,除非你自己为每一帧设置一个本地颜色表.在这个巨大的位图上执行颜色量化以构建调色板(再次假设,但强烈相信).
我有这个奇怪而疯狂的想法:我的应用程序中的帧图像每帧只能有一种颜色,甚至更好,我知道我的应用程序使用的是哪些小颜色.第一个/背景框架是一个框架,其中包含我无法控制的颜色(用户提供的内容,如照片),所以我想的是我将快照此视图,然后快照另一个视图,其中包含已知颜色的应用程序交易使用和制作一个位图上下文,我可以将其传递到正常的ImaegIOGIF制作例程.有什么好处?好吧,通过将两个图像合并为一个图像,可以将其从约1200帧降低到一帧.ImageIO然后将在更小的位图上执行其操作并使用一帧写出单个GIF.
现在我该怎么做才能构建实际的1200帧GIF?我想我可以采用单帧GIF并很好地提取颜色表字节,因为它们介于两个GIF协议块之间.我仍然需要手动构建GIF,但现在我不必计算调色板.我将窃取ImageIO最好的调色板,并将其用于我的字节缓冲区.我仍然需要一个LZW压缩器实现和赏金的帮助,但这应该比颜色量化更容易,这可能会非常缓慢.LZW也可能很慢,所以我不确定它是否值得; 不知道LZW将如何以~1200帧顺序执行.
你的想法是什么?
@property (nonatomic, strong) NSFileHandle *outputHandle;
- (void)makeGIF
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^
{
NSString *filePath = @"/Users/Test/Desktop/Test.gif";
[[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
self.outputHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
NSMutableData *openingData = [[NSMutableData alloc]init];
// GIF89a header
const uint8_t gif89aHeader [] = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 };
[openingData appendBytes:gif89aHeader length:sizeof(gif89aHeader)];
const uint8_t screenDescriptor [] = { 0x0A, 0x00, 0x0A, 0x00, 0x91, 0x00, 0x00 };
[openingData appendBytes:screenDescriptor length:sizeof(screenDescriptor)];
// Global color table
const uint8_t globalColorTable [] = { 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00 };
[openingData appendBytes:globalColorTable length:sizeof(globalColorTable)];
// 'Netscape 2.0' - Loop forever
const uint8_t applicationExtension [] = { 0x21, 0xFF, 0x0B, 0x4E, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2E, 0x30, 0x03, 0x01, 0x00, 0x00, 0x00 };
[openingData appendBytes:applicationExtension length:sizeof(applicationExtension)];
[self.outputHandle writeData:openingData];
for (NSUInteger i = 0; i < 1200; i++)
{
const uint8_t graphicsControl [] = { 0x21, 0xF9, 0x04, 0x04, 0x32, 0x00, 0x00, 0x00 };
NSMutableData *imageData = [[NSMutableData alloc]init];
[imageData appendBytes:graphicsControl length:sizeof(graphicsControl)];
const uint8_t imageDescriptor [] = { 0x2C, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x00 };
[imageData appendBytes:imageDescriptor length:sizeof(imageDescriptor)];
const uint8_t image [] = { 0x02, 0x16, 0x8C, 0x2D, 0x99, 0x87, 0x2A, 0x1C, 0xDC, 0x33, 0xA0, 0x02, 0x75, 0xEC, 0x95, 0xFA, 0xA8, 0xDE, 0x60, 0x8C, 0x04, 0x91, 0x4C, 0x01, 0x00 };
[imageData appendBytes:image length:sizeof(image)];
[self.outputHandle writeData:imageData];
}
NSMutableData *closingData = [[NSMutableData alloc]init];
const uint8_t appSignature [] = { 0x21, 0xFE, 0x02, 0x48, 0x69, 0x00 };
[closingData appendBytes:appSignature length:sizeof(appSignature)];
const uint8_t trailer [] = { 0x3B };
[closingData appendBytes:trailer length:sizeof(trailer)];
[self.outputHandle writeData:closingData];
[self.outputHandle closeFile];
self.outputHandle = nil;
dispatch_async(dispatch_get_main_queue(),^
{
// Get back to main thread and do something with the GIF
});
});
}
- (UIImage *)getImage
{
// Read question's 'Update 1' to see why I'm not using the
// drawViewHierarchyInRect method
UIGraphicsBeginImageContextWithOptions(self.containerView.bounds.size, NO, 1.0);
[self.containerView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *snapShot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Shaves exported gif size considerably
NSData *data = UIImageJPEGRepresentation(snapShot, 1.0);
return [UIImage imageWithData:data];
}
Run Code Online (Sandbox Code Playgroud)
您可以使用AVFoundation与图像一起编写视频.我已经将一个完整的工作测试项目上传到这个github存储库.在模拟器中运行测试项目时,它将打印到调试控制台的文件路径.在视频播放器中打开该路径以检查输出.
我将在这个答案中介绍代码的重要部分.
首先创建一个AVAssetWriter.我会给它AVFileTypeAppleM4V文件类型,以便视频在iOS设备上运行.
AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:self.url fileType:AVFileTypeAppleM4V error:&error];
Run Code Online (Sandbox Code Playgroud)
使用视频参数设置输出设置字典:
- (NSDictionary *)videoOutputSettings {
return @{
AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: @((size_t)size.width),
AVVideoHeightKey: @((size_t)size.height),
AVVideoCompressionPropertiesKey: @{
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline31,
AVVideoAverageBitRateKey: @(1200000) }};
}
Run Code Online (Sandbox Code Playgroud)
您可以调整比特率来控制视频文件的大小.我在这里非常保守地选择了编解码器配置文件(它支持一些非常古老的设备).您可能想要选择以后的配置文件.
然后创建一个AVAssetWriterInput媒体类型AVMediaTypeVideo和输出设置.
NSDictionary *outputSettings = [self videoOutputSettings];
AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];
Run Code Online (Sandbox Code Playgroud)
设置像素缓冲区属性字典:
- (NSDictionary *)pixelBufferAttributes {
return @{
fromCF kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
fromCF kCVPixelBufferCGBitmapContextCompatibilityKey: @YES };
}
Run Code Online (Sandbox Code Playgroud)
您不必在此指定像素缓冲区尺寸; AVFoundation将从输入的输出设置中获取它们.我在这里使用的属性(我相信)是使用Core Graphics绘图的最佳选择.
接下来,AVAssetWriterInputPixelBufferAdaptor使用像素缓冲区设置为您的输入创建一个.
AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:input
sourcePixelBufferAttributes:[self pixelBufferAttributes]];
Run Code Online (Sandbox Code Playgroud)
将输入添加到编写器并告诉编写器继续:
[writer addInput:input];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];
Run Code Online (Sandbox Code Playgroud)
接下来我们将告诉输入如何获取视频帧.是的,在我们告诉作者开始写作之后我们可以做到这一点:
[input requestMediaDataWhenReadyOnQueue:adaptorQueue usingBlock:^{
Run Code Online (Sandbox Code Playgroud)
这个块将完成我们需要用AVFoundation做的所有其他事情.每次输入准备接受更多数据时,输入都会调用它.它可能能够在一次调用中接受多个帧,因此我们将在它准备就绪时循环:
while (input.readyForMoreMediaData && self.frameGenerator.hasNextFrame) {
Run Code Online (Sandbox Code Playgroud)
我正在使用self.frameGenerator实际绘制框架.我稍后会显示该代码.在frameGenerator当影片结束(通过返回NO自定hasNextFrame).它还知道每个帧何时出现在屏幕上:
CMTime time = self.frameGenerator.nextFramePresentationTime;
Run Code Online (Sandbox Code Playgroud)
要实际绘制帧,我们需要从适配器获取像素缓冲区:
CVPixelBufferRef buffer = 0;
CVPixelBufferPoolRef pool = adaptor.pixelBufferPool;
CVReturn code = CVPixelBufferPoolCreatePixelBuffer(0, pool, &buffer);
if (code != kCVReturnSuccess) {
errorBlock([self errorWithFormat:@"could not create pixel buffer; CoreVideo error code %ld", (long)code]);
[input markAsFinished];
[writer cancelWriting];
return;
} else {
Run Code Online (Sandbox Code Playgroud)
如果我们无法获得像素缓冲区,我们会发出错误信号并中止所有内容.如果我们确实得到了像素缓冲区,我们需要在它周围包装一个位图上下文并要求frameGenerator在上下文中绘制下一帧:
CVPixelBufferLockBaseAddress(buffer, 0); {
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); {
CGContextRef gc = CGBitmapContextCreate(CVPixelBufferGetBaseAddress(buffer), CVPixelBufferGetWidth(buffer), CVPixelBufferGetHeight(buffer), 8, CVPixelBufferGetBytesPerRow(buffer), rgb, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); {
[self.frameGenerator drawNextFrameInContext:gc];
} CGContextRelease(gc);
} CGColorSpaceRelease(rgb);
Run Code Online (Sandbox Code Playgroud)
现在我们可以将缓冲区附加到视频中.适配器做到了:
[adaptor appendPixelBuffer:buffer withPresentationTime:time];
} CVPixelBufferUnlockBaseAddress(buffer, 0);
} CVPixelBufferRelease(buffer);
}
Run Code Online (Sandbox Code Playgroud)
上面的循环推动框架通过适配器,直到输入说它已经足够,或直到frameGenerator说它没有框架.如果frameGenerator有更多帧,我们只返回,当输入准备好更多帧时,输入将再次打电话给我们:
if (self.frameGenerator.hasNextFrame) {
return;
}
Run Code Online (Sandbox Code Playgroud)
如果frameGenerator帧数不足,我们会关闭输入:
[input markAsFinished];
Run Code Online (Sandbox Code Playgroud)
然后我们告诉作者完成.它完成后会调用一个完成处理程序:
[writer finishWritingWithCompletionHandler:^{
if (writer.status == AVAssetWriterStatusFailed) {
errorBlock(writer.error);
} else {
dispatch_async(dispatch_get_main_queue(), doneBlock);
}
}];
}];
Run Code Online (Sandbox Code Playgroud)
相比之下,生成帧非常简单.这是发电机采用的协议:
@protocol DqdFrameGenerator <NSObject>
@required
// You should return the same size every time I ask for it.
@property (nonatomic, readonly) CGSize frameSize;
// I'll ask for frames in a loop. On each pass through the loop, I'll start by asking if you have any more frames:
@property (nonatomic, readonly) BOOL hasNextFrame;
// If you say NO, I'll stop asking and end the video.
// If you say YES, I'll ask for the presentation time of the next frame:
@property (nonatomic, readonly) CMTime nextFramePresentationTime;
// Then I'll ask you to draw the next frame into a bitmap graphics context:
- (void)drawNextFrameInContext:(CGContextRef)gc;
// Then I'll go back to the top of the loop.
@end
Run Code Online (Sandbox Code Playgroud)
对于我的测试,我绘制了一个背景图像,并随着视频的进展慢慢用红色覆盖它.
@implementation TestFrameGenerator {
UIImage *baseImage;
CMTime nextTime;
}
- (instancetype)init {
if (self = [super init]) {
baseImage = [UIImage imageNamed:@"baseImage.jpg"];
_totalFramesCount = 100;
nextTime = CMTimeMake(0, 30);
}
return self;
}
- (CGSize)frameSize {
return baseImage.size;
}
- (BOOL)hasNextFrame {
return self.framesEmittedCount < self.totalFramesCount;
}
- (CMTime)nextFramePresentationTime {
return nextTime;
}
Run Code Online (Sandbox Code Playgroud)
Core Graphics将原点放在位图上下文的左下角,但我使用的是UIImage,而UIKit喜欢在左上角有原点.
- (void)drawNextFrameInContext:(CGContextRef)gc {
CGContextTranslateCTM(gc, 0, baseImage.size.height);
CGContextScaleCTM(gc, 1, -1);
UIGraphicsPushContext(gc); {
[baseImage drawAtPoint:CGPointZero];
[[UIColor redColor] setFill];
UIRectFill(CGRectMake(0, 0, baseImage.size.width, baseImage.size.height * self.framesEmittedCount / self.totalFramesCount));
} UIGraphicsPopContext();
++_framesEmittedCount;
Run Code Online (Sandbox Code Playgroud)
我调用我的测试程序用来更新进度指示器的回调:
if (self.frameGeneratedCallback != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
self.frameGeneratedCallback();
});
}
Run Code Online (Sandbox Code Playgroud)
最后,为了演示可变帧速率,我以每秒30帧的速度发射帧的前半部分,后半部分以每秒15帧的速度发射:
if (self.framesEmittedCount < self.totalFramesCount / 2) {
nextTime.value += 1;
} else {
nextTime.value += 2;
}
}
@end
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
4518 次 |
| 最近记录: |