Одной из приятных функций моего
приложения Nodality является небольшой
виджет для редактирования точек кривой тона . Кривая тона изменяет яркость изображения для данного тонального диапазона. Например, увеличение значения самой левой точки кривой делает тени ярче, и, наоборот, уменьшение значения самой правой точки делает светлые участки более темными.
CIToneCurve фильтр является частью
CoreImage и принимает пять точек , которые образуют кривые. В этом блоге рассматривается создание
приложения Swift, которое позволяет пользователю загружать изображение и редактировать кривую тона этого изображения с помощью пяти вертикальных ползунков.
Я построил приложение с двумя основными элементами управления —
ToneCurveEditor, который содержит вертикальные ползунки, и
ImageWidget, который загружает и отображает изображение и применяет к нему фильтр. Основной
ViewController размещает их обоих.
До сих пор я не задумывался о том, как реагировать на изменения макета, но это приложение требует, чтобы пользовательский интерфейс реагировал разумно на разные ориентации. В альбомной ориентации мне нужны элементы управления кривой тона слева и изображением справа, а в портретной ориентации — элементы управления кривой тона ниже и изображение выше:
Для этого мой переопределенный контроллер view
viewDidLayoutSubviews () немного умнее моих предыдущих версий. Он сравнивает ширину и высоту своего фрейма, чтобы увидеть его ориентацию, а также размеры и расположение компонентов соответственно
override func viewDidLayoutSubviews() { let topMargin = Int(topLayoutGuide.length) if view.frame.size.width < view.frame.size.height { // portrait mode let widgetWidth = Int(view.frame.size.width) let widgetHeight = Int(view.frame.size.height) / 2 imageWidget.frame = CGRect(x: 5, y: topMargin, width: widgetWidth - 10, height: widgetHeight - topMargin - topMargin) toneCurveEditor.frame = CGRect(x: 0, y: widgetHeight, width: widgetWidth, height: widgetHeight - 50) } else { // landscape mode let widgetWidth = Int(view.frame.size.width) / 2 let widgetHeight = Int(view.frame.size.height) imageWidget.frame = CGRect(x: widgetWidth, y: topMargin, width: widgetWidth - 5, height: widgetHeight - topMargin - topMargin) toneCurveEditor.frame = CGRect(x: 0, y: 0, width: widgetWidth, height: widgetHeight - 50) } }
Теперь, когда мое устройство поворачивается, пользовательский интерфейс плавно переключается между двумя макетами.
ToneCurveEditor содержит пять ползунков , которые устанавливают значения кривых тонов и выдвинутые
CALayer ,
ToneCurveEditorCurveLayer , что делает кривой , соединяющее значение.
Я мог бы вручную создать каждый слайдер, но гораздо лучше создать их в цикле. Это уменьшает репликацию кода и, если, например,
в будущем CIToneCurve изменится для поддержки большего количества точек кривой, все, что мне нужно будет сделать, это изменить верхнюю границу цикла, чтобы создать больше ползунков.
Создание слайдера выполняется внутри
переопределенного
init () ToneCurveEditor
функция. Каждый из них является экземпляром
UISlider, который повернут на 90 °, задан обработчик действия и добавлен как вспомогательное представление:
func createSliders() { let rotateTransform = CGAffineTransformIdentity for i in 0..<5 { let slider = UISlider(frame: CGRectZero) slider.transform = CGAffineTransformRotate(rotateTransform, CGFloat(-90.0 * M_PI / 180.0)); slider.addTarget(self, action: "sliderChangeHandler:", forControlEvents: .ValueChanged) sliders.append(slider) addSubview(slider) } }
Если какой — либо слайдер изменился, я регенерировать ToneCurveEditor «s curveValues массив и отправляет событие изменения. Так как у curveValues есть наблюдатель didSet , ToneCurveEditor перерисовывает фоновую кривую при ее изменении:
var curveValues : [Double] = [Double](count: 5, repeatedValue: 0.0) { didSet { for (i : Int, value : Double) in enumerate(curveValues) { sliders[i].value = Float(value) } drawCurve() // forces a curveLayer.setNeedsDisplay() } }
Внизу в ToneCurveEditorCurveLayer я переопределил drawInContext (), чтобы нарисовать набор кривых Безье, чтобы связать точки вместе.
Стоп Пресс: теперь обновлен с помощью сплайна Эрмита, см. Этот блог для получения дополнительной информации.
Лучший способ сделать это — использовать сплайн Catmull Rom , но у меня не хватило
времени для его реализации, и я делаю это с помощью серии кривых, которые выглядят немного лучше, чем набор прямых линий.
Это делается путем создания нового UIBezierPath , зацикливания точек кривой и добавления кубических срезов Безье для каждой точки:
for (i: Int, value: Double) in enumerate(curveValues) { let pathPointX = i * (widgetWidth / curveValues.count) + (widgetWidth / curveValues.count / 2) let pathPointY = thumbRadius + margin + widgetHeight - Int(Double(widgetHeight) * value) if i == 0 { previousPoint = CGPoint(x: pathPointX,y: pathPointY) path.moveToPoint(previousPoint) } else { // TODO - implement as Catmull-Rom let currentPoint = CGPoint(x: pathPointX, y: pathPointY) let controlPointOne = CGPoint(x: currentPoint.x, y: previousPoint.y) let controlPointTwo = CGPoint(x: previousPoint.x, y: currentPoint.y) path.addCurveToPoint(CGPoint(x: pathPointX, y: pathPointY), controlPoint1: controlPointOne, controlPoint2: controlPointTwo) previousPoint = currentPoint } }
Конечный результат выглядит так:
ImageWidget содержит
UIButton , который позволяет пользователю загружать и изображения, и
UIImageView , который отображает изображение.
Когда кнопка нажата, она использует вставленную ссылку на свой родительский контроллер представления, чтобы вызвать
presentViewController () с
UIImagePickerController . Поскольку
ImageWidget реализует
протокол UIImagePickerControllerDelegate , после того, как пользователь выбрал изображение,
вызывается imagePickerController () .
Я обнаружил, что пытался отфильтровать большие изображения (мой
Fuji XE-1дает 16MP изображений) было мучительно медленно и начал сбой приложения. Итак, я написал довольно простое расширение для
UIImage, которое изменяет размеры загруженного изображения с точностью до определенного ограничивающего квадрата:
extension UIImage { func resizeToBoundingSquare(#boundingSquareSideLength : CGFloat) -> UIImage { let imgScale = self.size.width > self.size.height ? boundingSquareSideLength / self.size.width : boundingSquareSideLength / self.size.height let newWidth = self.size.width * imgScale let newHeight = self.size.height * imgScale let newSize = CGSize(width: newWidth, height: newHeight) UIGraphicsBeginImageContext(newSize) self.drawInRect(CGRect(x: 0, y: 0, width: newWidth, height: newHeight)) let resizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext(); return resizedImage } }
В производственном приложении вы можете использовать этот меньший прокси во время взаимодействия и заменить необработанное изображение для окончательного рендеринга.
Таким образом,
главная обязанность imagePickerController () — создать копию необработанного изображения, чтобы он уместился в квадрат 1024 x 1024. При изменении изображения или точек введенной кривой
ImageWidget должен применить фильтр кривой тона к измененному изображению. Чтобы обеспечить отзывчивость пользовательского интерфейса, фильтрация выполняется в фоновом потоке (снова с использованием
библиотеки Async Тобиаса
). Таким образом, метод экземпляра выглядит следующим образом:
func applyFilterAsync() { backgroundBlock = Async.background { if !self.filterIsRunning && self.loadedImage != nil { self.filterIsRunning = true self.filteredImage = ImageWidget.applyFilter(loadedImage: self.loadedImage!, curveValues: self.curveValues, ciContext: self.ciContext, filter: self.filter) } } .main { self.imageView.image = self.filteredImage self.filterIsRunning = false } }
…. и функция класса, которая выполняет тяжелую работу, выглядит следующим образом:
class func applyFilter(#loadedImage: UIImage, curveValues: [Double], ciContext: CIContext, filter: CIFilter) -> UIImage { let coreImage = CIImage(image: loadedImage) filter.setValue(coreImage, forKey: kCIInputImageKey) filter.setValue(CIVector(x: 0.0, y: CGFloat(curveValues[0])), forKey: "inputPoint0") filter.setValue(CIVector(x: 0.25, y: CGFloat(curveValues[1])), forKey: "inputPoint1") filter.setValue(CIVector(x: 0.5, y: CGFloat(curveValues[2])), forKey: "inputPoint2") filter.setValue(CIVector(x: 0.75, y: CGFloat(curveValues[3])), forKey: "inputPoint3") filter.setValue(CIVector(x: 1.0, y: CGFloat(curveValues[4])), forKey: "inputPoint4") let filteredImageData = filter.valueForKey(kCIOutputImageKey) as CIImage let filteredImageRef = ciContext.createCGImage(filteredImageData, fromRect: filteredImageData.extent()) let filteredImage = UIImage(CGImage: filteredImageRef) return filteredImage }
И
CIContext, и
CIFilter являются константами экземпляров, которые нужно создать только один раз, а затем использовать повторно, поэтому я передаю их в
applyFilter () .
Сочетание использования прокси-изображения и фильтрации в фоновом режиме делает приложение очень плавным и отзывчивым.
И это об этом. Как всегда,
исходный код доступен в репозитории myGitHub , наслаждайтесь!