Давайте также воспользуемся фреймворком 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».
Вы можете переключаться между двумя режимами, чтобы выбрать больше букв или просмотреть предложенные слова. Если нужного вам слова там нет, вы переключаетесь обратно [используя взгляд вверх, чтобы переключаться между ними] и произносите слово целиком.
Наконец, я использовал жест высунуть язык, чтобы переместить слово из одного из двух меню в строку предложения, в которой я прочитал то, что было сказано. Посмотрите видео здесь, чтобы увидеть все это в действии.
Вы можете скачать полный исходный код из этого суть. И спасибо за чтение.