美文网首页swift编程开发iOS学习笔记小知识点,小技巧
AVFoundation与GPUImage实现多个滤镜的实时预览

AVFoundation与GPUImage实现多个滤镜的实时预览

作者: QYiZHong | 来源:发表于2018-09-28 16:53 被阅读37次

    先放上演示demo

    demo.gif

    底下是UICollectionView

    这里偷懒了,滤镜都是放同一个,大家可以自己更换不同的,下面主要讲一下如何将实时的相机预览放在cell里

    每个cell不同滤镜的方式看这里Swift如何更优雅的添加GPUImage滤镜

    尝试的失败方案

    1 使用GPUImage自带的相机和GPUImageView

    GPUImage自带的相机使用起来是很方便,然而我看了好久的头文件也没有试出来相机怎么获取每一帧。试着把GPUImageView传给cell,失败

    2 在每个cell里初始化GPUImage的camera

    这个想法有点疯狂,明显不可能的事情抱着试一试的心态,果然不行

    解决方案

    使用AVFoundation做自定义相机,使用GPUImage做单独的滤镜处理。写的时候发现AVCapturePhotoOutput并不能获得每一帧的图像,于是考虑再加一个AVCaptureVideoDataOutput。也就是把视频的output也作为输出,这个output用来获取每一帧

    var device: AVCaptureDevice?
    var input: AVCaptureDeviceInput?
    var photoOutput: AVCapturePhotoOutput?
    var videoOutput: AVCaptureVideoDataOutput?
    var session: AVCaptureSession?
    var previewLayer: AVCaptureVideoPreviewLayer?
    
    func cameraConfig() -> Void {
        device = AVCaptureDevice.default(for: AVMediaType.video)
        input = try? AVCaptureDeviceInput.init(device: device!)
        if (input != nil) {
            let outputSettings = AVCapturePhotoSettings.init(format: [AVVideoCodecKey : AVVideoCodecJPEG])
            photoOutput = AVCapturePhotoOutput.init()
            photoOutput?.photoSettingsForSceneMonitoring = outputSettings
            
            videoOutput = AVCaptureVideoDataOutput.init()
            let queue = DispatchQueue.init(label: "YZCamera.video")
            videoOutput?.setSampleBufferDelegate(self, queue: queue)
            
            session = AVCaptureSession.init()
            session?.addInput(input!)
            session?.addOutput(photoOutput!)
            previewLayer = AVCaptureVideoPreviewLayer.init(session: session!)
            previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
            previewLayer?.frame = CGRect.init(x: 0, y: 0, width: SCREEN_WIDTH, height: SCREEN_HEIGHT - 180)
            backView.layer.insertSublayer(previewLayer!, at: 0)
            session?.startRunning()
        }
    }
    

    这段代码可以看出来我的videoOutput只是初始化了,还并没有加到session里。在需要用到的时候再

    session?.addOutput(videoOutput!)
    

    不需要的时候

    session?.removeOutput(videoOutput!)
    

    这样做的原因有两个

    1. 启动视频的输出比较耗费时间,同时启动视频和相机的output会使得进入viewController需要较长的时间,体验不好。
    2. 一直开启视频的输出容易发烫,也费电。在iPhone 8 的真机上CPU长时间保持60%的占用率

    接下来需要完成一个代理

    AVCaptureVideoDataOutputSampleBufferDelegate
    

    用这个方法获取每一帧

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let image = handleSampleBuffer(buffer: sampleBuffer) else { return }
        DispatchQueue.main.async {
            let filterImage = self.addFilter(image: image)
            self.filterView.image = filterImage
        }
    }
    

    这里的sampleBuffer就是视频输出获取的每一帧。这里有一个坑,这个代理并不是在主线程,所以刷新UI一定要回到主线程。而且GPUImage处理图片也必须在主线程

    在拿到帧以后,获取最终输出图片的思路是buffer -> CIImage -> UIImage -> 等比压缩 -> 裁剪 -> CGImage -> 旋转图片 -> 滤镜 -> UIImage

    private func handleSampleBuffer(buffer: CMSampleBuffer) -> UIImage? {
        guard CMSampleBufferIsValid(buffer) == true else { return nil }
        let imageBuffer = CMSampleBufferGetImageBuffer(buffer)
        let ciImage: CIImage = CIImage.init(cvPixelBuffer: imageBuffer!)
        let image = UIImage.init(ciImage: ciImage).scaleImage(scale: 0.1)
        let rect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.width)
        let cgImage = image.convertToCGImage()
        guard let cgImageCorpped = cgImage.cropping(to: rect) else { return nil }
        let newImage = UIImage.init(cgImage: cgImageCorpped, scale: 1, orientation: UIImage.Orientation.right)
        return newImage
    }
    
    extension UIImage {
    
    
    /// 等比缩放图片
    ///
    /// - Parameter scale: 缩放程度 scale >= 0
    /// - Returns: UIImage
    func scaleImage(scale: CGFloat) -> UIImage {
        let reSize = CGSize.init(width: self.size.width * scale, height: self.size.height * scale)
        return reSizeImage(reSize: reSize)
    }
    
    
    /// UIImage -> CGImage
    ///
    /// - Returns: CGImage
    func convertToCGImage() -> CGImage {
        let cgImage = self.cgImage
        if cgImage == nil {
            let ciImage = self.ciImage
            let ciContext = CIContext.init()
            return ciContext.createCGImage(ciImage!, from: ciImage!.extent)!
        }else {
            return cgImage!
        }
    }
    
    private func reSizeImage(reSize: CGSize) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(reSize, false, UIScreen.main.scale)
        self.draw(in: CGRect.init(x: 0, y: 0, width: reSize.width, height: reSize.height))
        let reSizeImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return reSizeImage
    }
    }
    

    为了保证显示的image不被挤压,我在collectionView的cell里的imageView设置了

    contentMode = UIImageView.ContentMode.scaleAspectFill
    

    但是用这个contentMode就得让赋值的图片比例是对的,要不然图片就会超出autolayout。所以需要对图片进行裁剪,这里我把图片裁剪成了正方形

    然鹅不知道为什么拿到的图片都是左转了90度的,而且image的Orientation竟然还是.up,这就非常神奇。于是我让所有输出的图片都向右转了90度,就变正了。

    接着是添加滤镜,GPUImage自带有许多不同的滤镜

    private func addFilter(image: UIImage) -> UIImage {
        let filter = GPUImageSketchFilter.init()
        let newImage = filter.image(byFilteringImage: image)
        return newImage!
    }
    

    想要做到每个cell显示不同的滤镜各位可以想办法初始化不同的滤镜,这里就不演示了

    最后在collectionView里写一个didSet更新数据源就行了

    var image: UIImage? {
        didSet {
            reloadData()
        }
    }
    

    相关文章

      网友评论

        本文标题:AVFoundation与GPUImage实现多个滤镜的实时预览

        本文链接:https://www.haomeiwen.com/subject/eelpoftx.html