苹果照片应用程序中的过滤器 UIScrollView/UICollectionView 是如何实现的,它打开得如此之快?
Posted
技术标签:
【中文标题】苹果照片应用程序中的过滤器 UIScrollView/UICollectionView 是如何实现的,它打开得如此之快?【英文标题】:How is filters UIScrollView/UICollectionView in Apple's Photos app implemented that it opens so fast? 【发布时间】:2019-01-10 09:27:51 【问题描述】:我问的不是确切的代码,而是总体思路。
这是我的问题:我正在尝试创建类似于在照片应用中选择 UI 的过滤器。我尝试了多种方法,但它们都有其缺点。
1) 我已经尝试将Operation
和OperationQueue
与集合视图一起使用,该集合视图已启用预取。这会快速加载 viewController,但在滚动时会丢帧。
2) 现在我正在使用滚动视图和GCD
,但它加载 viewController 的时间过长(因为它一次将所有过滤器应用于其中的所有按钮),但随后它会平滑滚动。
注意:要回答这个问题,无需阅读以下部分(我相信),但是如果您对我如何尝试实现该功能感兴趣,欢迎阅读它。
为了实现所有过滤器,我使用了一个名为 Filters
的结构,它负责启动每个过滤器并将其附加到一个数组中。
struct Filters
var image: UIImage
var allFilters: [CIFilter] = []
init(image: UIImage)
self.image = image
guard let sepia = Sepia(image: image) else return
allFilters.append(contentsOf: [sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia])
现在我只使用一个过滤器。 Sepia
是 CIFilter
的子类。我将它创建为一个子类,因为将来我将创建一个自定义的子类。这是它的实现:
class Sepia: CIFilter
var inputImage: CIImage?
var inputIntensity: NSNumber?
@objc override var filterName: String?
return NSLocalizedString("Sepia", comment: "Name of a Filter")
convenience init?(image: UIImage, inputIntensity: NSNumber? = nil)
self.init()
guard let cgImage = image.cgImage else
return nil
if inputIntensity != nil
self.inputIntensity = inputIntensity
else
self.setDefaults()
let inputImage = CIImage(cgImage: cgImage)
self.inputImage = inputImage
override func setDefaults()
inputIntensity = 1.0
override var outputImage: CIImage?
guard let inputImage = inputImage, let inputIntensity = inputIntensity else
return nil
let filter = CIFilter(name: "CISepiaTone", withInputParameters: [kCIInputImageKey: inputImage, kCIInputIntensityKey: inputIntensity])
return filter?.outputImage
在viewController的viewDidLoad
我发起Filters
结构:
self.filters = Filters(image: image)
然后我调用一个方法,该方法根据filters.allFilters
数组中的过滤器数量配置一些视图(filterViews
)并对其进行迭代并调用一个方法,该方法采用缩略图UIImage
并对其应用过滤器然后在完成处理程序中返回它(出于调试原因,我在其中使用DispatchGroup
)。以下是将过滤器应用于缩略图的方法:
func imageWithFilter(filter: CIFilter, completion: @escaping(UIImage?)->Void)
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async
guard let outputImage = filter.value(forKey: kCIOutputImageKey) as? CIImage, let cgImageResult = self.context.createCGImage(outputImage, from: outputImage.extent) else
DispatchQueue.main.async
completion(nil)
group.leave()
return
let filteredImage = UIImage(cgImage: cgImageResult)
DispatchQueue.main.async
print (filteredImage)
completion(filteredImage)
group.leave()
group.notify(queue: .main)
print ("Filteres are set")
上面的打印语句和过滤后的图像地址很快就会打印出来,但是图像不会出现在视图中。
我曾尝试使用Time Profiler
,但它给了我一些奇怪的结果。例如,它显示以下内容需要很长时间才能在回溯的根目录中执行:
当我尝试在 Xcode 中查看代码时,我得到以下信息,这并没有多大帮助:
所以,这就是问题所在。如果您对如何在照片应用中实现它有任何想法,它是如此快速和响应迅速,或者如果您对我的实现有任何建议,我将非常感谢您的帮助。
【问题讨论】:
一些关于 CoreImage 的一般经验法则。 (1) 使用GLKView
或MTKView
进行渲染。这样您就可以简单地使用 CIImage,并且您只能在必要时创建 UIImage only。这两个都使用 GPU 进行渲染,而不是 CPU。 (2) 我在您发布的代码中没有看到任何内容,但请确保如果您使用的是CIContext
,您创建一个并分享它(如果您没有,考虑使用一个)。
关于上下文和线程的非常简短的介绍可以在 Simon Gladman 的名为 Core Image for Swift 的免费 iBook(也可提供 PDF 格式)中找到。第 9-10 页。它是用 Swift 2 编写的,因此可能需要调整很多代码,但它应该是准确的。
@dfd,谢谢你的回答。我今天看看书。我没有听说过关于GLKView
或MTKView
的任何消息,所以我想,我必须阅读文档。关于上下文:我用EAGL context
创建一个并分享它。
本书还介绍了 GLKViews。它涉及使用 MTKView (这是金属)。单一的 EAGL 上下文是可行的方法。最后一件事 - 从 ios 12 开始,Apple 已经“弃用”了 OpenGL 的所有内容,包括 GLKViews。他们希望您使用Metal 2
,它是在 WWDC '17 期间引入的。我编写了自己的CIKernels
,并将在 9 月努力更新一些“warp”和“general”内核代码,但我已经获得了 MTKView 工作。虽然这不是一个真正的答案,但我会发布一个带有格式化代码的答案来帮助你。
@dfd,好的,非常感谢。
【参考方案1】:
问题似乎是如何尽可能快地显示由 Core Image CIFilter 产生的 CIImage —— 如此之快,以至于它在视图控制器出现时立即出现;如此之快,事实上,用户可以使用滑块等调整 CIFilter 参数,并且图像将实时重新显示并跟上调整。
答案是使用 Metal Kit,尤其是 MTKView。渲染工作转移到设备的 GPU 上,速度非常快,速度足以在设备屏幕的刷新率下进入,因此在用户转动滑块时不会出现明显的延迟。
我有一个简单的演示,其中用户应用了一个名为 VignetteFilter 的自定义过滤器链:
当用户滑动滑块时,渐晕量(内圈)会平滑变化。在滑动的每一个瞬间,都会对原始图像应用一个新的滤镜,并在用户滑动滑块时一遍又一遍地渲染滤镜,与用户的动作保持同步。
正如我所说,底部的视图是 MTKView。以这种方式使用 MTKView 并不难;它确实需要一些准备,但都是样板文件。唯一棘手的部分实际上是让图像出现在您想要的位置。
这是我的视图控制器的代码(我忽略了除滑块和过滤图像的显示之外的所有内容):
class EditingViewController: UIViewController, MTKViewDelegate
@IBOutlet weak var slider: UISlider!
@IBOutlet weak var mtkview: MTKView!
var context : CIContext!
let displayImage : CIImage! // must be set before viewDidLoad
let vig = VignetteFilter()
var queue: MTLCommandQueue!
// slider value changed
@IBAction func doSlider(_ sender: Any?)
self.mtkview.setNeedsDisplay()
override func viewDidLoad()
super.viewDidLoad()
// preparation, all pure boilerplate
self.mtkview.isOpaque = false // otherwise background is black
// must have a "device"
guard let device = MTLCreateSystemDefaultDevice() else
return
self.mtkview.device = device
// mode: draw on demand
self.mtkview.isPaused = true
self.mtkview.enableSetNeedsDisplay = true
self.context = CIContext(mtlDevice: device)
self.queue = device.makeCommandQueue()
self.mtkview.delegate = self
self.mtkview.setNeedsDisplay()
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize)
func draw(in view: MTKView)
// run the displayImage thru the CIFilter
self.vig.setValue(self.displayImage, forKey: "inputImage")
let val = Double(self.slider.value)
self.vig.setValue(val, forKey:"inputPercentage")
var output = self.vig.outputImage!
// okay, `output` is the CIImage we want to display
// scale it down to aspect-fit inside the MTKView
var r = view.bounds
r.size = view.drawableSize
r = AVMakeRect(aspectRatio: output.extent.size, insideRect: r)
output = output.transformed(by: CGAffineTransform(
scaleX: r.size.width/output.extent.size.width,
y: r.size.height/output.extent.size.height))
let x = -r.origin.x
let y = -r.origin.y
// minimal dance required in order to draw: render, present, commit
let buffer = self.queue.makeCommandBuffer()!
self.context!.render(output,
to: view.currentDrawable!.texture,
commandBuffer: buffer,
bounds: CGRect(origin:CGPoint(x:x, y:y), size:view.drawableSize),
colorSpace: CGColorSpaceCreateDeviceRGB())
buffer.present(view.currentDrawable!)
buffer.commit()
【讨论】:
感谢您的回答。我已经为此使用MetalKit
。只是想确定它确实是以这种方式完成的:)
好吧,我看不到 Apple 的应用程序。但是现在没有其他方法可以快速渲染 CIImage,因为 OpenGL 已被弃用。
比我的答案要好得多。赞成并认为我的走了。
@dfd 对此很抱歉,但我非常希望你即将泄露 CIFilter 和 MTKView 的秘密,当情节标记在中间时我很失望...... :) 所以在最后我必须自己做,就像小红母鸡一样。以上是关于苹果照片应用程序中的过滤器 UIScrollView/UICollectionView 是如何实现的,它打开得如此之快?的主要内容,如果未能解决你的问题,请参考以下文章