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

先放上演示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()
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,104评论 4 62
  • 昨天开始轮到我读游戏力这本书,刚买来的时候读了一些,字太多书太厚,没有坚持下去,正好看到晓宁姐招募,我就想借着这个...
    勤summer阅读 234评论 0 1
  • 在死寂的夜里,一个苏非开始哭泣 他说,这个世界就像一个紧闭的棺椁 我们被关在其中 由于无知,我们在愚昧和隔绝中荒废...
    岳晓晴阅读 192评论 0 0
  • 文:莲花香片 青岛大剧院艺术节开幕了,开幕演出是云门舞集的《水月》,接下来是北方昆曲剧院的昆曲《红楼梦》。这两场演...
    莲花香片阅读 514评论 3 4
  • 下午儿子幼儿园搞活动,活动开始之后,老师问:哪个小朋友说说自己最喜欢的玩具是什么?我看好几个小朋友主动举手,我就让...
    闹闹小乖乖阅读 243评论 0 0