[iOS][swift] アニメーションGIFを作る
iOSではCGImageDestinationを使うことによって複数の画像を一つのGIFアニメにすることが出来ます。
今回はCGImageDestinationを使ってアニメーションGIFを作る方法について記載します。
検証環境
今回は下記環境で試しています。
Xcode | 7.3.1 |
---|---|
Swift | 2.2 |
Deployment Target | iOS9.0以上 |
アニメーションGIFを作る
概要
処理の流れは下記図のイメージです。
準備
GIFを作るための画像が必要です。ここでは、
private var images = [CGImage]()
に画像が入っているものとします。
また、下記importも必要です。
import ImageIO import MobileCoreServices
1.CGImageDestinationの作成
CGImageDestinationの作成には、DataConsumer、MutableData、URLに書き出すものの3パターンあります。
この記事ではURLに書き出す、 CGImageDestinationCreateWithURL()
を利用します。(扱いやすいため)
/* Create an image destination writing to `url'. The parameter `type' * specifies the type identifier of the resulting image file. The * parameter `count' specifies number of images (not including thumbnails) * that the image file will contain. The `options' dictionary is reserved * for future use; currently, you should pass NULL for this parameter. * Note that if `url' already exists, it will be overwritten. */ @available(iOS 4.0, *) public func CGImageDestinationCreateWithURL(url: CFURL, _ type: CFString, _ count: Int, _ options: CFDictionary?) -> CGImageDestination?
引数は下記の通りです。
url | このURLに画像が書き出されます |
type | 画像タイプの定数を設定 |
count | 画像の枚数 |
options | 今後のための予約。現時点ではnilを渡す必要がある |
コード
下記の例ではTemporaryに適当な名称のgifファイルのパスを作成しています。
CGImageDestinationCreateWithURLの画像タイプにはGIFの場合、kUTTypeGIF定数を入れます。
let url = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(NSUUID().UUIDString).gif") guard let destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, images.count, nil) else { print("CGImageDestinationの作成に失敗") return }
CGImageDestinationCreateWithURLの戻り値はOptional型なのでguardでチェックしました。
2.CGImageDestinationにプロパティをセットする
プロパティをセットするにはCGImageDestinationSetProperties
を使用します。
/* Specify the dictionary `properties' of properties which apply to all * images in the image destination `idst'. */ @available(iOS 4.0, *) public func CGImageDestinationSetProperties(idst: CGImageDestination, _ properties: CFDictionary?)
引数は下記の通りです。
idst | CGImageDestinationを設定 |
properties | プロパティを設定 |
プロパティを作成する
アニメーションGIFを作成するのに必要な情報は、
- ループカウント (アニメーションの再生回数)
- フレームレート (アニメーションの切り替わる速さ)
がありますが、SetPropertiesにはループカウントの方を設定します。
kCGImagePropertyGIFDictionaryをキーとして値にkCGImagePropertyGIFLoopCountをキーとしたDictionaryを設定します。kCGImagePropertyGIFLoopCountの値はアニメーションの再生回数になります。0を設定すると無限ループになります。
文字に書くと分かり難いですが、コードで書くと下記になります。
// ループカウント(0で無限ループ) let properties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]]
コード
7行目でプロパティを定義し、8行目で設定しています。
let url = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(NSUUID().UUIDString).gif") guard let destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, images.count, nil) else { print("CGImageDestinationの作成に失敗") return } let properties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]] CGImageDestinationSetProperties(destination, properties)
3.CGImageDestinationに画像を追加する
画像をセットするにはCGImageDestinationAddImage
を使用します。
また、追加する画像分呼び出す必要があります。
/* Set the next image in the image destination `idst' to be `image' with * optional properties specified in `properties'. An error is logged if * more images are added than specified in the original count of the image * destination. */ @available(iOS 4.0, *) public func CGImageDestinationAddImage(idst: CGImageDestination, _ image: CGImage, _ properties: CFDictionary?)
引数は下記の通りです。
idst | CGImageDestinationを設定 |
image | 画像を設定(CGImage) |
properties | プロパティを設定 |
プロパティを作成する
CGImageDestinationAddImageのプロパティにはフレームレートを設定します。
kCGImagePropertyGIFDictionaryをキーとして値にkCGImagePropertyGIFDelayTimeをキーとしたDictionaryを設定します。kCGImagePropertyGIFDelayTimeの値はフレームレート(秒数)になります。floatで設定します。
文字に書くと分かり難いですが、コードで書くと下記になります。
// フレームレート let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: 0.2]]
コード
ループで画像を取り出してCGImageDestinationAddImageに画像を入れています。
下記コードではフレームレートを一定にしていますが、個別で設定すると、だんだん早くしたり、特定の所でスローにしたりすることも出来ます。
let url = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(NSUUID().UUIDString).gif") guard let destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, images.count, nil) else { print("CGImageDestinationの作成に失敗") return } let properties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]] CGImageDestinationSetProperties(destination, properties) let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: 0.2]] for image in images { CGImageDestinationAddImage(destination, image, frameProperties) }
4.生成した画像を書き出す
最後に生成した画像を書き出す為にCGImageDestinationFinalize
を使用します。
/* Write everything to the destination data, url or consumer of the image * destination `idst'. You must call this function or the image * destination will not be valid. After this function is called, no * additional data will be written to the image destination. Return true * if the image was successfully written; false otherwise. */ @available(iOS 4.0, *) public func CGImageDestinationFinalize(idst: CGImageDestination) -> Bool
引数は下記の通りです。
idst | CGImageDestinationを設定 |
戻り値は成功だったらtrue、失敗だったらfalseになります。
コード
CGImageDestinationFinalizeが成功したら(戻り値がtrueだったら)、urlに作成されたアニメーションGIFが生成されます。
let url = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(NSUUID().UUIDString).gif") guard let destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, images.count, nil) else { print("CGImageDestinationの作成に失敗") return } let properties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]] CGImageDestinationSetProperties(destination, properties) let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: 0.2]] for image in images { CGImageDestinationAddImage(destination, image, frameProperties) } if CGImageDestinationFinalize(destination) { print("GIF生成が成功") } else { print("GIF生成に失敗") }
これでアニメーションGIFを作成することが出来ます。
さいごに
(コードが最適化されてませんが、)下記はカメラで写した映像をCoreImageのFilterをかけつつアニメーションGIFにするサンプルです。
Storyboard上にUIImageViewとUIButtonとUIProgressViewを用意します。
ボタンを押したらGIF用の画像をためて、もう一度ボタンを押すか一定数たまったら生成するようにしています。
import UIKit import AVFoundation import ImageIO import MobileCoreServices class ViewController: UIViewController { // フレームレート private let frameRate = CMTimeMake(1, 5) // GIFを作る画像の枚数の上限 private let maxCount = 25 // GIF用の画像を入れておく private var images = [CGImage]() // GIF用のキャプチャを行うか private var isCapture = false // AVCaptureSession private var captureSession: AVCaptureSession? // Filter private let filter = CIFilter(name: "CIPhotoEffectChrome") @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var startButton: UIButton! @IBOutlet weak var progressView: UIProgressView! override func viewDidLoad() { super.viewDidLoad() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) // カメラの使用準備 initCamera() } override func viewDidDisappear(animated: Bool) { // カメラの停止とメモリ解放. guard let cpsSession = captureSession else { return } cpsSession.stopRunning() for output in cpsSession.outputs { cpsSession.removeOutput(output as! AVCaptureOutput) } for input in cpsSession.inputs { cpsSession.removeInput(input as! AVCaptureInput) } captureSession = nil resetComponent() } func initCamera() { //カメラからの入力を作成 guard let device = backCamera() else { print("背面カメラが見つかりません") return } var deviceInput: AVCaptureDeviceInput! //入力データの取得 do { deviceInput = try AVCaptureDeviceInput(device: device) } catch { return } //出力データの取得 let videoDataOutput:AVCaptureVideoDataOutput = AVCaptureVideoDataOutput() //カラーチャンネルの設定 videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey : Int(kCVPixelFormatType_32BGRA)] //画像をキャプチャするキューを指定 videoDataOutput.setSampleBufferDelegate(self, queue: dispatch_get_main_queue()) //キューがブロックされているときに新しいフレームが来たら削除 videoDataOutput.alwaysDiscardsLateVideoFrames = true //セッションの使用準備 let session = AVCaptureSession() //Input if session.canAddInput(deviceInput) { session.addInput(deviceInput as AVCaptureDeviceInput) } //Output if(session.canAddOutput(videoDataOutput)) { session.addOutput(videoDataOutput) } //解像度の指定 session.sessionPreset = AVCaptureSessionPresetMedium session.startRunning() // GIF画像なのでフレームレートを調整する do { try device.lockForConfiguration() device.activeVideoMaxFrameDuration = frameRate device.activeVideoMinFrameDuration = frameRate device.unlockForConfiguration() } catch { } captureSession = session } /// 背面カメラを取得 private func backCamera() -> AVCaptureDevice? { for device: AnyObject in AVCaptureDevice.devices() { if device.position == AVCaptureDevicePosition.Back { return device as? AVCaptureDevice } } return nil } @IBAction func didTapStartButton(sender: AnyObject) { isCapture = !isCapture if isCapture { // 開始 startButton.setTitle("Stop", forState: .Normal) images.removeAll() progressView.progress = 0 } else { // 終了 startButton.enabled = false startButton.setTitle("Start", forState: .Normal) makeGifImage() } } private func resetComponent() { isCapture = false startButton.setTitle("Start", forState: .Normal) images.removeAll() progressView.progress = 0 } private func makeGifImage() { // ループカウント(0で無限ループ) let fileProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]] // フレームレート let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: CMTimeGetSeconds(frameRate)]] let url = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent("\(NSUUID().UUIDString).gif") guard let destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, images.count, nil) else { print("CGImageDestinationの作成に失敗") return } CGImageDestinationSetProperties(destination, fileProperties) for image in images { CGImageDestinationAddImage(destination, image, frameProperties) } if CGImageDestinationFinalize(destination) { let controller = UIActivityViewController(activityItems: [url], applicationActivities: nil) presentViewController(controller, animated: true, completion: nil) } else { print("GIF生成に失敗") } startButton.enabled = true resetComponent() } } extension ViewController : AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) { guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } let opaqueBuffer = Unmanaged<CVImageBuffer>.passUnretained(imageBuffer).toOpaque() let pixelBuffer = Unmanaged<CVPixelBuffer>.fromOpaque(opaqueBuffer).takeUnretainedValue() let ciImage = CIImage(CVPixelBuffer: pixelBuffer, options: nil) filter?.setValue(ciImage, forKeyPath: kCIInputImageKey) guard let outputImage = filter?.valueForKey(kCIOutputImageKey) as? CIImage else { print("Filter適用に失敗") return } // Filterを適用しない場合はこちら //let outputImage = CIImage(CVPixelBuffer: pixelBuffer, options: nil) let image = UIImage(CIImage: outputImage, scale: 1.0, orientation: .Right) imageView.image = image // GIF用のキャプチャリング中かどうか if isCapture { if images.count < maxCount { let context = CIContext(options: nil) let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent) images.append(cgImage) progressView.progress = Float(images.count) / Float(maxCount) } if images.count >= maxCount { progressView.progress = 1 isCapture = false startButton.enabled = false makeGifImage() } } } }