gpt4 book ai didi

ios - 使用 CGImageDestinationFinalize 创建大型 GIF - 内存不足

转载 作者:行者123 更新时间:2023-12-01 21:37:15 38 4
gpt4 key购买 nike

在创建具有大量帧的 GIF 时,我正在尝试解决性能问题。例如,某些 GIF 可能包含 > 1200 帧。使用我当前的代码,我的内存不足。我正在想办法解决这个问题;这可以分批完成吗?我的第一个想法是是否可以将图像附加在一起,但我认为没有方法可以做到这一点,也不认为 ImageIO 是如何创建 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)

我的新想法是,对于每 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.

这让我觉得我的想法行不通,但可惜我试过了。继续更新 3。

更新 3:
我一直在尝试 CGImageDestinationCopyImageSource下面是我更新后的代码的方法,但是我总是得到只有一帧的图像;这是因为最有可能是上述更新 2 中所述的文档。还有一种方法可以尝试: CGImageSourceCreateIncremental但我怀疑这就是我所需要的。

似乎我需要某种方式将 GIF 帧增量写入/附加到磁盘,以便我可以清除内存中的每个新块。也许是 CGImageDestinationCreateWithDataConsumer使用适当的回调来增量保存数据会是理想的吗?

更新 4:
我开始尝试 CGImageDestinationCreateWithDataConsumer方法来查看我是否可以使用 NSFileHandle 管理写入字节,但同样的问题是调用 CGImageDestinationFinalize一次发送所有字节,这与以前相同 - 我用完了内存。我真的需要帮助来解决这个问题,并会提供一大笔赏金。

更新 5:
我已经发布了一个大赏金。我希望看到一些没有第三方库或框架的出色解决方案来附加原始 NSData GIF 字节到彼此,并使用 NSFileHandle 增量地将其写入磁盘。 - 本质上是手动创建 GIF。或者,如果您认为可以使用 ImageIO 找到解决方案就像我尝试过的那样,那也太棒了。 Swizzling,子类化等。

更新 6:
我一直在研究如何在最低级别制作 GIF,并且我写了一个小测试,这与我在赏金的帮助下要实现的目标一致。我需要获取渲染的 UIImage,从中获取字节,使用 LZW 压缩它并将字节与其他一些工作(如确定全局颜色表)一起附加。信息来源: http://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html .

最新更新:

我花了整整一周的时间从各个角度对此进行研究,以了解在基于限制(例如最大 256 色)构建高质量 GIF 时究竟发生了什么。我相信并假设什么 ImageIO正在做的是在引擎盖下创建一个单一的位图上下文,将所有图像帧合并为一个,并且正在对该位图执行颜色量化以生成要在 GIF 中使用的单个全局颜色表。在由 ImageIO 制作的一些成功的 GIF 上使用十六进制编辑器确认他们有一个全局颜色表,除非您自己为每一帧设置它,否则永远不会有本地颜色表。在这个巨大的位图上执行颜色量化以构建调色板(再次假设,但强烈相信)。

我有一个奇怪而疯狂的想法:我的应用程序中的帧图像每帧只能有一种颜色,甚至更好的是,我知道我的应用程序使用了哪些小颜色集。第一个/背景框架是一个包含我无法控制的颜色的框架(用户提供的内容,例如照片),所以我在想我将对该 View 进行快照,然后使用具有我的应用程序处理的已知颜色的另一个 View 进行快照并使其成为我可以传递到普通 ImaegIO 的单个位图上下文GIF制作套路。有什么好处?好吧,通过将两个图像合并为一个图像,它可以从大约 1200 帧减少到 1 帧。 ImageIO然后将在小得多的位图上做它的事情,并用一帧写出单个 GIF。

现在我该怎么做才能构建实际的 1200 帧 GIF?我想我可以采用单帧 GIF 并很好地提取颜色表字节,因为它们位于两个 GIF 协议(protocol)块之间。我仍然需要手动构建 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];
}

最佳答案

您可以使用 AVFoundation 用您的图像编写视频。我上传了一个完整的工作测试项目 to this github repository .当您在模拟器中运行测试项目时,它将打印到调试控制台的文件路径。在视频播放器中打开该路径以检查输出。

我将在此答案中介绍代码的重要部分。

首先创建一个 AVAssetWriter .我会给它 AVFileTypeAppleM4V文件类型,以便视频在 iOS 设备上工作。

AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:self.url fileType:AVFileTypeAppleM4V error:&error];

使用视频参数设置输出设置字典:
- (NSDictionary *)videoOutputSettings {
return @{
AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: @((size_t)size.width),
AVVideoHeightKey: @((size_t)size.height),
AVVideoCompressionPropertiesKey: @{
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline31,
AVVideoAverageBitRateKey: @(1200000) }};
}

您可以调整比特率来控制视频文件的大小。我在这里非常保守地选择了编解码器配置文件 ( it supports some pretty old devices )。您可能想要选择较晚的配置文件。

然后创建一个 AVAssetWriterInput带媒体类型 AVMediaTypeVideo和您的输出设置。
NSDictionary *outputSettings = [self videoOutputSettings];
AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];

设置像素缓冲区属性字典:
- (NSDictionary *)pixelBufferAttributes {
return @{
fromCF kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
fromCF kCVPixelBufferCGBitmapContextCompatibilityKey: @YES };
}

您不必在此处指定像素缓冲区尺寸; AVFoundation 将从输入的输出设置中获取它们。我在这里使用的属性(我相信)是使用 Core Graphics 绘制的最佳选择。

接下来,创建一个 AVAssetWriterInputPixelBufferAdaptor使用像素缓冲区设置进行输入。
AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:input
sourcePixelBufferAttributes:[self pixelBufferAttributes]];

将输入添加到作者并告诉作者开始:
[writer addInput:input];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];

接下来我们将告诉输入如何获取视频帧。是的,我们可以在告诉作者开始写作后这样做:
    [input requestMediaDataWhenReadyOnQueue:adaptorQueue usingBlock:^{

这个块将完成我们需要用 AVFoundation 做的所有其他事情。每次准备好接受更多数据时,输入都会调用它。它可能能够在一次调用中接受多个帧,所以只要它准备好了,我们就会循环:
        while (input.readyForMoreMediaData && self.frameGenerator.hasNextFrame) {

我正在使用 self.frameGenerator实际绘制框架。稍后我将展示该代码。 frameGenerator决定视频何时结束(通过从 hasNextFrame 返回 NO )。它还知道每个帧应该何时出现在屏幕上:
            CMTime time = self.frameGenerator.nextFramePresentationTime;

要实际绘制帧,我们需要从适配器获取像素缓冲区:
            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 {

如果我们无法获得像素缓冲区,我们会发出错误信号并中止一切。如果我们确实得到了一个像素缓冲区,我们需要在它周围包裹一个位图上下文并询问 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);

现在我们可以将缓冲区附加到视频中。适配器这样做:
                    [adaptor appendPixelBuffer:buffer withPresentationTime:time];
} CVPixelBufferUnlockBaseAddress(buffer, 0);
} CVPixelBufferRelease(buffer);
}

上面的循环通过适配器推送帧,直到输入表示它已经足够了,或者直到 frameGenerator说它的帧数。如果 frameGenerator有更多帧,我们只需返回,当准备好更多帧时,输入将再次调用我们:
        if (self.frameGenerator.hasNextFrame) {
return;
}

如果 frameGenerator超出帧,我们关闭输入:
        [input markAsFinished];

然后我们告诉作者完成。完成后它会调用一个完成处理程序:
        [writer finishWritingWithCompletionHandler:^{
if (writer.status == AVAssetWriterStatusFailed) {
errorBlock(writer.error);
} else {
dispatch_async(dispatch_get_main_queue(), doneBlock);
}
}];
}];

相比之下,生成帧非常简单。这是生成器采用的协议(protocol):
@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

在我的测试中,我绘制了一个背景图像,并随着视频的进行慢慢地用纯红色覆盖它。
@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;
}

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;

我调用我的测试程序用来更新进度指示器的回调:
    if (self.frameGeneratedCallback != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
self.frameGeneratedCallback();
});
}

最后,为了演示可变帧速率,我以每秒 30 帧的速度发出前半帧,以每秒 15 帧的速度发出后半帧:
    if (self.framesEmittedCount < self.totalFramesCount / 2) {
nextTime.value += 1;
} else {
nextTime.value += 2;
}
}

@end

关于ios - 使用 CGImageDestinationFinalize 创建大型 GIF - 内存不足,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29723024/

38 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com