Давайте также воспользуемся фреймворком SwiftUI Charts, чтобы лучше понять возвращаемые значения.

Бон, так что моя муза — создать несколько приложений для громкой связи, используя возможности FaceID высококлассных устройств, таких как iPhone 14 Pro и iPad Pro.

Я пробовал выполнять задачи здесь и здесь с некоторым успехом, но мне все равно казалось, что это просто слишком сложно и ненадежно. Я сделал шаг назад, поговорил с коллегой и решил копнуть глубже. Присоединяйтесь ко мне в этой газете, чтобы узнать, куда я отправился в своем путешествии.

Графики SwiftUI

Проблемы потерялись в данных — где-то спрятались. У меня просто было слишком много данных, поступающих на меня одновременно.

Когда я посмотрел на проблему, я понял, что могу использовать новую инфраструктуру SwiftUI Charts, чтобы помочь мне понять ее. Поэтому я настроил код для его построения.

Вы смотрите на первую диаграмму, которую я составил. У меня есть две смешанные формы для каждого глаза, положение которых я рисую в режиме реального времени, используя опубликованные переменные для отображения результатов. Вы можете видеть четкую закономерность, когда я смотрю влево или вправо.

Однако со столбцом leftOut что-то не так. Когда я пытаюсь смотреть в центр экрана, он все равно регистрирует значения; он падает только тогда, когда я смотрю правильно.

if (faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue) > 0.1 {
    looker.eyeLookInLeft = faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue
}
if (faceAnchor.blendShapes[.eyeLookInRight]!.doubleValue) > 0.1 {
    looker.eyeLookInRight = faceAnchor.blendShapes[.eyeLookInRight]!.doubleValue
}
if (faceAnchor.blendShapes[.eyeLookOutLeft]!.doubleValue) > 0.1 {
    looker.eyeLookOutLeft = faceAnchor.blendShapes[.eyeLookOutLeft]!.doubleValu
}
if (faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue) > 0.1 {
    looker.eyeLookOutRight = faceAnchor.blendShapes[.eyeLookOutRight]!.doubleValue
}

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

Это выглядело лучше, потому что я мог получить сигнал без шума, хотя на этот раз что-то происходило с каналом rightIn.

if (faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue) > 0.1 {
    looker.eyeLookInLeft = faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue
    looker.eyeLookInLeftPot += looker.eyeLookInLeft
}
if (faceAnchor.blendShapes[.eyeLookInRight]!.doubleValue) > 0.1 {
    looker.eyeLookInRight = faceAnchor.blendShapes[.eyeLookInRight]!.doubleValue
    looker.eyeLookInRightPot += looker.eyeLookInRight
}
if (faceAnchor.blendShapes[.eyeLookOutLeft]!.doubleValue) > 0.1 {
    looker.eyeLookOutLeft = faceAnchor.blendShapes[.eyeLookOutLeft]!.doubleValue
    looker.eyeLookOutLeftPot += looker.eyeLookOutLeft
}
if (faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue) > 0.1 {
    looker.eyeLookOutRight = faceAnchor.blendShapes[.eyeLookOutRight]!.doubleValue
    looker.eyeLookOutRightPot += looker.eyeLookOutRight
}

Я создал третью диаграмму с графиком, который теперь записывает ряд значений, наблюдаемых за X секунд. Я стал чувствовать себя увереннее. На этот раз я подумал, что смогу получить четкий сигнал, даже если в самом конце столбец «rightIn» снова зарегистрирует какие-то мошеннические значения.

Причина, по которой я не мог выполнить эту работу, начала обретать смысл; код, который я разработал, предполагал, что оба глаза будут работать с одинаковыми параметрами, но, глядя на графики, данные не подтверждают это предположение.

if (faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue) > 0.1 {
    looker.eyeLookInLeft = faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue
    looker.eyeLookInLeftPot += 1
}
if (faceAnchor.blendShapes[.eyeLookInRight]!.doubleValue) > 0.1 {
    looker.eyeLookInRight = faceAnchor.blendShapes[.eyeLookInRight]!.doubleValue
    looker.eyeLookInRightPot += 1
}
if (faceAnchor.blendShapes[.eyeLookOutLeft]!.doubleValue) > 0.1 {
    looker.eyeLookOutLeft = faceAnchor.blendShapes[.eyeLookOutLeft]!.doubleValue
    looker.eyeLookOutLeftPot += 1
}
if (faceAnchor.blendShapes[.eyeLookInLeft]!.doubleValue) > 0.1 {
    looker.eyeLookOutRight = faceAnchor.blendShapes[.eyeLookOutRight]!.doubleValue
    looker.eyeLookOutRightPot += 1
}

У меня был еще один вариант, который я хотел построить. Версия, в которой использовался угол, указанный в преобразованиях faceAnchor глаза, потому что он, казалось, мог обнаруживать небольшие изменения.

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

Хотя мне нужно было быть более осторожным, так как в портретном режиме левая/правая были бы осью X, а в ландшафтном — осью Y, и мне приходилось иметь дело также с положительными и отрицательными значениями. Смотри внимательно и ты увидишь ось на правом переключении.

Это было и не было полезным упражнением; Теперь я понял, почему было так больно получать достоверные результаты.

Выборка

Размышляя над задачей, я понял, что все мы используем своего рода курсор на экране в качестве механизма обратной связи. Я оглянулся на одну из первых созданных мной версий и скопировал код, чтобы создать теневой узел, который будет торчать перед моим лицом. Он работал нормально, но его движения были слишком быстрыми, слишком беспорядочными.

Затем я создал второй узел, положение которого обновлял только раз в секунду, используя координаты теневого узла. Я добавил анимацию, чтобы сгладить ситуацию. Я тут что-то напутал — вы уже видели результат — зеленую летучую рыбу в заголовке этой статьи.

Теперь у меня было быстрое движение по мановению взгляда, медленное движение за счет движения головы и медленное, но определенное, собирающее количество значений, зарегистрированных за X секунд, которые я отметил с помощью графиков. Игра началась, пришло время построить третью версию.

Перезагрузить

Я взял этот код из первой статьи в качестве отправной точки, а также работу, которую я только что обрисовал в общих чертах с faceAnchors.

Изображение, на которое вы смотрите, содержит 1220 вершин — магическое число индексов, которые ARKit использует для построения моделей для идентификации лиц.

Вершины, внутри которых есть некоторые фиксированные магические числа, подобные перечисленным здесь. [Я говорю набор; Я нашел их в этом посте SO и перепроверил, что они все еще верны, поэтому исправлено на данный момент, но, возможно, не завтра, будьте осторожны].

let mouthTopLeft = Array(250...256)
let mouthTopCenter = [24]
let mouthTopRight = Array(685...691).reversed()
let mouthRight = [684]
let mouthBottomRight = [682, 683,700,709,710,725]
let mouthBottomCenter = [25]
let mouthBottomLeft = [265,274,290,275,247,248]
let mouthLeft = [249]
let mouthClockwise : [Int] = mouthLeft +
                               mouthTopLeft + mouthTopCenter +
                               mouthTopRight + mouthRight +
                               mouthBottomRight + mouthBottomCenter +
                               mouthBottomLeft
let eyeTopLeft = Array(1090...1101)
let eyeBottomLeft = Array(1102...1108) + Array(1085...1089)
let eyeTopRight = Array(1069...1080)
let eyeBottomRight = Array(1081...1084) + Array(1061...1068)
let nose = [9]
let leftEye = [1064]
let rightEye = [42]
let mouth = [24,25]
let forehead = [20]

Я буду использовать вершины для привязки маркера к центру моего экрана — раньше я использовал ноль, но использование faceAnchor работает лучше, потому что оно динамическое.

Затем я создал виртуальный шар, плавающий перед моим носом. Я также связываю ориентацию faceAnchor с уравнением.

Идея состоит (или была) в том, что пользователю нужно держать поплавок перед носом, чтобы iPhone точно выравнивался. Выравнивание является ключом к эффективной работе отслеживания взгляда.

func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
    let device = trackingView.device
   
    faceGeometry = ARSCNFaceGeometry(device: device!)
    faceNode = SCNNode(geometry: faceGeometry)
    faceNode.geometry?.firstMaterial?.fillMode = .lines
    faceNode.geometry?.firstMaterial?.diffuse.contents = UIColor.white.withAlphaComponent(0.75)
    
    faceNode.addChildNode(sphereNode)
    return faceNode
}

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
  
  faceGeometry.update(from: faceAnchor.geometry)
}

Упомянутый здесь sphereNode был ничем иным, как простой сферой. Но это было только начало; Настоящая головоломка, которую мне нужно было реализовать, заключалась в том, чтобы попытаться стабилизировать/сделать выбор менее трудоемким.

После некоторых фальстартов я реализовал нечто совершенно другое. Эта отслеживающая версия долго кто-то смотрел в ту или иную сторону, а не сколько долго смотрели в ту или иную сторону. так или иначе.

if (looked.gazeX > 0.05 && !looked.paused && !looked.outOfBounds && looked.spell) {
    DispatchQueue.main.async { [self] in
        Task { await looks.addShape(faceSeen: looked.gazeX) }
    }
}

Взгляды

Бон — чтобы он реагировал как можно быстрее, я также отслеживал и сообщал о взгляде в обратном вызове рендеринга, хотя это означает, что я получаю дюжину или более хитов, когда я поднимаю взгляд влево, вправо, вверх или вниз. Сохранение указанных ответов «актеру», чтобы сохранить чистоту.

actor Looks: NSObject {
    static var shared = Looks()

    func addShape(faceSeen:Float) {
        gazesX.append(faceSeen)
    }

    func rightShapes() -> Int {
        let gazesSeen = gazesX.filter( { $0 > 0 } )
        return gazesSeen.count
    }
    
    func leftShapes() -> Int {
        let gazesSeen = gazesX.filter( { $0 < 0 } )
        return gazesSeen.count
    }
    
    func resetShapes() {
        gazesX.removeAll()
    }
}

Затем я проверяю количество взглядов, зарегистрированных через заданные промежутки времени, чтобы измерить, как долго чей-то взгляд смотрит в ту или иную сторону. После этого я обновляю интерфейс SwiftUI. Проверка, которую я делал каждую секунду или около того с обратным вызовом рендеринга.

if looksRight > 16 {
    self.looked.vindex += 1
    self.looked.vindex = self.looked.vindex % vowels.count
}

Но, как вы обнаружите, если вы попробуете использовать приложение — расшифровывать слова буква за буквой может быстро стать очень утомительным. Мне нужно было больше — я хотел реализовать что-то вроде приложения WhatsApp, когда вы печатаете текст.

Я сделал это, используя простой список самых популярных слов на английском языке в сочетании с простым регулярным выражением, чтобы вернуть список находок на основе введенных букв, поэтому вы вводите «t», вы получаете все записи с «t»; введите «th», и список будет перестроен только с элементами, начинающимися с «th».

Вы можете переключаться между двумя режимами, чтобы выбрать больше букв или просмотреть предложенные слова. Если нужного вам слова там нет, вы переключаетесь обратно [используя взгляд вверх, чтобы переключаться между ними] и произносите слово целиком.

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

Вы можете скачать полный исходный код из этого суть. И спасибо за чтение.