简介:这是一份基于Swift语言实现的《Instafilter》实时滤镜应用完整工程源码,面向iOS开发者与图像处理初学者,演示了从相机实时视频帧捕获、CoreImage滤镜链到图片保存、社交分享的完整流程。工程采用Storyboard搭建界面,涵盖AVCaptureSession视频采集、CIColorControls亮度/对比度/饱和度调节、CIPhotoEffectNoir黑白滤镜、CISepiaTone棕褐色调以及滑块动态调整强度,同时配置相机与相册访问权限,并实现保存到相册及UIActivityViewController分享。压缩包内共12个文件,包含3个Swift源文件、3个Plist配置文件、1个Storyboard界面文件、1个pbxproj工程文件及工作区数据等,结构精简清晰,便于直接打开工程对照学习。资源包仅13KB,轻量易读,适合快速定位关键代码;已有232人下载学习,适合希望掌握实时滤镜与AVFoundation/CoreImage结合实践的开发者参考。通过该工程可以了解滤镜模型类的设计思路、DispatchQueue异步处理与CoreImage硬件加速等性能优化手段,并在此基础上进一步扩展更多创意特效。
1. Instafilter 到底是做什么的:iOS 滤镜卡顿的另一种解法
做滤镜类 App 的人大概率经历过同一个场景:照片在相册里滑动浏览没问题,可一旦把 Core Image 的 CIFilter 直接挂上去实时预览,画面就开始掉帧,滑动滤镜列表时像在翻幻灯片。问题通常不在滤镜算法本身,而是你把图像处理任务交给了 CPU,让它在 1200 万像素的照片上逐个像素地做数学运算。Instafilter 解决的正是这个问题:它是一层架在 Core Image 与 Metal 之间的 Swift 渲染框架,把 CIFilter 的执行搬到了 GPU 上,同时内置了几十个可直接调用的滤镜封装,让实时预览、视频逐帧处理、导出保存变成一套顺手的工作流。适合已经能写 Swift、但不想从零折腾 Metal 渲染管线的 iOS 开发者。
2. 核心机制:Instafilter 如何把 Core Image 滤镜搬到 GPU 上
2.1 先理清 Instafilter 和 Core Image 的关系
不少人在第一次接触 Instafilter 时会误以为它是一套全新的滤镜实现,要替换掉 Core Image。实际不是这样。Core Image 是 iOS 系统自带的图像处理框架,CIFilter 定义“做什么”,CIContext 决定“在哪里做、怎么做”。Instafilter 没有重新发明滤镜算法,它把 CIFilter 的实例管理、渲染上下文、色彩空间处理封装起来,然后强制让渲染发生在 Metal 上。
苹果从 iOS 5 开始就在 Core Image 里支持 GPU 渲染,但默认行为并不总是走 GPU,而且 CIContext 的创建和复用是一件很容易写错的事。常见错误是每次处理图片都新建 CIContext,导致内存和 GPU 资源频繁分配回收,最终表现比 CPU 渲染还要慢。Instafilter 把这一层固定下来:它内部直接持有一个 Metal 设备(MTLDevice),基于它创建 CIContext,所有滤镜渲染都在这个上下文里执行。
在 API 层面,Instafilter 对 CIFilter 做了二次封装,这可能是它最让人困惑的地方:你不直接操作 CIFilter 的 inputKeys,而是操作一个滤镜配置对象,比如调整饱和度就写 filter.saturation = 1.5,底层才会映射到 CIColorControls 的 inputSaturation。这样做的好处是参数名统一、类型安全,坏处是如果你对某一款 CIFilter 的原始 key 非常熟悉,刚开始会有点不适应。
| 滤镜类别 | 底层 Core Image 滤镜 | 配置文件里对应参数 |
|---|---|---|
| 颜色调整 | CIColorControls | saturation / brightness / contrast |
| 单色效果 | CIPhotoEffectMono | 无参数 |
| 高斯模糊 | CIGaussianBlur | radius |
| 边缘检测 | CIEdges | intensity |
| 色温 | CITemperatureAndTint | neutral / targetNeutral |
上面这张表只列了最常用的几个。实际使用时不需要记住每个 CIFilter 的 key,直接查配置文件里暴露了哪些属性即可。
2.2 渲染管线:从 UIImage 到屏幕中间发生了什么
一张图片经过 Instafilter 显示到屏幕上,大致经过四个阶段。理解这条链路对排查性能问题很关键,因为每多一次不必要的拷贝,帧时间都会肉眼可见地上涨。
第一阶段:输入图像被转换为 CIImage。CIImage 是 Core Image 的核心数据结构,但它不是一张真实存在的“图片”,更像是一个计算指令。这个特性决定了 Core Image 是懒执行的:只要没有调用 render 方法,前面的滤镜链都只是挂在内存里的描述。Instafilter 的 ImageRenderer 接受 UIImage 或 CIImage 输入,内部保持一个当前待处理的 CIImage。
第二阶段:滤镜链被附加到 CIImage 上。当你给 renderer 的 filter 属性赋值时,它在内部调用 CIFilter 的 setValue(forKey:) 方法,然后把 outputImage 串到上一步的结果后面。多个滤镜可以形成链式结构,前一个的输出成为后一个的输入。这一步不产生任何像素计算,纯粹是构建指令图。
第三阶段:渲染发生。CIContext 调用 render(_:to:),真正把指令图交给 Metal。GPU 开始执行像素着色器,把结果写进 Metal 纹理(MTLTexture)。这期间图像数据已经不在 CPU 内存里,而是作为纹理存在于 GPU 显存中,CPU 侧拿不到像素值,也没必要拿。
第四阶段:显示或导出。MetalView 会在自己的 draw 回调里把渲染好的纹理绘制到 CAMetalLayer 上;ImageRenderer 则把纹理读回 UIImage,用于保存到相册或分享。
注意:CIContext 是整个链路里最贵的资源,一个 App 里创建两三个就是明显的浪费。Instafilter 内部会把上下文管理好,但如果你自己单独创建了 CIContext 去处理别的图片,请务必复用。
2.3 为什么 GPU 方案比 CPU 方案快
这不是玄学,是硬件结构决定的。一张 1200 万像素的照片有大约 1200 万个像素点,绝大多数滤镜(色温、饱和度、模糊、锐化)对每个像素的计算是相互独立的,这正好是 GPU 最擅长的工作模式。一块移动端 GPU 有几百个执行单元,可以把 1200 万像素拆成大量小任务并行执行;CPU 通常只有 6 到 8 个核心,即便支持 SIMD 指令,单次能处理的像素数量也和 GPU 差着数量级。
但有一个边界要知道:不是所有滤镜都适合 GPU 加速。某些滤镜需要对整张图像做统计,比如自动色阶要先扫描全图找出最亮和最暗的像素,这类算法存在“先归约、再广播”的依赖关系,GPU 执行并不一定比 CPU 快。Instafilter 预置的滤镜大多避开了这类情况,选的都是局部算子,也可以说它在选品上有意做了偏向性。如果你的业务里明确需要全局统计类滤镜(比如直方图均衡),GPU 收益就要重新评估。
另一个容易被忽略的点是内存带宽。GPU 处理图片时,数据以纹理形式放在显存里,不需要在 CPU 内存和显存之间来回拷贝。如果按 CPU 方案处理,每一帧都要先在内存里读一遍原始图像,算完再写一遍,再交给后续流程,这个拷贝开销在视频处理场景下会放大到不可接受。
3. 最小可跑工程:用 MetalView 在 iOS 项目里集成 Instafilter
3.1 集成方式:先用 Swift Package Manager 跑起来
先说明白:这个库的集成方式在不同版本上有过变化,但 Swift Package Manager(SPM)是目前最常见、也最省事的选择。打开 Xcode,在项目设置里的 Package Dependencies 中添加依赖,搜索 Instafilter,选择最新版本即可。
添加之后,在需要用到的文件顶部写import Instafilter。这一步可能遇到两个问题:一是搜索不到包,通常是网络环境导致的,检查 Xcode 是否能正常访问 GitHub;二是编译报错,比如找不到 Metal 框架,这时需要在项目的 Build Phases 里手动添加 Metal.framework 和 CoreImage.framework,虽然 SPM 通常会帮你自动链接,但偶尔会漏掉。
import UIKit import Instafilter class ViewController: UIViewController { @IBOutlet var imageView: MetalView! override func viewDidLoad() { super.viewDidLoad() // 1. 创建滤镜配置对象 let adjust = Adjust() adjust.saturation = 1.4 adjust.brightness = 0.05 adjust.contrast = 1.1 // 2. 绑定到 MetalView imageView.image = UIImage(named: "sample") imageView.filter = adjust } }这段代码做了三件事:创建滤镜配置、把图片交给 MetalView、把滤镜挂上去。MetalView 是 Instafilter 提供的渲染视图,它继承自 MTKView,内部维护了完整的渲染循环。注意顺序:先设置滤镜参数,再赋值给 view,因为滤镜参数是在创建时读取的;如果你先设了 view 再调整 saturation,画面可能不会即时刷新,后面避坑章节会细说。
3.2 用 ImageRenderer 导出处理后的 UIImage
预览归预览,用户最终是要保存图片或者分享出去的。这时候用 MetalView 去截图不现实。Instafilter 提供了 ImageRenderer,专门处理“输入一张图、输出一张新图“的场景。
let renderer = ImageRenderer() renderer.filter = adjust // 输入图片 let inputImage = UIImage(named: "sample")! renderer.inputImage = inputImage // 获取输出图片 let outputImage = renderer.outputImageImageRenderer 的用法相当直接:设置 inputImage,读取 outputImage。内部会把这个输入转成 CIImage,执行滤镜链,然后通过 CIContext 渲染成 UIImage。
实际项目中通常会把这个操作放到异步队列里,因为第一次渲染时 GPU 要做不少准备,直接在主线程执行会造成卡顿。更重要的是,同一个 renderer 不要同时被两个线程调用,它不是线程安全的,关掉并发操作,从设计上规避崩溃。
提示:如果你只需要显示滤镜效果,不导出图片,直接使用 MetalView 是最省资源的路径。MetalView 的输出天然是 Metal 纹理,不需要做 CPU 和 GPU 之间的数据回读,这比 ImageRenderer 高效得多。
3.3 参数调整:FilterPack 里值得优先关注的 3 个
FilterPack 是 Instafilter 自带的滤镜集合,数量不少,但实际做产品时常用的也就几个。我建议优先调通三个:Adjust(颜色调整)、Blur(模糊)、PhotoEffects(单色系列),这三个覆盖了滤镜类 App 最基础的调色、背景虚化、胶片感需求。
// 颜色调整滤镜 let adjust = Adjust() adjust.saturation = 1.4 // 默认 1.0,0 表示完全去色,2.0 表示色彩极为浓烈 adjust.brightness = 0.1 // 默认 0.0,范围通常 -1.0 到 1.0 adjust.contrast = 1.2 // 默认 1.0,大于 1 增加对比度,小于 1 降低 // 模糊滤镜 let blur = Blur() blur.radius = 10.0 // 单位是像素,值越大越模糊 // 胶片效果滤镜 let effects = PhotoEffects() effects.effect = .mono // 可选 mono、instant、noir 等参数含义需要单独解释一下。saturation 是饱和度,0 时图片变成灰度图,2.0 时色彩会非常夸张,一般修图 App 会在 0.8 到 1.6 之间做滑杆。brightness 是亮度偏移,正值提亮、负值压暗,注意它和曝光不是一回事,亮度偏移会把暗部也强行提高,容易让黑色发灰。contrast 是对比度,大于 1 时亮的地方更亮、暗的地方更暗。blur 的 radius 单位是像素,10 在 1200 万像素大图上只有轻微模糊效果,在 500 万像素小图上已经很明显,跟图片分辨率直接相关。
PhotoEffects 是几个无参数滤镜的集合,里面封装了系统自带的 CIPhotoEffectMono、CIPhotoEffectInstant、CIPhotoEffectNoir 等,作用相当于给图片叠加一层风格预设,适合做滤镜列表第一屏的快速预览效果。
3.4 参数修改后如何让画面刷新
这是使用 MetalView 最常见的困惑点:滤镜参数改了,画面一动不动,像没反应一样。原因在于 MetalView 的 draw 循环不是每次都重新执行滤镜链的。它默认只在 image 或 filter 被赋值时标记为“需要渲染”。一旦你通过滑杆连续改变 filter 的属性,view 根本不知道画面已经过期了。
处理办法有两种。第一种是重新赋一次 filter 引用,强制触发视图更新,代码最简单但会多一次不必要的内部状态重置。第二种是拿到 MetalView 内部的渲染回调,在改变参数后手动调用刷新方法。具体调用哪个方法取决于你集成到的版本,但思路是一致的:改参数之后,让视图认为“内容变了”。
写出来大致是这个模式:
adjust.saturation = slider.value imageView.filter = adjust // 如果上述方式不触发刷新,手动标记 imageView.forceRender = true如果发现改了参数还是没反应,优先检查你是不是在设置 filter 之后又通过其他途径修改了同一个滤镜对象。另外注意,MetalView 必须在主线程操作,在其他线程改属性同样会出现画面不刷新的问题。
4. 给视频加实时滤镜:VideoRenderer 的接入与参数调整
4.1 视频滤镜的硬指标:帧时间预算
视频和静态图是两套完全不同的性能逻辑。静态图渲染一次 200 毫秒都无所谓,视频不行——24fps 下每帧只有约 41.7 毫秒,30fps 下只有 33.3 毫秒,加上视频采集、编码、显示本身的消耗,留给滤镜的时间往往不到 15 毫秒。
在这个预算内,CPU 方案基本没有胜算。GPU 方案虽然也有压力,但只要不是同时挂五六个重度滤镜,一般能跑满 30fps。Instafilter 的 VideoRenderer 做的事和 ImageRenderer 类似,但面向逐帧输入:你传入一个 CVPixelBuffer(摄像头采集的原始帧),它执行滤镜链,然后输出处理后的 pixel buffer。
我用 VideoRenderer 时的最深刻的体验是:性能不是主要瓶颈,线程模型才是。视频帧回调来自系统相机管线,运行在专用队列上,如果你在这个队列里做了任何阻塞操作(比如同步等待 GPU 结果),整条采集链路都会被拖住。
import Instafilter import AVFoundation class VideoProcessor { let renderer = VideoRenderer() let filter = Adjust() init() { filter.saturation = 1.3 renderer.filter = filter } // 在 AVCaptureVideoDataOutputSampleBufferDelegate 回调中调用 func process(_ sampleBuffer: CMSampleBuffer) -> CVPixelBuffer? { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil } return renderer.render(pixelBuffer) } }这段代码展示了 VideoRenderer 的基本用法:初始化一次,在帧回调里反复调用 render。关键点是 renderer 和 filter 都只创建一次,不要在回调里新建任何滤镜对象或渲染上下文。
4.2 实时预览与录制的帧率匹配
做视频滤镜时,预览和录制往往走两条路:预览用 MetalView 直接显示,录制则用 VideoRenderer 处理后再交给 AVAssetWriter。这两条路共享同一个滤镜配置,但性能开销是加倍的,因为同一帧被渲染了两次(一次给预览,一次给编码)。
我建议在这种场景下做帧丢策略。具体做法是给 VideoRenderer 加一个标志位:如果上一帧还没处理完,新来的帧直接丢弃,而不是排队等待。视频编码可以接受偶尔丢帧,但绝不能接受延迟累积——延迟一旦堆积,音画不同步的问题会远超掉帧的视觉影响。
final class FrameDropper { private var isProcessing = false private let renderer = VideoRenderer() func process(_ pixelBuffer: CVPixelBuffer) -> CVPixelBuffer? { guard !isProcessing else { return nil } isProcessing = true defer { isProcessing = false } return renderer.render(pixelBuffer) } }这个“处理中就不接新帧”的模式,在实时相机场景里非常实用。成本极低,但能避免绝大多数由渲染速度波动引起的连锁卡顿。
如果追求更均衡的效果,可以在此基础上做动态降级:当连续多帧超时,自动把滤镜链从“饱和度调整 + 边缘增强”降级为仅饱和度调整。这需要自己设计预设等级,但它比帧丢弃更平滑,用户几乎感知不到降级发生。
4.3 色彩空间参数对视频滤镜的影响
视频滤镜的颜色偏色是高频问题,根源几乎都出在色彩空间不匹配上。摄像头采集的 pixel buffer 通常是 YCbCr 格式,而 Core Image 滤镜期望的输入是某种 RGB 色彩空间。如果 renderer 内部没有做正确的转换,输出的颜色就会发灰、发蓝或者发绿。
Instafilter 在创建 CIContext 时会设置 working color space,正常情况下不需要你干预。但如果你在自定义滤镜链里手动创建了 CIFilter,并且对输入做了颜色空间假设,就有可能与 renderer 的设置不一致。
排查思路很简单:先用系统相机拍一段视频,不做任何滤镜直接输出,如果颜色正常,说明采集链路没问题;再换成单一滤镜(比如只调饱和度)观察偏色,如果偏色出现,说明滤镜链里的色彩空间处理有问题。逐段排查,不要同时怀疑多个环节。
// 自定义渲染上下文时需要显式指定色彩空间 let context = CIContext(mtlDevice: device, options: [ .workingColorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, .cacheIntermediates: true ])在自定义场景中,把 working color space 固定为 sRGB 是一种保守且可靠的做法。它能避免 extended range 带来的亮度漂移,代价是最暗与最亮的动态范围会受限,但这对于常规视频应用完全够用。
5. 实战排查:Instafilter 集成中的 5 个经典踩坑
5.1 内存持续上涨,几分钟后崩溃
现象:使用 MetalView 做实时预览,内存占用肉眼可见地持续上涨,大约在几十秒到几分钟内崩溃。
原因:代码里每次渲染都新建了 CIContext,或者反复创建 VideoRenderer。CIContext 是重型资源,内部持有 GPU 缓冲区,频繁创建不仅是性能问题,更会造成显存泄漏。
解决:CIContext 只能复用,不能放纵。Instafilter 的 renderer 自己管理上下文,你只要保证 renderer 是单例或长生命周期对象即可。不要做“每次处理图片都 new 一个 ImageRenderer”的操作。
// 错误示范:每次调用都新建 renderer func applyFilter(to image: UIImage) -> UIImage? { let renderer = ImageRenderer() // 不可取 renderer.inputImage = image return renderer.outputImage } // 正确做法:renderer 作为属性复用 final class FilterService { let renderer = ImageRenderer() func applyFilter(to image: UIImage) -> UIImage? { renderer.inputImage = image return renderer.outputImage } }5.2 改了滤镜参数,画面纹丝不动
现象:通过滑杆调整 saturation 或 radius,MetalView 上显示的图像没有任何变化,但导出图片时滤镜效果正常。
原因:MetalView 不会自动监听 filter 属性的变化,它只在 image 或 filter 被赋值时触发重新渲染。直接修改 filter 内部属性不会产生任何消息通知。
解决:每次修改参数后,重新给 MetalView 赋值 filter,或者调用视图的刷新方法强制重绘。
@IBAction func sliderChanged(_ sender: UISlider) { adjust.saturation = sender.value // 强制触发 MetalView 重新渲染 metalView.filter = adjust }5.3 视频画面颜色发灰、偏蓝
现象:用 VideoRenderer 处理摄像头实时画面时,输出颜色整体偏冷或发灰,与预览界面直接看到的画面不一致。
原因:色彩空间转换出问题。摄像头输出 YCbCr,Core Image 处理时需要在输出端转回 RGB,如果目标色彩空间的半色调曲线(gamma)不匹配,就会出现灰度和色偏。
解决:不要在处理链里手动写颜色转换代码,交给 renderer 的默认设置。如果你自定义了 CIContext,用 sRGB 作为 working color space,并且保证输入输出的像素格式一致。
注意:这种色偏问题在真机上容易发现,模拟器上反而不明显,因为模拟器对色彩空间的模拟不完整。所以不要用模拟器做色彩验收。
5.4 模拟器流畅运行,真机卡成幻灯片
现象:模拟器上滤镜滑动流畅、帧率稳定,安装到 iPhone 上后掉帧严重,画面撕裂。
原因:模拟器并不直接在虚拟设备上执行 Metal,而是依赖宿主机的 GPU 或软件渲染。宿主机的性能远高于 iPhone,尤其纹理带宽和 GPU 核心数不在一个量级,模拟器跑 60fps 不代表真机能跑 20fps。
解决:从第一天开始就在真机上做性能评估。特别是视频滤镜,务必用真机 + 系统相机采集链路进行测试,这种场景下模拟器数据没有任何参考价值。
5.5 切后台再回来,Metal 视图黑屏
现象:App 切到后台几秒后回来,MetalView 显示区域变成黑屏,重新设置 image 也没用。
原因:Metal 在 App 进入后台时可能释放部分 GPU 资源,或者 CAMetalLayer 的 drawable 状态被系统重置。MetalView 内部没有检测到这一变化,继续用失效的纹理去绘制。
解决:监听 UIApplication 的前后台切换通知,在进入前台时重新触发渲染,必要时重置 MetalView 的渲染状态。
NotificationCenter.default.addObserver( forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main ) { _ in // 重新设置 image,触发完整渲染流程 self.metalView.image = self.currentImage }6. 进阶:自定义滤镜链与性能验证方法
6.1 把自定义 CIFilter 接入滤镜体系
FilterPack 预置的滤镜覆盖了 80% 的常规需求,但产品总有自己的差异化诉求。好在这个框架不会限制你使用自定义滤镜。常见做法是写一个符合 Filter 协议的配置对象,内部持有你自己创建的 CIFilter 或 CIImage 处理链。
struct VintageWarmFilter: Filter { var warmth: Float = 0.5 func apply(_ input: CIImage) -> CIImage { let colorControls = CIFilter( name: "CIColorControls", parameters: [ kCIInputImageKey: input, kCIInputSaturationKey: 0.8, kCIInputContrastKey: 1.1 ] )! let temperature = CIFilter( name: "CITemperatureAndTint", parameters: [ kCIInputImageKey: colorControls.outputImage!, "inputNeutral": CIVector(x: CGFloat(6500 - warmth * 2000), y: 0) ] )! return temperature.outputImage! } }这个滤镜把饱和度调整和色温调整组合在一起,模拟一种偏暖的复古色调。自定义滤镜要注意两点:每一级 CIFilter 的 outputImage 都要有可选绑定,因为 CIFilter 在参数异常时可能返回 nil;参数单位的理解要准确,CITemperatureAndTint 的 inputNeutral 单位是开尔文,6500 是偏冷白平衡,数值降低则偏暖。
6.2 滤镜链的性能代价:合并渲染
多个滤镜串联时,每个滤镜的输出都可能中间纹理落一次 GPU 内存。一段五个滤镜的链就有五次写入,这在实时视频流里是巨大压力。核心优化思路是减少中间缓存:能在一个 CIFilter 里完成的就合并,比如色彩类调整(饱和度、亮度、对比度)本质上都是逐像素算子,可以使用 CIColorMatrix 一次完成三个调整。
func applyCombinedAdjustments(to input: CIImage) -> CIImage { let matrix = CIFilter(name: "CIColorMatrix")! // 用一个矩阵同时调整 RGB 增益和亮度偏移 matrix.setValue(input, forKey: kCIInputImageKey) matrix.setValue(CIVector(x: 1.2, y: 1.0, z: 1.0, w: 0), forKey: "inputRVector") matrix.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBiasVector") return matrix.outputImage! }比这更高效的方案是直接写 Metal Kernel,完全绕过 Core Image 的中间纹理管理,那就是另一套技术了。Instafilter 的价值在于:它让 80% 的常规滤镜能在 GPU 上跑起来,不需要你接触着色器语言;但当性能成为硬指标、滤镜又足够简单时,不排除回到 Metal Shader 这条路。
6.3 性能验证你该盯哪几个指标
性能验证不能只看帧率。帧率是结果,不是原因。要定位瓶颈,建议用 Instruments 的 Metal System Trace 模板,三个指标足够说明问题:
第一是每帧 GPU 时间,即 GPU 执行滤镜链花了多少毫秒。这个值受当前负载影响,需要连续采样几秒取中位数,单帧峰值意义不大。第二是纹理内存占用,如果滤镜链产生了大量中间纹理,这块会持续上涨。第三是帧间隔的时间分布,观察有没有周期性尖峰,尖峰如果出现在采集回调里,说明 CPU 侧有同步等待。
我在做滤镜类功能时,会固定一个性能基准流程:同一张测试图、同一款滤镜、同一个机型,连续处理一百帧取平均值。每次改动滤镜参数或渲染链路后都跑一遍这个基准,对比数据而不是凭感觉判断优化效果。时间久了,这些数据就是最可靠的项目资产。
滤镜开发的特点是有很多反直觉的边界:CPU 在某种场景下可能比 GPU 快,参数不是越大越有视觉冲击,颜色问题大多数与算法无关。这些细节没办法从文档里学透,只能是实际工程里一个一个碰出来。希望上面这些经验能帮你绕开我走过的弯路,让 Instafilter 集成这条路走得更顺一些。
本文还有配套的精品资源,点击获取