Статьи

3D ReTouch: экспериментальное приложение для ретуширования с использованием 3D Touch

Вот последние в  моей серии экспериментов,  посвященных возможностям  3D Touch . Мое   приложение 3DReTouch позволяет пользователю выбрать одну из нескольких настроек изображения и применить эту настройку локально с интенсивностью, основанной на силе их прикосновения. Быстрое встряхивание их iPhone удаляет все их настройки и позволяет им начать все сначала.

Как и  ForceSketch , это приложение использует  CIImageAccumulator,  а также использует маску Core Core для выборочного наложения фильтра на изображение с использованием радиального градиента, центрированного по касанию пользователя.

Основы

Предустановленные фильтры — это константы типа Filter, и вместе с экземпляром соответствующего фильтра Core Image они содержат сведения о том, какие из их параметров зависят от силы прикосновения. Например,   фильтр повышения резкости является фильтром повышения резкости, и его  входная резкость  изменяется в зависимости от силы

 Filter(name: "Sharpen", ciFilter: CIFilter(name: "CISharpenLuminance")!,
        variableParameterName: kCIInputSharpnessKey,
        variableParameterDefault: 0,
        variableParameterMultiplier: 0.25)

Массив фильтров действует как поставщик данных для  UIPickerView . Когда вид выбора изменяется, я устанавливаю currentFilter для выбранного элемента для использования позже:

func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
    {
        currentFilter = filters[row]
    }

Фильтрация изображений происходит в фоновом потоке путем выбора ожидающих элементов из очереди «первым пришелпервым обслужен» , поэтому я использую  CADisplayLink  для планирования этой задачи при каждом обновлении кадра. 

Сенсорная обработка

С обоими обработчиками касаний touchesBegan и touchesMoved я хочу создать структуру PendingUpdate, содержащую силу, положение и текущий фильтр касания, и добавить его в свою очередь. Это делается внутри applyFilterFromTouches (), и в первую очередь защита гарантирует, что у нас есть касание, и оно находится на границе изображения:

guard let touch = touches.first
        where imageView.frame.contains(touches.first!.locationInView(imageView)) else
    {
        return
    }

Затем я нормализую силу касания или создаю значение по умолчанию для устройств без 3D Touch:

 let normalisedForce = traitCollection.forceTouchCapability == UIForceTouchCapability.Available ?
        touch.force / touch.maximumPossibleForce :

        CGFloat(0.5)

Затем, используя масштаб изображения, я могу вычислить позицию касания на реальном изображении, создать свой объект PendingUpdate и добавить его в очередь:

let imageScale = imageViewSide / fullResImageSide
    let location = touch.locationInView(imageView)
        
    let pendingUpdate = PendingUpdate(center: CIVector(x: location.x / imageScale, y: (imageViewSide - location.y) / imageScale),
        radius: 80,
        force: normalisedForce,
        filter: currentFilter)

Применение фильтра

Моя функция update () вызывается из CADisplayLink:

let displayLink = CADisplayLink(target: self, selector: Selector("update"))
    displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)

Используя guard (снова!), Я гарантирую, что на самом деле ожидается обновление для процесса:

guard pendingUpdatesToApply.count > 0 else
    {
        return
    }

… и если есть, я удаляю его и назначаю pendingUpdate:

  let pendingUpdate = pendingUpdatesToApply.removeFirst()

Остальная часть метода происходит внутри dispatch_async и разбивается на несколько частей. Прежде всего, я обновляю положение моего радиального градиента в зависимости от местоположения касания:

let gradientFilter = CIFilter(name: "CIGaussianGradient",
        withInputParameters: [
            "inputColor1": CIColor(red: 0, green: 0, blue: 0, alpha: 0),
            "inputColor0": CIColor(red: 1, green: 1, blue: 1, alpha: 1)])!

    self.gradientFilter.setValue(pendingUpdate.center,
        forKey: kCIInputCenterKey)

Затем я установил фильтр Core Image для параметров объекта pendingUpdate. Ему нужно входное изображение, которое я извлекаю из накопителя изображений, и оно требует обновления своего зависимого от силы параметра (variableParameterName) на основе силы касания:

pendingUpdate.filter.ciFilter.setValue(self.imageAccumulator.image(), forKey: kCIInputImageKey)
    
    pendingUpdate.filter.ciFilter.setValue(
        pendingUpdate.filter.variableParameterDefault + (pendingUpdate.force * pendingUpdate.filter.variableParameterMultiplier),
        forKey: pendingUpdate.filter.variableParameterName)

С их помощью я могу заполнить   параметры фильтра Blend With Mark . Он будет использовать градиент в качестве маски для наложения вновь отфильтрованного изображения поверх существующего изображения из аккумулятора только там, где пользователь коснулся:

Итак, для маски необходимо базовое изображение, отфильтрованное изображение и градиент:

let blendWithMask = CIFilter(name: "CIBlendWithMask")!


    self.blendWithMask.setValue(self.imageAccumulator.image(), forKey: kCIInputBackgroundImageKey)
    
    self.blendWithMask.setValue(pendingUpdate.filter.ciFilter.valueForKey(kCIOutputImageKey) as! CIImage,
        forKey: kCIInputImageKey)
    
    self.blendWithMask.setValue(self.gradientFilter.valueForKey(kCIOutputImageKey) as! CIImage,
        forKey: kCIInputMaskImageKey)

Наконец, я могу взять вывод этой смеси, переназначить ее на аккумулятор и создать UIImage для отображения на экране:

self.imageAccumulator.setImage(self.blendWithMask.valueForKey(kCIOutputImageKey) as! CIImage)

    let finalImage = UIImage(CIImage: self.blendWithMask.valueForKey(kCIOutputImageKey) as! CIImage)

В заключении

В качестве технической демонстрации  3D ReTouch  демонстрирует, как 3D Touch от Apple можно использовать для эффективного управления мощностью фильтра Core Image. Однако я подозреваю, что iPhone не идеальное устройство для этого — мои пухлые пальцы блокируют то, что происходит на экране. IPad Pro, с его Карандашем, был бы намного более подходящим устройством. Кроме того, имитация отдельного трекпада (аналогично глубокому нажатию на клавиатуре iOS) может работать лучше.

Как всегда, исходный код для этого можно найти в  моем репозитории GitHub здесь . Наслаждайтесь!