我不是在询问确切的代码,而是整体的想法。
这是我的问题:我正在尝试创建与“照片”应用中的过滤器选择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)
然后,我调用一个方法,该方法根据filterViews
数组中过滤器的数量来配置一些视图(filters.allFilters
),并对其进行迭代,并调用采用缩略图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
,但这给了我一些奇怪的结果。例如,它显示以下内容需要很长时间才能在backtrace的根目录中执行:
当我尝试查看Xcode中的代码时,得到以下内容,但并没有太大帮助:
所以,这就是问题所在。如果您对如何在“照片”应用中实现如此快速和快速的响应有任何想法,或者对我的实现有任何建议,非常感谢您的帮助。
答案 0 :(得分:4)
问题似乎是如何尽可能快地显示由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()
}
}