Статьи

Scribe: компонент распознавания рукописного ввода для iOS

С недавним прибытием моего  Apple Pencil  мои мысли превратились в почерк и поглаживание распознавания жестов. В Swift, DBPathRecognizer , уже есть отличная среда распознавания рукописного ввода  от  Didier Brun , но в моей версии используется другая техника. В то время как Дидье использует распознавание пути, мое больше похоже на сопоставление двух изображений.

Основным преимуществом моего подхода является то, что пользователь может построить письмо в любом порядке. Например, буква «Е» может быть сформирована путем рисования трех горизонтальных полос и вертикальной полосы в качестве отдельного элемента, она может быть сформирована путем рисования формы «С» и добавления отсутствующей центральной горизонтальной полосы, или она может быть сформирована путем рисования форма «L» и добавление двух других горизонтальных полос в виде буквы «C» с вертикальной частью, перекрывающей существующую вертикальную полосу. 

С другой стороны, эта свобода требует большего обучения. В моей  структуре Patterns вы можете увидеть  количество сэмплов, которые я взял, чтобы получить демо-версию. 

Кодирование пользовательского жеста

Мой   компонент ScribeView расширяет  UIView  и отвечает за кодирование штрихов пользователя и вызов метода для его  ScribeViewDelegate  после завершения жеста. Я использую стандартные функции  touchesBegan ()  и touchesMoved (),  чтобы вычислить ограничивающую рамку жеста и сохранить все местоположения касания (конечно, я использую  объединенные касания ):   

 for touch in coalescedTouches

    {

        let locationInView = touch.locationInView(self)



        strokePoints.append(locationInView)

        bezierPath.addLineToPoint(locationInView)



        minX = min(locationInView.x, minX)

        minY = min(locationInView.y, minY)



        maxX = max(locationInView.x, maxX)

        maxY = max(locationInView.y, maxY)



    }

Поскольку один жест может состоять из более чем одного касания, в  touchesEnded () я планирую таймер на 0,3 «. Если в течение этого периода начинается новое касание, этот таймер становится недействительным, и данные из нового касания добавляются к существующий жест.

Однако, если таймер завершает работу, я  вызываю handleGesture (),  и именно здесь происходят интересные вещи.

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

  let gestureWidth = abs(minX - maxX)
    let gestureHeight = abs(minY - maxY)

Я хочу, чтобы жест был определен в сетке 8 x 8, поэтому, когда  cellCount  определен как 8, я вычисляю размер ячеек на экране:

  let cellWidth = max(gestureWidth / CGFloat(cellCount - 1), 1)
    let cellHeight = max(gestureHeight / CGFloat(cellCount - 1), 1)

Затем создайте двумерный массив логических значений для хранения моих данных:

  var cells = [[Bool]](count: cellCount, repeatedValue: [Bool](count: cellCount, repeatedValue: false))

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

Причина, по которой я выбрал 8 x 8, состоит в том, что эти 64 булевых значения идеально вписываются в  UInt64,  который в основном является хешем для жеста и используется для сопоставления с моей библиотекой известных жестов в  шаблонах .  Таким образом, поскольку  клетка  представляет собой массив массивов, я использую  flatMap ()  ,  чтобы сгладить это тем  уменьшить ()  с битовым сдвигом , чтобы создать что большое целое число без знака , которое представляет массив в 8 х 8:

   let strokeResult = cells.flatMap({ return $0 }).reduce(UInt64(0))
    {
        ($0 << 1 | ($1 ? 1 : 0))
    }

Следующий шаг — сопоставить этот результат с моей библиотекой. Чтобы сделать это, я выполняю логическое «и» в зависимости  от результата удара пользователя и  UInt64 моих известных ударов и получаю подсчет населения, или  подсчет  этого. Использование уменьшения ()  снова, я выбираю элемент с самой высокой  popcount :

  let bestMatch = patterns.reduce((UInt64(0), UInt64(0), ""))

    {
        let popcount = ($1.0 & strokeResult).popcount()

        return popcount > $0.0 ? (popcount, $1.0, $1.1) : $0
    }

Чтобы помочь мне создать библиотеку, у меня есть функция  printToConsole (),  которая печатает ASCII-версию массива и его  значение UInt64 :

Вывод

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

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