Core ML Model の画像出力を動画にする

Create ML で Video Style Transfer モデルが追加された（WWDC2020）ということもあり、出力画像を動画にしたい場面もあるのではないでしょうか。

AVFoundation フレームワークで可能です。

ぼくはカメラの出力を一旦 Pixel Buffer コピーの配列にして保存してから、順次処理しました。
[VideoCaptureOutputのフレームを参照し続けると処理が止まる](AVCaptureVideoDataOutputSampleBufferDelegateのcaptureOutputでフレームを配列で保持するとサンプルを取得できなくなる問題【対処法】)ので、PixelBuffer をディープコピーしてから使用した方がいいと思ってます。


#手順
###1,AVAssetWriterとを準備

サイズを Core ML の出力に合わせておきました。

```swift
func assetWriterSetting(){
    guard let url = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent(fileName + ".mp4") else { print("nil"); return}
    print(url)
    let videoSettings = [
      AVVideoWidthKey: 256,
      AVVideoHeightKey: 256,
      AVVideoCodecKey: AVVideoCodecType.h264
    ] as [String: Any]
        
    videoAssetInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
    pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoAssetInput, sourcePixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)])
    frameNumber = 0
    do {
        try assetWriter = AVAssetWriter(outputURL: url, fileType: .mp4)
       assetWriter.add(videoAssetInput)
       assetWriter.startWriting()
       assetWriter.startSession(atSourceTime: CMTime.zero)
       } catch {
          print("could not start video recording ", error)
      }
}
```

###2,PixelBufferを追加していく
VNCoreMLRequest の Completion Handler 内で Pixel Buffer を AVAssetWriterInputPixelBufferAdaptor に追加していきます。
ぼくは [Core ML Helpers](https://qiita.com/john-rocky/items/bfa631aba3e7aabc2a66) をつかって Multi Array から CGImage に変換したので、 CGImage を　Pixel Buffer に変換しています。 

上記 AssetWriter のセッティングで sourcePixelBufferAttributes を BGRA にしているため、 RGB から BGR 色空間への変換もおこなっています。 AssetWriter のセッティングで RGB を設定する方法を知っている方がいたら、教えてください。

**Pixel Buffer Poolがnilで返ってくる時は、AssetWriterの設定ミス（書き込み先URLが既に存在している。sourcePixelBufferAttributesで取扱不可のものを設定している）が原因のことが多いです。**

```swift
let result = coreMLRequest.results?.first as! VNCoreMLFeatureValueObservation
let multiArray = result.featureValue.multiArrayValue
guard let cgimage = multiArray?.cgImage(min: -1, max: 1, channel: nil)?.toBGR() else {print("drop"); return}
guard let pixelBufferPool = pixelBufferAdaptor.pixelBufferPool else {
    fatalError("Failed to allocate the PixelBufferPool")
}
var pixelBufferOut: CVPixelBuffer? = nil
CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &pixelBufferOut)
            
guard let pixelBuffer = pixelBufferOut else {
    fatalError("Failed to create the PixelBuffer")
}
            
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0)))
            
let context = CGContext(
    data: CVPixelBufferGetBaseAddress(pixelBuffer),
    width: cgimage.width,
    height: cgimage.height,
    bitsPerComponent: cgimage.bitsPerComponent,
    bytesPerRow: cgimage.bytesPerRow,
    space: CGColorSpaceCreateDeviceRGB(),
    bitmapInfo: cgimage.bitmapInfo.rawValue)
context?.draw(cgimage, in: cgimage.frame)
            
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0)))
if videoAssetInput.isReadyForMoreMediaData {
            
//処理速度が速すぎると、 isReadyForMoreMediaData が追いつかずに PixelBufferを追加できないことがあります。 適宜調整してください。
    
frameTime = CMTimeMake(value: Int64(Double(frameCount * fps) * durationForEachImage), timescale: Int32(fps))
pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: frameTime)
frameCount += 1
print(frameTime)
```
CGImage のサイズ取得とBGR変換のextension

```swift
extension CGImage {
    var frame: CGRect {
        return CGRect(x: 0, y: 0, width: self.width, height: self.height)
    }
    
    func toBGR()->CGImage{
        let ciImage = CIImage(cgImage: self)
        let ctx = CIContext(options: nil)
        let swapKernel = CIColorKernel( source:
                                            "kernel vec4 swapRedAndGreenAmount(__sample s) {" +
                                            "return s.bgra;" +
                                            "}"
        )
        let ciOutput = swapKernel?.apply(extent: (ciImage.extent), arguments: [ciImage as Any])
        let cgOut:CGImage = ctx.createCGImage(ciOutput!, from: ciOutput!.extent)!
        return cgOut
    }
}
```

###3,書き込み


```swift
if mlRequestsEnded {
   videoAssetInput.markAsFinished()
   assetWriter.endSession(atSourceTime: frameTime)    
   assetWriter.finishWriting(completionHandler: { [self] in
   print("comp")
}
```