核心图像:照片效果 react/change 在真实 iPhone 中从滑块改变强度时缓慢
Core Image: Photo effect react/change slowly in real iPhone when changing intensity from a Slider
下面的代码应用了一个图像选择器并在 contentView 中显示过滤后的图像,在这种情况下我使用了 SepiaTone 效果。当我在滑块中进行移动时,更改过滤器强度。照片反应延迟,滑动不流畅。
但是当使用来自 App Store 的某些应用程序时,例如 "Afterlight",使用它们的照片滤镜,当用户移动滑块时,照片效果的变化很顺利。
那么有什么不同,那些应用程序使用其他框架吗?如果我们仍然使用 Core Image,我们如何提高连续改变过滤器强度的性能?
图片选择器
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
picker.dismiss(animated: true, completion: nil)
}
}
@Binding var image: UIImage?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
内容视图
import CoreImage
import CoreImage.CIFilterBuiltins
import SwiftUI
struct ContentView: View {
@State private var image: Image?
@State private var filterIntensity = 0.5
@State private var showingImagePicker = false
@State private var inputImage: UIImage?
@State private var currentFilter = CIFilter.sepiaTone()
let context = CIContext()
var body: some View {
let intensity = Binding<Double>(
get: {
self.filterIntensity
},
set: {
self.filterIntensity = [=13=]
self.applyProcessing()
})
return NavigationView {
VStack {
ZStack {
Rectangle()
.fill(Color.secondary)
if image != nil {
image?
.resizable()
.scaledToFit()
} else {
Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
}
}
.onTapGesture {
self.showingImagePicker = true
}
HStack {
Text("Intensity")
Slider(value: intensity)
}
.padding(.vertical)
HStack {
Button("Change Filter") {
// change filter
}
Spacer()
Button("Save") {
// save the picture
}
}
}
.padding([.horizontal, .bottom])
.navigationBarTitle("Instafilter")
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
ImagePicker(image: self.$inputImage)
}
}
}
func loadImage() {
guard let inputImage = inputImage else { return }
let beginImage = CIImage(image: inputImage)
currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
applyProcessing()
}
func applyProcessing() {
currentFilter.intensity = Float(filterIntensity)
guard let outputImage = currentFilter.outputImage else { return }
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
let uiImage = UIImage(cgImage: cgImage)
image = Image(uiImage: uiImage)
}
}
}
它有助于在后台线程中执行图像过滤器,以免阻塞图像处理的主队列。此外,不对每一个变化做出反应而是对变化进行反跳是有意义的:在实际计算完成之前等待一小段时间,比如 50 毫秒没有变化——这可以防止在过滤器参数发生变化时发生大量图像计算通常在短时间内(例如当用户拖动滑块时)。两者都可以使用 Combine 框架来实现,这里是一个基于您的代码的示例。效果在单独的 class ImageEffect
中分开,每当 inputImage/filterIntensity 发生变化时,使用 Combine 在后台执行图像处理,使用 debounce operator:
进行优化
import CoreImage
import CoreImage.CIFilterBuiltins
import SwiftUI
import Combine
class ImageEffect: ObservableObject {
@Published var filterIntensity = 0.5
@Published var inputImage: UIImage?
@Published var outputImage: UIImage?
@Published var currentFilter = CIFilter.sepiaTone()
let context = CIContext()
var subscriptions = Set<AnyCancellable>()
let queue = DispatchQueue(label: "Image processing")
init() {
self.$inputImage
.map { inputImage -> CIImage? in
guard let inputImage = inputImage else { return nil }
return CIImage(image: inputImage)
}
.combineLatest(self.$filterIntensity)
.debounce(for: .milliseconds(50), scheduler: queue)
.map { inputImage, filterIntensity -> UIImage? in
guard let inputImage = inputImage else { return nil }
self.currentFilter.inputImage = inputImage
self.currentFilter.intensity = Float(filterIntensity)
guard let outputImage = self.currentFilter.outputImage else { return nil }
guard let cgImage = self.context.createCGImage(outputImage, from: outputImage.extent) else { return nil }
return UIImage(cgImage: cgImage)
}
.receive(on: RunLoop.main)
.sink { image in
self.outputImage = image
}
.store(in: &self.subscriptions)
}
}
struct ImageEffectView: View {
@ObservedObject var imageEffect = ImageEffect()
@State private var showingImagePicker = false
var body: some View {
NavigationView {
VStack {
ZStack {
Rectangle()
.fill(Color.secondary)
if imageEffect.outputImage != nil {
Image(uiImage: imageEffect.outputImage!)
.resizable()
.scaledToFit()
} else {
Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
}
}
.onTapGesture {
self.showingImagePicker = true
}
HStack {
Text("Intensity")
Slider(value: $imageEffect.filterIntensity)
}
.padding(.vertical)
HStack {
Button("Change Filter") {
// change filter
}
Spacer()
Button("Save") {
// save the picture
}
}
}
.padding([.horizontal, .bottom])
.navigationBarTitle("Instafilter")
.sheet(isPresented: $showingImagePicker) {
ImagePickerView(image: self.$imageEffect.inputImage)
}
}
}
}
下面的代码应用了一个图像选择器并在 contentView 中显示过滤后的图像,在这种情况下我使用了 SepiaTone 效果。当我在滑块中进行移动时,更改过滤器强度。照片反应延迟,滑动不流畅。
但是当使用来自 App Store 的某些应用程序时,例如 "Afterlight",使用它们的照片滤镜,当用户移动滑块时,照片效果的变化很顺利。
那么有什么不同,那些应用程序使用其他框架吗?如果我们仍然使用 Core Image,我们如何提高连续改变过滤器强度的性能?
图片选择器
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
picker.dismiss(animated: true, completion: nil)
}
}
@Binding var image: UIImage?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
内容视图
import CoreImage
import CoreImage.CIFilterBuiltins
import SwiftUI
struct ContentView: View {
@State private var image: Image?
@State private var filterIntensity = 0.5
@State private var showingImagePicker = false
@State private var inputImage: UIImage?
@State private var currentFilter = CIFilter.sepiaTone()
let context = CIContext()
var body: some View {
let intensity = Binding<Double>(
get: {
self.filterIntensity
},
set: {
self.filterIntensity = [=13=]
self.applyProcessing()
})
return NavigationView {
VStack {
ZStack {
Rectangle()
.fill(Color.secondary)
if image != nil {
image?
.resizable()
.scaledToFit()
} else {
Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
}
}
.onTapGesture {
self.showingImagePicker = true
}
HStack {
Text("Intensity")
Slider(value: intensity)
}
.padding(.vertical)
HStack {
Button("Change Filter") {
// change filter
}
Spacer()
Button("Save") {
// save the picture
}
}
}
.padding([.horizontal, .bottom])
.navigationBarTitle("Instafilter")
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
ImagePicker(image: self.$inputImage)
}
}
}
func loadImage() {
guard let inputImage = inputImage else { return }
let beginImage = CIImage(image: inputImage)
currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
applyProcessing()
}
func applyProcessing() {
currentFilter.intensity = Float(filterIntensity)
guard let outputImage = currentFilter.outputImage else { return }
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
let uiImage = UIImage(cgImage: cgImage)
image = Image(uiImage: uiImage)
}
}
}
它有助于在后台线程中执行图像过滤器,以免阻塞图像处理的主队列。此外,不对每一个变化做出反应而是对变化进行反跳是有意义的:在实际计算完成之前等待一小段时间,比如 50 毫秒没有变化——这可以防止在过滤器参数发生变化时发生大量图像计算通常在短时间内(例如当用户拖动滑块时)。两者都可以使用 Combine 框架来实现,这里是一个基于您的代码的示例。效果在单独的 class ImageEffect
中分开,每当 inputImage/filterIntensity 发生变化时,使用 Combine 在后台执行图像处理,使用 debounce operator:
import CoreImage
import CoreImage.CIFilterBuiltins
import SwiftUI
import Combine
class ImageEffect: ObservableObject {
@Published var filterIntensity = 0.5
@Published var inputImage: UIImage?
@Published var outputImage: UIImage?
@Published var currentFilter = CIFilter.sepiaTone()
let context = CIContext()
var subscriptions = Set<AnyCancellable>()
let queue = DispatchQueue(label: "Image processing")
init() {
self.$inputImage
.map { inputImage -> CIImage? in
guard let inputImage = inputImage else { return nil }
return CIImage(image: inputImage)
}
.combineLatest(self.$filterIntensity)
.debounce(for: .milliseconds(50), scheduler: queue)
.map { inputImage, filterIntensity -> UIImage? in
guard let inputImage = inputImage else { return nil }
self.currentFilter.inputImage = inputImage
self.currentFilter.intensity = Float(filterIntensity)
guard let outputImage = self.currentFilter.outputImage else { return nil }
guard let cgImage = self.context.createCGImage(outputImage, from: outputImage.extent) else { return nil }
return UIImage(cgImage: cgImage)
}
.receive(on: RunLoop.main)
.sink { image in
self.outputImage = image
}
.store(in: &self.subscriptions)
}
}
struct ImageEffectView: View {
@ObservedObject var imageEffect = ImageEffect()
@State private var showingImagePicker = false
var body: some View {
NavigationView {
VStack {
ZStack {
Rectangle()
.fill(Color.secondary)
if imageEffect.outputImage != nil {
Image(uiImage: imageEffect.outputImage!)
.resizable()
.scaledToFit()
} else {
Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
}
}
.onTapGesture {
self.showingImagePicker = true
}
HStack {
Text("Intensity")
Slider(value: $imageEffect.filterIntensity)
}
.padding(.vertical)
HStack {
Button("Change Filter") {
// change filter
}
Spacer()
Button("Save") {
// save the picture
}
}
}
.padding([.horizontal, .bottom])
.navigationBarTitle("Instafilter")
.sheet(isPresented: $showingImagePicker) {
ImagePickerView(image: self.$imageEffect.inputImage)
}
}
}
}