苹果照片应用程序中的过滤器 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) 我已经尝试将OperationOperationQueue 与集合视图一起使用,该集合视图已启用预取。这会快速加载 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])

       
  

现在我只使用一个过滤器。 SepiaCIFilter 的子类。我将它创建为一个子类,因为将来我将创建一个自定义的子类。这是它的实现:

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) 使用GLKViewMTKView 进行渲染。这样您就可以简单地使用 CIImage,并且您只能在必要时创建 UIImage only。这两个都使用 GPU 进行渲染,而不是 CPU。 (2) 我在您发布的代码中没有看到任何内容,但请确保如果您使用的是CIContext,您创建一个并分享它(如果您没有,考虑使用一个)。 关于上下文和线程的非常简短的介绍可以在 Simon Gladman 的名为 Core Image for Swift 的免费 iBook(也可提供 PDF 格式)中找到。第 9-10 页。它是用 Swift 2 编写的,因此可能需要调整很多代码,但它应该是准确的。 @dfd,谢谢你的回答。我今天看看书。我没有听说过关于GLKViewMTKView 的任何消息,所以我想,我必须阅读文档。关于上下文:我用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 是如何实现的,它打开得如此之快?的主要内容,如果未能解决你的问题,请参考以下文章

为啥苹果拍的照片发到安卓上变黑了

通过python获取苹果手机备份文件中的照片,视频等信息采集

苹果手机怎样传照片

小组件怎么设置自己想要的照片?

安卓怎么把照片传到电脑?

基于布尔值过滤模板中的 Ember.js 数据