先放上演示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!)
这样做的原因有两个
- 启动视频的输出比较耗费时间,同时启动视频和相机的output会使得进入viewController需要较长的时间,体验不好。
- 一直开启视频的输出容易发烫,也费电。在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()
}
}
网友评论