たくさんのフレームでGIFを作成するときのパフォーマンスの問題を修正しようとしています。たとえば、一部のGIFには1200を超えるフレームが含まれる場合があります。現在のコードでは、メモリが不足しています。私はこれを解決する方法を見つけようとしています。これはバッチで行うことができますか?私の最初のアイデアは、画像を一緒に追加することが可能かどうかでしたが、そのための方法や、ImageIO
フレームワークによってGIFを作成する方法はないと思います。複数のCGImageDestinationAddImages
メソッドがあればいいのですが、ないので、これを解決する方法がわかりません。私は提供された助けに感謝します。コードが長くなってしまい申し訳ありませんが、GIFの作成を段階的に示す必要があると感じました。
ビデオで異なるGIFフレーム遅延が可能であり、各フレームのすべてのアニメーションの合計ほど長くはかからない限り、GIFの代わりにビデオファイルを作成できます。
注:バックストーリーをスキップするには、以下の最新の更新の見出しにジャンプしてください。
アップデート1〜6:GCD
を使用してスレッドロックを修正しましたが、メモリの問題は残っています。作業の実行中にUIActivityIndicatorView
を表示するため、ここでは100%のCPU使用率は問題ではありません。 drawViewHierarchyInRect
メソッドを使用すると、renderInContext
メソッドよりも効率的/高速になる可能性がありますが、drawViewHierarchyInRect
プロパティがYESに設定されているバックグラウンドスレッドではafterScreenUpdates
メソッドを使用できないことがわかりました。スレッドをロックします。
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
メソッドを試しましたが、常に1フレームのみの画像が返されます。これは、上記のアップデート2に記載されているドキュメントが原因である可能性が最も高いためです。おそらく試す方法がもう1つあります。CGImageSourceCreateIncremental
しかし、それが私に必要なことではないかと思います。
新しいチャンクをメモリから削除できるように、GIFフレームをディスクに段階的に書き込んだり追加したりする方法が必要なようです。おそらく、データを段階的に保存するための適切なコールバックを備えたCGImageDestinationCreateWithDataConsumer
が理想的でしょうか?
更新4:CGImageDestinationCreateWithDataConsumer
メソッドを試して、NSFileHandle
を使用してバイトを書き出すことができるかどうかを確認し始めましたが、CGImageDestinationFinalize
を呼び出すと、以前と同じようにすべてのバイトが1回で送信されるという問題があります。 -メモリが不足しています。私はこれを解決するために本当に助けが必要であり、大きな報奨金を提供します。
更新5:大きな報奨金を投稿しました。サードパーティのライブラリやフレームワークを使用せずに、生のNSData
GIFバイトを相互に追加し、NSFileHandle
を使用してディスクに段階的に書き出す優れたソリューションをいくつか見たいと思います。基本的には手動でGIFを作成します。または、私が試したようなImageIO
を使用して解決策が見つかると思うなら、それも素晴らしいでしょう。スウィズリング、サブクラス化など。
更新6:私はGIFが最低レベルでどのように作成されるかを調査しており、賞金の助けを借りて私が目指していることに沿った小さなテストを作成しました。レンダリングされたUIImageを取得し、そこからバイトを取得し、LZWを使用して圧縮し、グローバルカラーテーブルの決定などの他の作業とともにバイトを追加する必要があります。情報源: http://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html 。
私はこれをあらゆる角度から調査して、制限(最大256色など)に基づいてまともな品質のGIFを構築するために何が起こっているのかを正確に調べてきました。 ImageIO
が行っていることは、すべての画像フレームが1つにマージされた、内部で単一のビットマップコンテキストを作成し、このビットマップで色の量子化を実行して、GIFで使用される単一のグローバルカラーテーブルを生成することだと思います。 ImageIO
から作成されたいくつかの成功したGIFで16進エディターを使用すると、グローバルカラーテーブルがあり、フレームごとに自分で設定しない限り、ローカルカラーテーブルがないことが確認されます。この巨大なビットマップに対して色の量子化が実行され、カラーパレットが構築されます(ここでも想定していますが、強く信じています)。
私はこの奇妙でクレイジーなアイデアを持っています。私のアプリのフレーム画像は、フレームごとに1色しか異なることができません。さらに良いことに、アプリが使用する色の小さなセットを知っています。最初の/背景フレームは、制御できない色(写真などのユーザー提供のコンテンツ)を含むフレームなので、このビューのスナップショットを作成してから、アプリが処理する既知の色の別のビューのスナップショットを作成します。これを使用して、通常のImaegIO
GIF作成ルーチンに渡すことができる単一のビットマップコンテキストにします。利点は何ですか?これにより、2つの画像を1つの画像にマージすることで、最大1200フレームから1フレームになります。 ImageIO
は、はるかに小さいビットマップでその処理を実行し、1つのフレームで単一のGIFを書き出します。
では、実際の1200フレームGIFを作成するにはどうすればよいですか? 2つの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];
}
AVFoundationを使用して、画像を含むビデオを作成できます。完全に機能するテストプロジェクトをアップロードしました このgithubリポジトリに 。シミュレーターでテストプロジェクトを実行すると、デバッグコンソールへのファイルパスが出力されます。ビデオプレーヤーでそのパスを開いて、出力を確認します。
この回答では、コードの重要な部分について説明します。
AVAssetWriter
を作成することから始めます。ビデオがiOSデバイスで動作するように、AVFileTypeAppleM4V
ファイルタイプを指定します。
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) }};
}
ビットレートを調整して、ビデオファイルのサイズを制御できます。ここではかなり控えめにコーデックプロファイルを選択しました( かなり古いデバイスをサポートしています )。後のプロファイルを選択することをお勧めします。
次に、メディアタイプAVAssetWriterInput
と出力設定でAVMediaTypeVideo
を作成します。
NSDictionary *outputSettings = [self videoOutputSettings];
AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];
ピクセルバッファ属性ディクショナリを設定します。
- (NSDictionary *)pixelBufferAttributes {
return @{
fromCF kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
fromCF kCVPixelBufferCGBitmapContextCompatibilityKey: @YES };
}
ここでピクセルバッファのサイズを指定する必要はありません。 AVFoundationは、入力の出力設定からそれらを取得します。ここで使用した属性は、コアグラフィックスでの描画に最適です(私は信じています)。
次に、ピクセルバッファ設定を使用して、入力用のAVAssetWriterInputPixelBufferAdaptor
を作成します。
AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:input
sourcePixelBufferAttributes:[self pixelBufferAttributes]];
ライターに入力を追加し、ライターに開始するように指示します。
[writer addInput:input];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];
次に、ビデオフレームを取得する方法を入力に指示します。はい、ライターに書き始めるように指示した後、これを行うことができます。
[input requestMediaDataWhenReadyOnQueue:adaptorQueue usingBlock:^{
このブロックは、AVFoundationで行う必要のある他のすべてを実行します。入力は、より多くのデータを受け入れる準備ができるたびにそれを呼び出します。 1回の呼び出しで複数のフレームを受け入れることができる場合があるため、準備ができている限りループします。
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 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はOriginをビットマップコンテキストの左下隅に配置しますが、私はUIImage
を使用しており、UIKitはOriginを左上に配置するのが好きです。
- (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
kCGImagePropertyGIFHasGlobalColorMap
をNO
に設定すると、メモリ不足は発生しません。