Программные средства для заполнения базы персональных данных

  • Вид работы:
    Дипломная (ВКР)
  • Предмет:
    Информационное обеспечение, программирование
  • Язык:
    Русский
    ,
    Формат файла:
    MS Word
    1,69 Мб
  • Опубликовано:
    2017-07-10
Вы можете узнать стоимость помощи в написании студенческой работы.
Помощь в написании работы, которую точно примут!

Программные средства для заполнения базы персональных данных

СОДЕРЖАНИЕ

автоматизированный ввод база данные

ВВЕДЕНИЕ

. ПОСТАНОВКА ЗАДАЧИ

Общее описание разрабатываемой системы

. АНАЛИТИЧЕСКИЙ ОБЗОР

.1 Теоретические основы

. ПРОЕКТИРОВАНИЕ

.1 Диаграмма вариантов использования

.2 Архитектура приложения

.3 Выбор инструментальных средств разработки

.4 Проектирование структур данных и алгоритмов

.5 Проектирование пользовательского интерфейса

. РЕАЛИЗАЦИЯ

.1 Создание проекта

.2 Операция выбора режима

.3 Операция распознавания

.4 Корректировка и подтверждение распознанных данных

.5 Механизм аутентификации

.6 Просмотр документов

.7 Обучение Tesseract

.8 Сборка приложения

. ТЕСТИРОВАНИЕ

.1 Обоснование методики тестирования

.2 Результаты тестирования

ЗАКЛЮЧЕНИЕ

СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ

ПРИЛОЖЕНИЯ

ВВЕДЕНИЕ


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

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

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

Цель данной ВКР - исследование возможностей ускорения процессов заполнения базы персональных данных. Это может быть достигнуто за счет сокращения ручного ввода персональных данных путем применения технологий оптического распознавания символов.

Данное исследование выполняется по предложению руководителя отдела разработки АО «Сбебанк-Технологии».

В ВКР решены следующие задачи:

. Аналитический обзор существующих средств для автоматического заполнения базы персональных данных

. Проектирование автоматизированной системы

. Реализация системы

. Тестирование системы

. Проведен эксперимент по занесению персональных данных с помощью разработанной системы

Результаты работы докладывались на международной студенческой конференции «Молодые исследователи - регионам».

1. ПОСТАНОВКА ЗАДАЧИ


Одной из задач ввода формализованных документов является индексирование - присвоение каждому документу набора атрибутов с возможностью автоматического ввода индексных полей с помощью распознавания. На рисунке 1 показаны основные этапы ввода формализованных документов [1].

Рисунок 1 - Схема системы ввода формализованных документов

После контроля данные отправляются в базу данных. Таким образом, результатом ввода форм чаще всего является не сам документ в исходном виде, а определенная запись в некоторой базе данных. Технологии распознавания печатных символов обозначаются термином оптическое распознавание символов (англ. optical character recognition, OCR). Задачи распознавания при вводе форм не обязательно связаны с распознаванием текста. При вводе форм может потребоваться распознавание различных меток и знаков, для которого тоже существует свой термин: OMR (Optical Mark Recognition).

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

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

Общее описание разрабатываемой системы

Разрабатываемая система будет представлять собой приложение по ОС Android. Она должна предоставлять возможность автоматического ввода данных документов, удостоверяющих личность, а именно паспортов и страховых свидетельств государственного пенсионного страхования.

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

Анализируя функции системы можно выделить следующие:

.        Регистрация и аутентификация пользователя

.        Получение изображения документа при помощи цифровой фотокамеры

.        Распознавание данных документа

.        Возможность корректировки распознанных данных

.        Сохранение данных документа в БД

.        Возможность просмотра ранее внесенных документов

.        Возможность удаления ранее внесенных документов

2. АНАЛИТИЧЕСКИЙ ОБЗОР

Аналитический обзор существующих решений по распознаванию данных документов

Поиск существующих свободно распространяемых решений, специализированных именно на распознавании документов, удостоверяющих личность, не дал результатов. Поэтому был проведен анализ продуктов, предлагающих возможности OCR, которые предлагают API или SDK для использования в собственном проекте [2]. Список ПО, удовлетворяющего условиям свободного распространения и наличия SDK, получился следующим: Tesseract, Puma.NET, CuneiForm, MeOCR, FreeOCR, Ocrad.

. Tesseract

Наиболее популярная OCR-библиотека, разрабатываемая компанией Google. Поддерживает более 100 языков. Написана на языке C++, однако имеет обертку (wrapper) для языка Java.

. Puma.NET

Обертка для CuneiForm для платформы .NET, следовательно, не может быть использована в приложении для ОС Android.

. CuneiForm

Разработанная в России система оптического распознавания символов. Поддерживает 28 языков, в том числе русский. Последняя версия выпущена в 2011 году. Не предоставляет интерфейса для Java.

. MeOCR

Не поддерживается с 2012 года. Средства разработки доступны только для платформы .NET

5. FreeOCR

Оболочка для Microsoft OCR Library. Может быть использована только на устройствах под управлением ОС Windows.

. Ocrad

Написанная на C++ OCR-библиотека. Поддерживает только латинский алфавит, вследствие чего не может быть использована в проекте.

Таким образом, в качестве средства распознавания, наиболее подходит библиотека Tesseract. Однако изображение перед передачей на распознавание в Tesseract необходимо обработать: произвести фильтрацию и выделить блоки текста [3].

Существуют комплексные системы оптического распознавания символов, такие как ABBYY FineReader, CuneiForm и т.д., однако их использование в данном проекте не представляется возможным в связи с невозможностью или сложностью интеграции и громоздкостью пакетов.

 

2.1 Теоретические основы


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

Процесс распознавания текста можно разбить на три этапа:

.        предварительная фильтрация и подготовка изображения;

.        логическая обработка результатов фильтрации;

.        алгоритмы принятия решений на основе логической обработки.

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

 

.1.1 Предварительная фильтрация и подготовка изображения

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

Операции, выполняемые на этапе фильтрации:

. Преобразование цветного изображения в оттенки серого для упрощения дальнейшей обработки. Пример приведен на рисунке 2.

 

Рисунок 2 - Преобразование изображения в оттенки серого

. Сглаживание изображения - выполняется в целях уменьшения шума. Для выполнения операции сглаживания к изображению применяется фильтр. Пример сглаживания изображен на рисунке 3.

 

Рисунок 3 - Сглаживание изображения фильтром Гаусса

Существует множество фильтров, но наиболее часто используются следующие:

) Размытие по рамке (box blur)

Простейший из фильтров. Каждый выходной пиксель равен среднему арифметическому соседних пикселей. Матрица свертки:


) Размытие по Гауссу (Gaussian blur)

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

Рисунок 4 - Функция свертки размытия по Гауссу

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

) Медианный фильтр (Median blur)

Заменяет каждый пиксель медианным значением соседних пикселей.

) Двустороннее размытие (Bilateral blur)

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

Так же, как и фильтр Гаусса, двусторонний фильтр ставит в соответствие каждому соседнему пикселю определенный коэффициент. Эти коэффициенты имеют две составляющих: первая соответствует коэффициенту фильтра Гаусса, а вторая учитывает разницу в интенсивности между соседними пикселями и тем, который рассчитывается.

. Выделение границ

Для выделения символов на окружающем фоне применяют методы выделения признаков, в частности - методы выделения границ.

Выделение границ - метод для поиска границ объекта в пределах изображения. Он нацелен на поиск точек изображения, где резко меняется яркость.

Существует множество подходов к выделению границ, но практически все можно разделить на две категории: методы, основанные на поиске максимумов, и методы, основанные на поиске нулей. Методы, основанные на поиске максимумов, выделяют границы с помощью вычисления «силы края», обычно выражения первой производной, такого как величина градиента, и затем поиска локальных максимумов силы края, используя предполагаемое направление границы, обычно перпендикуляр к вектору градиента. Методы, основанные на поиске нулей, ищут пересечения оси абсцисс выражения второй производной, обычно нули лапласиана или нули нелинейного дифференциального выражения [5].

) Оператор Собеля

Широко известным и довольно простым фильтром выделения границ является оператор Собеля. Он вычисляет приближенное значение градиента яркости изображения. Оператор Собеля использует две матрицы свертки для вычисления приблизительных значений производных по горизонтали и по вертикали изображения.

Если исходное изображение обозначить A, а изображения, где каждая точка соответствует приближенному значению производной в горизонтальном и вертикальном направлениях -  и  соответственно, то изображения производных будут рассчитываться следующим образом:


где  - оператор свертки.

Координата x здесь возрастает «вправо», а y - «вниз». Значение градиента в каждой точке может быть вычислено, используя:


Имея эту информацию, мы также можем вычислить направление градиента:


Пример использования оператора Собеля изображен на рисунке 5.

 

Рисунок 5 - Пример использования оператора Собеля

) Оператор Кэнни

Более широкое применение нашел оператор Кэнни. Он использует многоступенчатый алгоритм для обнаружения широкого спектра границ в изображении. Хотя он был разработан на заре компьютерного зрения, оператор обнаружения границ Кэнни до сих пор является одним из лучших детекторов. Кроме особенных частных случаев трудно найти детектор, который бы работал существенно лучше, чем детектор Кэнни [6].

Алгоритм оператора Кэнни состоит из следующих шагов:

·        Применение фильтра Гаусса для снижения шума.

·        Поиск градиентов (выполняется аналогично оператору Собеля). Направление градиента округляется до 0, 45, 90 или 135 °.

·        Подавление немаксимумов - удаляются пиксели, которые скорее всего не являются частью границы. Остаются только тонкие линии.

·        Двойная пороговая фильтрация и трассировка области неоднозначности. Задаются два пороговых значения (верхнее и нижнее), пиксель, градиент которого выше верхнего порога, принимается как граница, пиксель с градиентом ниже нижнего порога - отклоняется, пиксель с градиентом, находящимся между пороговыми значениями принимается как граница, только в случае, если рядом с ним находится пиксель с градиентом выше верхнего порога. Кэнни рекомендует, чтобы соотношение верхний порог/нижний порог находилось между 2 и 3. Пример использования оператора Кэнни приведен на рисунке 6.

 

Рисунок 6 - Пример использования оператора Кэнни

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

. Морфологические операции

На заключительном этапе предварительной обработки символы должны быть объединены в блоки текста. Это может быть достигнуто путем применения морфологических операций. Рассмотрим 3 морфологических операции, которые необходимы при выполнении данного этапа: наращивание, эрозия, замыкание.

) Наращивание (dilation):

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

Проходя каждый пиксель изображения, находится максимальное значение среди значений, перекрываемых матрицей свертки пикселей и значение текущего пикселя (расположенного в центре матрицы свертки) заменяется этим значением. Вследствие этого яркие области изображения «растут». Пример использования операции наращивания приведен на рисунке 7.

 

Рисунок 7 - Пример использования операции наращивания

) Эрозия (erosion)

Эта операция обратна операции наращивания: вместо максимального значения среди значений, перекрываемых матрицей свертки пикселей, ищется минимальное значение. Это приводит к тому, что яркие области изображения становятся меньше, в то время как темные - «растут». Пример использования операции эрозии приведен на рисунке 8.

 

Рисунок 8 - Пример использования операции эрозии

) Замыкание (closing)

Является последовательным выполнением операций наращивания и эрозии.

Операция замыкания «закрывает» небольшие внутренние «дырки» в изображении, и убирает углубления по краям области. Если к изображению применить сначала операцию наращивания, то мы сможем избавиться от малых дыр и щелей, но при этом произойдёт увеличение контура объекта. Избежать этого увеличения позволяет операция эрозия, выполненная сразу после наращивания с тем же структурным элементом [7]. Пример использования операции замыкания приведен на рисунке 9.

 

Рисунок 9 - Пример использования операции замыкания

 

2.1.2 Логическая обработка результатов фильтрации

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

Контур - кривая, огибающая непрерывные блоки пикселей, имеющих одинаковый цвет и интенсивность.

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

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

OpenCV имеет модуль обработки изображений imgproc, который включает весь спектр операций, описанных выше. Несмотря на то, что библиотека реализована на языке C++, она имеет обертку (wrapper) для языка Java, на котором ведется разработка под ОС Android, что избавляет от необходимости ручной настройки взаимодействия с кодом, написанным на C++, используя интерфейс JNI. Кроме обертки, поддерживаемой командой OpenCV, в которой отсутствует приличная часть интерфейсов, существует обертка JavaCPP, разрабатываемая командой Bytedeco.

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

2.1.3 Алгоритмы принятия решений на основе логической обработки

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

Распознавание символов может быть реализовано средствами OpenCV, например, при помощи алгоритма k ближайших соседей:

Алгоритм k ближайших соседей - один из простейших алгоритмов классификации, доступных для обучения с учителем. Основным принципом метода ближайших соседей является то, что объект присваивается тому классу, который является наиболее распространённым среди соседей данного элемента. Соседи берутся исходя из множества объектов, классы которых уже известны, и, исходя из ключевого для данного метода значения k высчитывается, какой класс наиболее многочислен среди них. Каждый объект имеет конечное количество атрибутов (размерностей). Пример классификации k ближайших соседей приведен на рисунке 10.

 

Рисунок 10 - Пример классификации k ближайших соседей. Тестовый образец (зеленый круг) должен быть классифицирован как синий квадрат (класс 1) или как красный треугольник (класс 2). Если k = 3, то она классифицируется как 2-й класс, потому что внутри меньшего круга 2 треугольника и только 1 квадрат. Если k = 5, то он будет классифицирован как 1-ый класс (3 квадрата против 2ух треугольников внутри большего круга).

Однако процессы разработки механизма распознавания и обучения алгоритма могут быть упрощены при использовании библиотеки оптического распознавания символов Tesseract. Преимущество Tesseract заключается в максимальной автоматизации процесса обучения шрифту и простоте его использования. Tesseract так же, как и OpenCV, имеет обертку в JavaCPP, дающую возможность использования его в проекте.

Распознанный в ходе предыдущих этапов текст будет использован для построения текстового представления документа и выведен на экран устройства для подтверждения корректности распознавания пользователем.

3. ПРОЕКТИРОВАНИЕ

3.1 Диаграмма вариантов использования


На основе технического задания была построена диаграмма вариантов использования. Она изображена на рисунке 11.

 

Рисунок 11 - Диаграмма вариантов использования разрабатываемой системы

 

3.2 Архитектура приложения


Все взаимодействие пользователей с приложениями в ОС Android построено на операциях (activity, активность). Операция - это компонент приложения, который выдает экран, и с которым пользователи могут взаимодействовать для выполнения каких-либо действий, например, набрать номер телефона, сделать фото, отправить письмо или просмотреть карту [8]. С программной точки зрения операция представляет Java-класс, наследующийся от класса android.app.Activity. С каждым activity должен быть ассоциирован определенный макет. Макет представляет собой XML-файл, в котором определена визуальная структура пользовательского интерфейса [9].

Архитектура приложения представлена на рисунке 12.

 

Рисунок 12 - Архитектура приложения

 

3.3 Выбор инструментальных средств разработки


Разработка приложения будет выполняться в интегрированной среде разработки Android Studio 2.3.2 - она является официально поддерживаемым компанией Google средством разработки под ОС Android.

Для обучения Tesseract предлагается официальная консольная утилита. Однако в таком случае, BOX-файлы необходимо вручную составлять в текстовом редакторе. Поэтому удобнее будет воспользоваться сторонними визуальными редакторами BOX-файлов. На ОС Windows может быть использован редактор jTessBoxEditor запускаемый на Java [10].

3.4 Проектирование структур данных и алгоритмов

 

.4.1 Схема БД

База данных приложения будет состоять из трех таблиц:

. USER - пользователи. Столбцы:

_ID - идентификатор,

USERNAME - имя пользователя,

PASSWORD - хеш-сумма пароля;

. DOCUMENT_TYPE - типы документов. Столбцы:

_ID - идентификатор,

CODE - код (например, PASSPORT),

NAME - локализованное наименование;

. DOCUMENT -документы.

_ID - идентификатор,

DOC_TYPE_ID - ID типа документа,

USER_ID - ID владельца документа,

DATA - данные документа

Схема БД представлена на рисунке 13.

 

Рисунок 13 - Схема БД

3.4.2 Алгоритмы

Основным и наиболее сложным алгоритмом в приложении является обработка изображения предпросмотра, которая включает в себя обнаружение и в случае необходимости распознавание полей. Алгоритмы для документов типа паспорт и страховое свидетельство отличаются только в том, что для паспорта необходимо учитывать вертикальное расположение серии и номера паспорта. В связи с этим целесообразно рассмотреть алгоритм для паспорта, как более сложный. Блок-схема алгоритма представлена на рисунке 14.

Рисунок 14 - Блок-схема алгоритма обработки изображения предпросмотра

Алгоритм подпрограммы обнаружения полей представлена на рисунке 15.

Рисунок 15 - Алгоритм подпрограммы обнаружения полей

Алгоритм подпрограммы распознавания текста в полях представлен на рисунках 16, 17.

Рисунок 16 - Алгоритм подпрограммы распознавания текста

Рисунок 17 - Алгоритм подпрограммы распознавания текста

3.5 Проектирование пользовательского интерфейса


Проектирование пользовательского интерфейса будет произведено путем создания макетов в Android Studio.

Макет формы аутентификации представлен на рисунке 18.

Рисунок 18 - Макет формы аутентификации

Макет формы выбора режима представлен на рисунке 19.

 

Рисунок 19 - Макет формы выбора режима

Макет формы списка документов представлен на рисунке 20.

 

Рисунок 20 - Макет формы списка документов

Макет формы верификации и просмотра документа представлен на рисунке 21.

 

Рисунок 21 - Макет формы верификации и просмотра документа

Макет операции распознавания содержит единственный компонент - окно предпросмотра, поэтому представление его здесь не имеет смысла.

4. РЕАЛИЗАЦИЯ

 

.1 Создание проекта


Первым делом был создан проект в Android Studio. Минимальным необходимым для установки приложения уровнем API выбран уровень 15, поскольку с этого уровня добавлена поддержка непрерывного автофокуса, который будет необходим в приложении, и в тоже время обеспечивает поддержку приложений на устройствах с ОС Android 4.0 и выше, что на момент разработки ВКР покрывает 97,4 % используемых устройств на ОС Android [11].

4.2 Операция выбора режима


Сперва необходимо создать операцию средствами Android Studio. Для нее автоматически будет создан пустой макет. Класс операции будет иметь имя OperationActivity, а сгенерированный файл макета - соответственно, activity_operation.xml.

Визуальные компоненты в Android называются представлениями (view), и их базовым классом является android.view.View. Роль данной операции заключается в предоставлении пользователю выбора между режимами работы: внесением нового документа и просмотром существующего. Возможность выбора может быть реализована при помощи переключателя, который в Android представлен компонентом RadioButton. Несколько переключателей, из которых может быть выбран один, должны быть помещены в представление RadioGroup. Таким образом, на макет необходимо добавить RadioGroup с двумя вложенными RadioButton, подписанными как «Внесение документа» и «Просмотр документа».

Визуальные компоненты в Android называются представлениями (view), и их базовым классом является android.view.View. Роль данной операции заключается в предоставлении пользователю выбора между режимами работы: внесением нового документа и просмотром существующего. Возможность выбора может быть реализована при помощи переключателя, который в Android представлен компонентом RadioButton. Несколько переключателей, из которых может быть выбран один, должны быть помещены в представление RadioGroup. Таким образом, на макет необходимо добавить RadioGroup с двумя вложенными RadioButton, подписанными как «Внесение документа» и «Просмотр документа».

В случае выбора пункта «Внесение документа», пользователь также должен указать тип документа. Это может быть реализовано в рамках той же операции. Для этого на макет добавлен еще один RadioGroup с двумя опциями - «Паспорт» и «Страховое свидетельство». Изначально переключатель выбора типа документа должен быть скрыт и отображаться только при выборе режима «Внесение документа».

Последним представлением на данном макете будет кнопка «Продолжить». Она также должна быть скрыта по умолчанию и отображаться только при выбранной операции и при необходимости типа документа. Листинг макета представлен в приложении 3.

За обработку событий взаимодействия с операцией отвечает класс OperationActivity. Соответственно, в нем будет реализовано три метода: для события выбора опций из двух групп и для события нажатия на кнопку продолжения. Листинг операции представлен в приложении 9.

Метод обработки событий выбора режима управляет видимостью переключателей выбора типа документа и кнопки «Продолжить».

Метод обработки событий выбора типа документа управляет только видимостью кнопки «Продолжить».

Метод, вызываемый при нажатии кнопки продолжения вызывает одну из двух операций в зависимости от выбора пользователя. Запускаемая операция описывается при помощи намерения (intent), в котором указывается требуемая операция. Также в намерение могут быть помещены дополнительные данные (extras) для использования в вызываемой операции. В данном случае при выборе режима внесения нового документа будут добавлены дополнительные данные, содержащие тип документа. Операция будет запущена только в случае наличия разрешения на использование камеры. Для этого в файл манифеста AndroidManifest.xml необходимо добавить соответствующее разрешение (android.permission.CAMERA). Листинг файла AndroidManifest.xml представлен в приложении 1.

 

4.3 Операция распознавания


Большая часть логики приложения будет реализована в операции RecognitionActivity. Именно эта операция отвечает за получение изображения документа с камеры и распознавания данных. Прежде чем реализовать логику взаимодействия с камерой и с библиотеками OpenCV и Tesseract, необходимо подключить к проекту необходимые библиотеки в конфигурации Gradle (система автоматической сборки, используемая в проектах для Android). Для этого в секцию dependencies файла build.gradle в модуле app необходимо добавить следующие зависимости (листинг файла build.gradle приведен в приложении 23):

compile group: 'org.bytedeco', name: 'javacv', version: '1.3.2'group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '3.2.0-1.3', classifier: 'android-arm'group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '3.2.1-1.3', classifier: 'android-arm'group: 'org.bytedeco.javacpp-presets', name: 'tesseract', version: '3.04.01-1.3'group: 'org.bytedeco.javacpp-presets', name: 'tesseract', version: '3.04.01-1.3', classifier: 'android-arm'group: 'org.bytedeco.javacpp-presets', name: 'leptonica', version: '1.73-1.3', classifier: 'android-arm'

Библиотека org.bytedeco.javacv содержит утилитные классы, а также зависимости от org.bytedeco.javacpp, содержащей основные типы, применяемые при взаимодействии с нативным C++-кодом (например, класс BytePointer - представление указателя на тип char) и от предустановленных в JavaCPP библиотек, в том числе OpenCV.

Библиотека org.bytedeco.javacpp-presets.opencv и остальные в приведенном списке, имеющие классификатор android-arm содержат непосредственно C++-библиотеки, скомпилированные для процессоров с архитектурой ARM.

После подключения библиотек можно приступить к реализации операции распознавания данных документа. Для этого в первую очередь необходимо реализовать получение изображения с камеры. В репозитории примеров команды Bytedeco имеется проект javacv-android-camera-preview [12] с представлением CvCameraPreview - окном предпросмотра с возможностью обработки изображения, оперируя объектом класса opencv_core.Mat (Mat - центральный класс в библиотеке OpenCV. Он является контейнером для изображения и состоит из двух частей: заголовка матрицы и указателя на матрицу, содержащую значения пикселей [13]). Все что нужно - это реализовать интерфейс CvCameraPreview.CvCameraViewListener.

Таким образом, на макете операции RecognitionActivity необходимо расположить представление CvCameraPreview, и установить ему атрибуты app:camera_type="back" для выбора задней камеры и app:scale_type="fit" для того, чтобы изображение предпросмотра полностью вмещалось в окно. Листинг макета представлен в приложении 4, а операции - в приложении 12.

Всю логику обработки, обнаружения и распознавания можно вынести в отдельные сервисные классы. Общий для всех типов документов функционал будет расположен в базовом абстрактном классе BaseDocRecognitionService, в то время как подклассы будут реализовать функционал специфичный для документа. В базовом классе достаточно единственного публичного метода - processPreview(), который принимает параметром изображение с камеры и возвращает изображение для показа в окне предпросмотра. Стоит отметить, что разрешение исходного изображения ограничено 640 пикселями в обоих измерениях для сохранения приемлемой скорости отрисовки кадров в связи с высокой сложностью алгоритма. Как было сказано в разделе 3, операция получения изображения предпросмотра должна состоять из обнаружения полей документа и выделении их на изображении предпросмотра, следовательно, можно выделить два соответствующих метода - detect() и drawFields(). Листинг класса BaseDocRecognitionService представлен в приложении 13, реализаций PassportRecognitionService и SnilsRecognitionService - в приложениях 14 и 15 соответственно. Тип документа устанавливается перечислением DocumentType, листинг которого приведен в приложении 22.

 

4.3.1 Описание алгоритма метода обнаружения полей

Поскольку документы имеют разный размер и соотношение сторон, логично перед дальнейшей работой с изображением обрезать его до требуемого размера. Для этого объявим абстрактный метод getRatio(), который будет возвращать величину соотношения сторон для конкретного типа документа - например, 88/125 для паспорта [14]. Размер изображения документа в пикселях рассчитывается в конструкторе на основе этого соотношения и разрешения изображения предпросмотра. Затем исходное изображение обрезается под требуемый формат используя метод Mat.apply(). Метод apply() создает новый объект Mat с указателем на часть исходной матрицы. После этого над обрезанным изображением производятся следующие операции:

. Конвертация изображения из цветового пространства RGBA в оттенки серого.

Для этой операции применяется метод opencv_imgproc.cvtColor, принимающий 3 параметра: исходное изображение, преобразованное изображение и код конвертации, определяющий цветовые пространства входного и выходного изображений. Второй параметр может принимать ссылку на тот же объект, что и передается в качестве входного изображения. В таком случае на уровне C++ все равно будет создана новая матрица и данные исходного изображения не будут затронуты. В качестве кода операции в данном случае используется CV_RGBA2GRAY.

. Выделение границ

Эту задачу выполняют реализации абстрактного метода BaseDocRecognitionService.findEdges(). В реализациях для обоих типов документов вызывается единственный метод - opencv_imgproc.Canny() (оператор Кэнни). Он принимает в качестве параметров ссылки на исходное изображение, выходное бинарное изображение с выделенными границами, величину нижнего порога, величину верхнего порога, размер матрицы свертки для оператора Собеля и признак, указывающий на то, должна ли быть использована более точная формула расчета градиента  вместо . Величины нижнего, верхнего порога и размера матрицы свертки зависят от цветов фона и шрифта документа. Они подобраны опытным путем, учитывая рекомендацию Кэнни по отношению величины верхнего порога к нижнему находящемуся в пределах (2; 3), и принимают следующие значения:

, 2000, 5 - для паспорта;

, 250, 3 - для страхового свидетельства.

Стоит отметить, что при увеличении размера матрицы свертки, величины порогов также должны быть увеличены.

3. Применение к изображению морфологических операций

Морфологические операции выполняются в методе BaseDocRecognitionService.applyMorphology(). Реализация этого метода в сервисе для паспорта следующая:

Изображение разбивается на левую и правую части, таким образом, чтобы в левой части находился текст, расположенный горизонтально, а в правой - вертикально, то есть серия и номер паспорта. К левой и правой частям изображения применяется операция замыкания с целью объединения символов и слов в единые блоки белого цвета (значения пикселей - 255). В левой части части матрица свертки имеет больший размер по горизонтали, в то время как в правой - по вертикали. Размер матрицы свертки, а также интервалы столбцов для частей изображения с горизонтальным и вертикальным текстом устанавливаются в конструкторе, чтобы снизить затраты процессорного времени. После операции замыкания, к изображению применяется операция размыкания. Ее роль заключается в том, чтобы убрать с изображения шумы и отсеять текст с мелким шрифтом.

Используемые в этом методе переменные рассчитываются в конструкторе следующим образом:

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

. Выделение полей документа

Этот метод, BaseDocRecognitionService.detectFields(), реализован в базовом классе. На вход он принимает бинарное изображение с выделенными белым цветом блоками текста.

В первую очередь находятся контуры блоков текста. Для этого применяется метод opencv_imgproc.findContours(input, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE), где input - входное бинарное изображение, contours - найденные контуры, RETR_EXTERNAL - режим поиска контуров, при котором находятся только внешние контуры, а внутренние, находящиеся во внешних, игнорируются, CHAIN_APPROX_NONE - аппроксимация контуров отключена, каждому пикселю соответствует одна точка контура.

Затем для каждого найденного контура выполняется: аппроксимация контура при помощи метода approxPolyDP(), в числе аргументов которого следует отметить epsilon, как величину характеризующую точность аппроксимации и равную максимальной дистанции между исходным и результирующим контуром, и closed, как признак того, нужно ли замыкать крайние точки контура. В нашем случае epsilon можно принять равным 3, а closed = true.

Следующим шагом полученные контуры вписываются в прямоугольники методом opencv_imgproc.boundingRect(). Полученные прямоугольники увеличиваются в размерах на 3 пикселя в каждом направлении, чтобы скомпенсировать возможную ошибку при выполнении предыдущих этапов. Далее этим прямоугольникам ставятся в соответствие поля документа. Эту функцию выполняет метод getFieldCorrespondingTo. Его описание будет приведено позднее.

Для поля документа необходимо создать интерфейс DocumentField с двумя методами: getName(), возвращающим название поля и getRelativeSearchingArea(), возвращающим область, в которой может находиться это поле в долях от ширины и высоты документа. Листинг интерфейса DocumentField приведен в приложении 16. Данный интерфейс будет реализован перечислениями PassportField и SnilsField. Их листинги приведены в приложениях 17 и 18 соответственно. В базовом классе имеется поле fieldsSearchingAreas() типа Map<DocumentField, Rect> - ассоциативный массив, ключом которого является поле документа, а значением - прямоугольник, в пределах которого это поле может располагаться на изображении. Этот массив инициализируется в конструкторе класса через вызов метода calculateSearchingAreas(), который преобразует относительные координаты областей поиска полей в абсолютные координаты, соответствующему конкретному разрешению изображения предпросмотра. Метод getFieldCorrespondingTo() принимает в качестве параметра прямоугольник с распознанным текстом и проходя по записям в ассоциативном массиве находит поле, которому принадлежит этот прямоугольник

Наконец, поле DocumentField и соответствующий ему прямоугольник Rect добавляются в Multimap - ассоциативный массив, допускающий соответствие одному ключу нескольких значений. Мультиотображение необходимо, потому что одно поле может содержать несколько строк текста, а при обнаружении одной строке текста соответствует один прямоугольник. Multimap и его реализации, такие как ArrayListMultimap, присутствуют в библиотеке Google Guava. Чтобы подключить эту библиотеку в файл build.gradle необходимо добавить зависимость:

compile 'com.google.guava:guava:22.0-android'

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

fields.containsAll(getAllFields());

Где getAllFields() - абстрактный метод, возвращающий список всех полей для данного типа документа.

 

4.3.2 Отрисовка прямоугольников для предпросмотра

В случае если не все необходимые поля были обнаружены, нужно визуализировать на исходном изображении прямоугольники с распознанными полями, а также прямоугольники, в которых производится поиск этих полей. Для этого создан метод visualizeRects(), выполняющий обрезку исходного изображения до размеров документа, рассчитанных в конструкторе класса, отрисовку на изображении найденных на прошлом шаге прямоугольников и прямоугольников, в которых должны располагаться соответствующие поля, и восстановление изображения до исходного размера с заполнением краев белым цветом. Эти действия вынесены в отдельные методы, из алгоритмов которых стоит отдельно выделить операции opencv_imgproc.rectangle(), служащую непосредственно для отрисовки на изображении прямоугольника определенного цвета и Mat.copyTo(), копирующую одну матрицу (изображение) в другую. В данном случае производится копирование обрезанного изображения в центральную часть полноразмерного изображения. Эта операция является заключительной в процессе обнаружения полей.

 

4.3.3 Интеграция операции RecognitionActivity с сервисом PassportRecognitionService

Как было упомянуто ранее, для осуществления взаимодействия с представление CvCameraPreview необходимо реализовать интерфейс слушателя CvCameraViewListener. Он содержит 3 метода: onCameraViewStarted(), вызываемый, при запуске предпросмотра, onCameraViewStopped() - при его остановке и onCameraFrame() - при получении очередного кадра. Наибольший интерес представляют методы onCameraViewStarted() и onCameraFrame().

В методе onCameraViewStarted() можно произвести инициализацию сервиса распознавания. Конструктор сервиса распознавания должен принимать следующие аргументы:

·        ширину и высоту изображения предпросмотра - на их основе будут вычислены такие величины, как абсолютные координаты областей поиска полей, размеры обрезанного под документ изображения, размеры матриц сверток, используемых в морфологических и других операциях;

·        контекст текущей операции (объект android.content.Context) - нужен для запуска следующей операции;

·        объект android.hardware.Camera - он необходим для захвата фото в полном разрешении для последующего распознавания текста;

·        объект tesseract.TessBaseAPI - API для распознавания текста, подробнее будет описано далее.

Контекст операции можно получить при помощи ключевого слова this, поскольку каждая операция (Activity) является наследником Context. Ширина и высота изображения поступают в метод onCameraViewStarted() в качестве параметров.

Для захвата полноразмерного снимка должен использоваться тот же объект Camera, который используется для получения изображений предпросмотра. Он инициализируется в классе CvCameraPreview, однако не передается в onCameraViewStarted в качестве аргумента, поэтому необходимо модифицировать CvCameraPreview и интерфейс CvCameraViewListener, добавив третий параметр - Camera.

Для работы Tesseract необходимы обученные данные для определенного шрифта и языка - файл с расширением traineddata. Обучение Tesseract используемым в документах шрифтам будет описано в пункте 4.7. Полученный после обучения файл (в данном случае rus.traineddata) необходимо встроить в приложение. ОС Android не предоставляет возможности обращения к файлам, встроенным в пакет приложения (APK), с использованием абсолютных путей, что необходимо для API библиотеки Tesseract. Поэтому файл с обученными данными можно скопировать из пакета приложения, например, в директорию кэша приложения. Копирование этого файла, если он отсутствует в директории кэша, а также инициализация API при помощи метода TessBaseAPI.Init() производятся при старте операции RecognitionActivity в методе initTesseract().

Таким образом, получены все необходимые для инстанцирования сервиса распознавания аргументы. В зависимости от типа документа, полученного ранее через дополнительные данные в намерении, в методе onCameraViewStarted() создается либо экземпляр класса PassportRecognitionService, либо SnilsRecognitionService. В методе onCameraFrame() достаточно вызывать processPreview() этого сервиса - обработанное этим методом изображение будет возвращено в CvCameraPreview для отрисовки на окне предпросмотра.

4.3.4 Распознавание

Распознавание текста полей также будет реализовано в BaseDocRecognitionService. Положительный результат проверки isAllFieldsFound() говорит о том, что можно приступить к распознаванию текста в полях. Эту роль выполняет метод takeFullSizePicture(). В нем происходит асинхронный захват полноразмерного изображения с камеры при помощи takePicture() того же экземпляра Camera, который используется для предпросмотра. Поскольку захват выполняется асинхронно, методу takePicture() в параметре jpeg необходимо сообщить реализацию интерфейса обратного вызова PictureCallback. Метод onPictureTaken() этого интерфейса будет вызван после полной обработки изображения и принимает на вход два параметра - массив байт, содержащий изображение и объект Camera, который был использован для захвата изображения.

. Фильтрация

Прежде чем приступать к распознаванию, снятое изображение необходимо перевернуть и сконвертировать в цветовое пространство RGBA. Для этого могут быть применены фильтры FFmpeg, поскольку они также применяются в CvCameraPreview для фильтрации изображения предпросмотра. Алгоритм фильтрации можно вынести в метод filterFrame(), который примет на вход изображение в виде типа byte[], ширину и высоту исходного изображения.

Сперва нужно создать объект Frame - основной объект в FFmpeg, соответствующий одному кадру, и поместить в этот объект изображение (массив байт). После этого необходимо настроить фильтры. В данном случае - поворот изображения и конвертацию в цветовое пространство RGBA. Для этого предусмотрены фильтры transpose и format. Параметр dir фильтра transpose указывает направление поворота. Изображение необходимо повернуть на 90 градусов по часовой стрелке, и этому соответствует dir=1. Формат выходного изображения указывается в параметре pix_fmts фильтра format и для RGBA равен, соответственно, rgba. Фильтры указываются в строке через запятую.

Таким образом, строка фильтров будет выглядеть так:=1,format=pix_fmts=rgba

После настройки необходимо обработать изображение этими фильтрами. Для этого сначала фильтр запускается вызовом FFmpegFrameFilter.start, затем исходный кадр помещается в очередь обработки при помощи FFmpegFrameFilter.push, обработанный кадр вытаскивается из очереди методом FFmpegFrameFilter.pull и фильтр останавливается вызовом FFmpegFrameFilter.stop.

Обработанный фильтрами кадр должен быть преобразован к формату Mat для использования в OpenCV. Для конвертации между Frame и Mat в JavaCV существует утилитный класс OpenCVFrameConverter.ToMat, а именно - метод convert этого класса. Данному методу передается кадр и на выходе получается изображение в виде Mat.

. Повторное обнаружение

Между получением очередного изображения предпросмотра и захватом полноразмерного изображения проходит некоторое время, за которое положение камеры относительно документа могло измениться, хоть и не на много. Этого смещения достаточно для того, чтобы обнаруженные прямоугольники с текстом не соответствовали реальному расположению текста на снятом изображении. В связи с этим, необходимо произвести повторное обнаружение полей. Для этого изображение сначала уменьшается до размеров изображения предпросмотра, чтобы снизить затраты процессорного времени на обнаружение, при помощи метода opencv_imgproc.resize(). Используемый алгоритм интерполяции - INTER_AREA. Он рекомендуется при уменьшении разрешения изображения, поскольку дает результат без муарового узора [15]. После изменения уменьшения, изображение вновь передается в метод detect() для обнаружения полей.

Распознавание

После обнаружения полей выполняется заключительная часть - непосредственно распознавание. Она выделена в абстрактный метод recognize(), принимающий на вход полноразмерное изображение и мультиотображение DocumentField -> Rect, то есть поля с соответствующими им прямоугольниками на изображении.

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

Во-первых, изображение необходимо преобразовать в оттенки серого уже упоминавшейся операцией cvtColor(). После чего последовательно проходя записи в мультиотображении:

·        Прямоугольник масштабируется под полноразмерное изображение. Это необходимо поскольку обнаружение полей производилось по уменьшенному изображению.

·        Часть изображения, находящаяся в этом прямоугольнике, копируется в новый объект Mat, чтобы избежать повреждения полного изображения.

·        В случае, если текущее поле - серия и номер паспорта, вырезанное изображение переворачивается на 90 градусов против часовой стрелки. Для этого последовательно выполняются операции транспонирования матрицы и отражения ее относительно оси X.

·        Адаптивная пороговая бинаризация. В отличие от простой пороговой бинаризации этот алгоритм рассчитывает порог для небольшой области изображения, что дает разные пороги для разных частей изображения и лучшие результаты при непостоянном освещении [16]. Используемый алгоритм расчета порога - ADAPTIVE_THRESH_MEAN_C, порог рассчитывается как среднее значение из значений соседних пикселей. На выходе получается бинарное изображение с черным текстом на белом фоне.

·        Операция замыкания отсеивает шумы.

·        Полученное изображение передается для распознавания в Tesseract через метод TessBaseAPI.TesseractRect() и результат распознавания помещается в результирующее отображение DocumentField -> String. В случае если для этого поля уже была распознана некоторая строка (в случае многострочных полей), полученная на этом шаге строка добавляется к существующей.

На этом распознавание текста закончено и можно перейти к операции верификации распознанных данных. Ассоциативный массив полей документа с их значениями необходимо поместить в дополнительные данные намерения. Все поля с их значениями можно сгруппировать в объект Bundle и поместить его в намерение под ключом documentFields. Так же в намерение необходимо поместить тип документа - от него будут зависеть поля, отображаемые на форме верификации.

 

4.4 Корректировка и подтверждение распознанных данных


После распознавания данных запускается операция DocumentActivity. Ее макет activity_document.xml должен содержать список полей с их значениями и возможностью корректировки. Набор полей зависит от типа документа. Для возможности прокрутки формы с полями предназначено представление ScrollView. Соответственно на макете можно разместить два таких представления, из которых отображаться будет только одно, в зависимости от типа документа. Листинг операции DocumentActivity представлен в приложении 16, макета activity_document - в приложении 5.

ScrollView подразумевает, что в нем будет расположен единственный дочерний элемент, уже в котором будет находиться содержимое для прокрутки. Очевидно, для группировки нескольких произвольных элементов следует использовать макет, поэтому поместим в ScrollView макет типа ConstraintLayout как наиболее гибкий и удобный в использовании. На этом макете следует расположить представления EditText и дать им уникальные идентификаторы для последующего заполнения этих полей распознанным текстом. Кроме того, каждый EditText необходимо снабдить компонентом TextView с подписью для поля. Аналогичным образом создается второе представление ScrollView и обоим представлениям устанавливается атрибут android:visibility=”gone” для их скрытия по умолчанию.

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

 

4.4.1 Заполнение полей формы данными

Заполнение текстовых полей выполняется при запуске Activity в методе onCreate(). Из дополнительных данных намерения получается тип документа и Bundle, содержащий значения полей этого документа. Затем в зависимости от типа документа атрибут android:visibility того или иного представления ScrollView устанавливается в значение VISIBLE.

В классе операции определены отображения полей документов (то есть элементов перечислений PassportField или SnilsField) на идентификаторы представлений EditText, который соответствуют этим полям. Это отображение используется при заполнении текстовых полей на форме конкретными значениями из Bundle-а, полученного в намерении. Так же для последующего сохранения в БД данные из объекта Bundle копируются в поле класса типа Map<DocumentField, String>.

 

4.4.2 Сохранение в БД

1. Создание БД

В ОС Android присутствует встроенный механизм хранения данных приложения в СУБД SQLite и API для работы с БД в пакете android.database.sqlite.

Сперва необходимо написать так называемый контрактный класс, который явно определит схему БД [17]. В контрактном классе следует разместить константы, определяющие URI, таблицы и столбцы. Для схемы БД данного проекта этот класс представлен в приложении 19.

После определения контрактного класса нужно реализовать методы создания и управления базой данных и таблицами. Android дает возможность удобного управления БД при помощи класса SQLiteOpenHelper. Чтобы его использовать, необходимо создать класс-наследник, в котором реализовать по крайней мере методы onCreate() и onUpgrade(). В методе onCreate() должны выполняться SQL-скрипты для инициализации БД, такие как создание таблиц и при необходимости их заполнение. В методе onUpgrade() должны быть описаны действия, выполняемые при увеличении версии схемы БД. Реализация данного класса DbHelper представлена в приложении 20.

Итак, при создании БД нужно создать 3 таблицы: DOCUMENT, DOCUMENT_TYPE и USER. Скрипты для создания этих таблиц разместятся в статичных final-полях класса. Метод onCreate() принимает параметром объект SQLiteDatabase - именно этому обеспечивает взаимодействие с БД. Таблицы можно создать, вызывая метод execSQL() этого объекта, передавая ему в качестве параметра SQL-скрипт.

Затем необходимо заполнить таблицу DOCUMENT_TYPE. Вставка строки в таблицу осуществляется путем передачи объекта ContentValues методу insert(). Сначала создается объект ContentValues, затем в него при помощи метода put() помещаются значения для отдельных полей. Например, для паспорта:

ContentValues docTypeValues = new ContentValues();.put(COLUMN_NAME_CODE, VALUE_CODE_PASSPORT);.put(COLUMN_NAME_NAME, VALUE_NAME_PASSPORT);.insert(DocRecognizerContract.DocumentType.TABLE_NAME, null, docTypeValues);

После этого вызывается метод SQLiteDatabase.insert(), которому первым аргументом передается имя таблицы, третьим - ContentValues, созданный ранее. Во втором параметре в данном случае нет необходимости и вместо него передается null.

Схема БД будет редактироваться только во время разработки приложения, поэтому в методе onUpgrade() нет необходимости делать миграцию данных. Все что выполняет этот метод - удаляет существующие таблицы и повторно вызывает метод onCreate().

. Сохранение в БД

После нажатия кнопки сохранения используя экземпляр написанного ранее класса DbHelper создается подключение к БД с доступом на запись, используя метод DbHelper.getWritableDatabase(). Затем в зависимости от типа документа, необходимо прочитать из таблицы DOCUMENT_TYPE соответствующий типу документа ID. Этот идентификатор впоследствии будет вставлен в столбец DOCUMENT.DOCUMENT_TYPE_ID. Работа с записями производится через курсор, получаемый вызовом метода SQLiteDatabase.query(). В первом аргументе метода указывается название таблицы, во втором массив названий полей, которые должны быть выбраны, в третьем - условие выборки с символом “?” в местах, куда должны быть подставлены параметры, переданные в четвертом параметре.

Изначально курсор находится на позиции -1, для получения результата выборки, его необходимо передвинуть на первую запись с помощью метода Cursor.moveToFirst(). Теперь можно прочитать искомый ID - для этого вызывается метод Cursor.getLong(), который принимает в качестве параметра номер столбца. Поскольку в выборку попадает единственное поле _ID, методу getLong() необходимо передать аргумент 0. После того как работа с курсором закончена, его нужно закрыть методом Cursor.close().

Для преобразования Java-объектов в JSON-текст существует целый ряд библиотек. Из них можно выделить Google Gson, так как она свободно распространяемая, нашла широкое применение и, что немаловажно, поддерживает сериализацию отображений (Map). Чтобы подключить библиотеку Gson к проекту необходимо добавить в конфигурацию Gradle следующую зависимость:

compile 'com.google.code.gson:gson:2.8.0'

Чтобы использовать Gson, сначала нужно создать экземпляр класса Gson вызвав метод GsonBuilder.create(). Все что нужно для сериализации в строку отображения названий полей на их значения - вызвать метод Gson.toJson(), передав ему это отображение. Таким образом, отображение

“NUMBER” -> “19 08 000000”,

“ISSUED_BY” -> “Отделом УФМС России”,

“LAST_NAME” -> “Иванов”

преобразуется в:

{"NUMBER":"19 08 000000","ISSUED_BY":"Отделом УФМС России",

"LAST_NAME":"Иванов"}

Теперь необходимо сохранить этот документ в БД. Создается новый экземпляр класса ContentValues, в него помещаются значения столбцов DOC_TYPE_ID (был найден ранее), DATA (JSON-строка) и USER_ID (ID пользователя, механизм аутентификации будет реализован позднее) и новая запись вставляется в таблицу DOCUMENT. На этом сохранение в БД завершено и снова производится переход к операции выбора режима OperationActivity.

4.5 Механизм аутентификации


Каждому пользователю приложения соответствует уникальный идентификатор, хранящийся в таблице USER. Идентификатор, который принадлежит аутентифицированному в данный момент пользователю должен быть доступен в любой операции приложения, в частности - в операции верификации/просмотра данных документа и в операции отображения списка документов. Для этого необходимо каким-либо способом передать ID пользователя в эти операции. Одним из вариантов является проброс ID при помощи дополнительных данных намерений. Однако этот способ сильно запутывает код, поскольку перед каждым запуском другой операции идентификатор нужно поместить в намерение, а при старте каждой операции - брать его оттуда. Существует еще два подхода - использование паттерна «Синглтон» для хранения переменной или расширение класса Application и хранение переменной в нем [18]. Предпочтительным из этих подходов является второй, поскольку он естественен для приложения Android.

Таким образом, необходимо создать класс, наследующий Application - DocRecogApplication. Этот класс должен содержать поле userId типа Long, а также геттер и сеттер для этого поля. Чтобы приложение использовало этот класс, необходимо в файле AndroidManifest.xml элементу application добавить атрибут android:name=”.DocRecogApplication”. Листинг класс DocRecogApplication приведен в приложении 21.

Теперь нужно создать операцию аутентификации. Android Studio предлагает набор шаблонных операций, в числе которых операция аутентификации - LoginActivity. По умолчанию она просто проверяет введенные логин пароль на равенство жестко закодированным в операции строкам. Необходимо модифицировать этот класс таким образом, чтобы аутентификация производилась с использованием базы данных. Листинги макета и операции представлены в приложениях 2 и 8 соответственно.

Пароль должен храниться в виде хеш-суммы. Хеширование пароля будет осуществляться алгоритмом SHA-256. Реализация этого алгоритма есть в библиотеке Google Guava, которая уже подключена к проекту. Хеширование производится следующим образом:

String passwordHash = Hashing.sha256().hashUnencodedChars(password).toString();

После этого необходимо открыть подключение к базе данных и проверить наличие пользователя с соответствующим именем пользователя. В случае если пользователь найден, проверяется соответствие хеш-сумм паролей, и, если результат положительный, ID пользователя сохраняется в поле класса. В случае же, если пользователь не найден, необходимо его зарегистрировать - в таблицу USER вставляется запись с именем пользователя и хеш-суммой пароля и ID пользователя соответственно становится равным значению столбца _ID только что добавленной записи.

Взаимодействие с БД осуществляется асинхронно в отдельном потоке. По его завершении в основном UI-потоке выполняется метод onPostExecute(), в котором идентификатор пользователя устанавливается в DocRecogApplication и становится доступен остальным частям приложения, и запускается операция OperationActivity. В случае если пользователь был найден, но хеш-суммы паролей не совпали, рядом с текстовым полем ввода пароля выводится текст ошибки.

Теперь для получения в приложении идентификатора пользователя необходимо вызвать ((DocRecogApplication) getApplication()).getUserId().

 

4.6 Просмотр документов


В случае, если выбран режим «Просмотр существующего документа», должен быть отображен список внесенных пользователем документов, для чего необходимо создать новую операцию - DocListActivity. Список элементов может быть создан при помощи представления ListView. Список автоматически заполняется элементами при помощи адаптера (класс Adapter), который конвертирует данные из таких источников данных, как массивы или БД, в элементы списка. Листинг макета данной операции представлен в приложении 6. Листинг операции представлен в приложении 11.

Для данных, получаемых из курсора, существует реализация адаптера SimpleCursorAdapter. Для его использования необходимо указать макет, который будет использован для отображения каждой строки, и значения каких столбцов должны быть вставлены в какие представления этого макета. Поэтому, сперва необходимо создать макет строки - list_item.xml. Стоить заметить, что SimpleCursorAdapter может заполнять данными только компоненты TextView. На макете list_item.xml будет расположен единственное представление - TextView, в котором будет выводиться тип документа и его порядковый номер. Листинг list_item.xml находится в приложении 7.

В методе onCreate операции нужно открыть подключение к базе данных и создать курсор запроса на выборку доступных текущему пользователю документов. Для того, чтобы получить название типа документа, необходимо соединить таблицы DOCUMENT и DOCUMENT_TYPE оператором JOIN, однако стандартный метод SQLiteDatabase.query() не предоставляет такой возможности. Для построения произвольных запросов существует метод SQLiteDatabase.rawQuery(). В первом аргументе методу передается текст запроса с символами “?” на местах, куда должны быть подставлены значения, переданные в массиве во втором аргументе. Для применения в адаптере запрос также должен возвращать обязательный столбец _ID. Он же будет использован для нумерации документов. Запрос на выборку будет выглядеть следующим образом:

SELECT DOCUMENT._ID, DOCUMENT_TYPE.NAME || “ ” || DOCUMENT._ID AS DOC_NAMEDOCUMENTDOCUMENT_TYPE ON DOCUMENT.DOC_TYPE_ID = DOCUMENT_TYPE._ID DOCUMENT.USER_ID = ?

На место “?” будет подставлен ID текущего пользователя. Далее необходимо создать адаптер, передав ему этот курсор, идентификатор макета строки list_item, наименование столбца, из которого будет получено значение для отображения на TextView (в данном случае DOC_NAME) и идентификатор TextView, в который данное значение будет установлено. После чего необходимо назначить этот адаптер списку методом ListView.setAdapter.

При нажатии на элемент списка, нужно открывать на чтение соответствующий документ. Для этого необходимо запустить операцию DocumentActivity, предварительно поместив в намерение идентификатор документа. Также необходимо модифицировать DocumentActivity таким образом, чтобы в случае если в намерении установлен ID документа, поля документа загружались из БД, TextView, в которых отображаются эти поля становились недоступными для редактирования, а кнопка сохранения - невидимой.

Загрузка документа из БД вынесена в метод loadDocument(). В нем сначала производится выборка столбцов DOCUMENT.DATA и DOCUMENT_TYPE.CODE для документа с требуемым ID. Затем производится десериализация JSON-объекта при помощи той же библиотеки Gson, а именно метода Gson.fromJson(). Кроме JSON-строки методу необходимо передать объект типа Type, определяющий тип десериализованного объекта. Чтобы получить объект Type, необходимо воспользоваться методом getType() класса TypeToken. Например, для того, чтобы на выходе получить отображение PassportField -> String, нужно создать тип следующим образом:

new TypeToken<Map<PassportField, String>>(){}.getType();

Удаление документов

Удаление существующего документа осуществляется после долгого нажатия на элемент списка ListView. Чтобы обработать событие долгого нажатия, при запуске операции просмотра списка документов необходимо реализовать интерфейс OnItemLongClickListener. В методе onItemLongClick этого интерфейса нужно вызвать метод SQLiteDatabase.delete(), передав ему в качестве аргументов название таблицы, из которой производится удаление, - DOCUMENT и условие, по которому будет произведено удаление (_ID = id). Список ListView после удаления элемента самостоятельно не обновится - для удаления необходимо обновить курсор адаптера списка.

На этом разработка приложения в части кодирования завершена.

 

4.7 Обучение Tesseract


Tesseract предлагает пакет инструментов для обучения. Первым действием при обучении должно быть генерирование изображений для обучения (в формате TIFF) и файлов с координатами прямоугольников (в формате BOX), в которых заключены символы на изображениях [19]. При наличии требуемого шрифта в формате TrueType, генерация TIFF и BOX-файлов может быть автоматизирована при помощи утилиты text2image, однако для печати текста в паспортах и страховых свидетельствах используются встроенные в принтеры Epson «матричные» шрифты, для которых не существует шрифтов в требуемом формате. Создание изображений и BOX-файлов в таком случае осуществляется вручную.

Подготовка TIFF и BOX-файлов

В качестве обучающих изображений могут быть использованы сканы или фотографии документов. Цветные изображения необходимо перевести в оттенки серого и затем в черно-белые, например, при помощи пороговой бинаризации, так, чтобы текст был черным на белом фоне. Полученное изображение желательно сохранить в формате TIFF. После этого должен быть создан BOX-файл с координатами символов. Для этого будет использована программа jTessBoxEditor. После запуска программы нужно переключиться на вкладку Box Editor и открыть изображение документа. Интерфейс программы изображен на рисунке 22.

 

Рисунок 22 - Интерфейс программы jTessBoxEditor

Далее необходимо каждый символ поместить в отдельный прямоугольник, добавив его кнопкой Insert на панели инструментов, и сопоставить с прямоугольником символ Юникода. После повторения этой операции для каждого символа необходимого шрифта на изображении, BOX-файл сохраняется нажатием кнопки Save.

Собрав достаточное количество обучающих данных (Tesseract рекомендует не менее 20 образцов для часто встречающихся символов и 5-10 - для редких) можно приступить к непосредственно обучению. Обучение также может быть выполнено в jTessBoxEditor - для этого следует переключиться на вкладку Trainer, в поле Training Data указать источник изображений с BOX-файлами, в поле Language ввести “rus” в поле выбора режима установить Train with Existing Box и запустить обучение нажатием кнопки Run. После обучения в указанной папке появится директория tessdata с файлом rus.traineddata - эту директорию необходимо поместить в папку assets проекта Android Studio. При запуске приложения на смартфоне этот файл будет скопирован из APK в папку кэша приложения.

4.8 Сборка приложения


Приложение готово к сборке. В связи с большим количеством библиотечных методов байт-код приложения должен располагаться в нескольких DEX-файлах. Для этого в конфигурации Gradle необходимо разрешить создание нескольких DEX-файлов, добавив строку “multiDexEnabled true” в секцию defaultConfig.

Затем нужно переключить вариант сборки с debug на release и собрать пакет, нажав “Build APK”. Перед тем как пакет сможет быть установлен на устройство, его необходимо подписать. Для этого сначала следует создать хранилище ключей и ключ, после чего подписать пакет приложения выбрав подписание целого пакета (V2). После этого в папке проекта будет создан пакет приложения app-release.apk, готовый к установке на устройство.

5. ТЕСТИРОВАНИЕ

 

.1 Обоснование методики тестирования


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

Тест-кейс №1. Регистрация пользователя: имя пользователя короче 4-х симолов.

Шаги

1.                  Запустить приложение

2.      Ввести имя пользователя «usr».

.        Ввести пароль «pswd»

.        Нажать на кнопку войти/зарегистрироваться

Ожидаемый результат

Появляется сообщение об ошибке «Имя пользователя слишком короткое»

Тест-кейс №2. Регистрация пользователя: пароль короче 4-х симолов.

Шаги

.                    Запустить приложение

2.      Ввести имя пользователя «user».

.        Ввести пароль «pwd»

.        Нажать на кнопку войти/зарегистрироваться

Ожидаемый результат

Появляется сообщение об ошибке «Пароль слишком короткий»

Тест-кейс №3. Регистрация пользователя: имя пользователя и пароль не менее 4-х символов.

Шаги

.                    Запустить приложение

.        Ввести пароль «pswd»

.        Нажать на кнопку войти/зарегистрироваться

Ожидаемый результат

Регистрация успешна - происходит переход в операции выбора режима

Тест-кейс №4. Аутентификация пользователя с некорректным паролем.

Предварительные условия

Зарегистрирован пользователь «user» с паролем «pswd»

Шаги

.                    Запустить приложение

2.      Ввести имя пользователя «user».

.        Ввести пароль «password»

.        Нажать на кнопку войти/зарегистрироваться

Ожидаемый результат

Появляется сообщение об ошибке «Неверный пароль»

Тест-кейс №5. Аутентификация пользователя с корректным паролем.

Предварительные условия

Зарегистрирован пользователь «user» с паролем «pswd»

Шаги

.                    Запустить приложение

2.      Ввести имя пользователя «user».

.        Ввести пароль «pswd»

.        Нажать на кнопку войти/зарегистрироваться

Ожидаемый результат

Аутентификация успешна - происходит переход в операции выбора режима

Тест-кейс №6. Внесение документа типа паспорт при различных условиях освещенности и используемых устройствах.

Шаги

.                    Запустить приложение

2.      Ввести имя пользователя «user».

.        Ввести пароль «pswd»

.        Нажать на кнопку войти/зарегистрироваться

.        Выбрать опцию «Внесение документа», затем «Паспорт» и нажать на кнопку «Продолжить

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

.        Сравнить результат распознавания с истинным текстом на документе при помощи инструмента String similarity test [20].

Ожидаемый результат

Требования к точности распознавания не определены.

Тест-кейс №7. Внесение документа типа страховое свидетельство при различных условиях освещенности и используемых устройствах.

Шаги

.                    Запустить приложение

2.      Ввести имя пользователя «user».

.        Ввести пароль «pswd»

.        Нажать на кнопку войти/зарегистрироваться

.        Выбрать опцию «Внесение документа», затем «Страховое свидетельство» и нажать на кнопку «Продолжить

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

.        Сравнить результат распознавания с истинным текстом на документе при помощи инструмента String similarity test.

Ожидаемый результат

Требования к точности распознавания не определены.

Тест-кейс №8. Просмотр документа типа паспорт.

Предварительные условия

Для пользователя «user» с паролем «pswd» создан документ типа паспорт

Шаги

.                    Запустить приложение

2.      Ввести имя пользователя «user».

.        Ввести пароль «pswd»

.        Нажать на кнопку войти/зарегистрироваться

.        Выбрать опцию «Просмотр документа» и на кнопку «Продолжить»

.        Выбрать из списка документ

Ожидаемый результат

Отображается форма просмотра документа с заполненными полями «Серия и номер», «Кем выдан», «Дата выдачи», «Код подразделения», «Фамилия», «Имя», «Отчество», «Пол», «Дата рождения», «Место рождения». Поля заполнены такими же данными, какие были при сохранении.

Тест-кейс №9. Просмотр документа типа страховое свидетельство.

Предварительные условия

Для пользователя «user» с паролем «pswd» создан документ типа страховое свидетельство

Шаги

.                    Запустить приложение

2.      Ввести имя пользователя «user».

.        Ввести пароль «pswd»

.        Нажать на кнопку войти/зарегистрироваться

.        Выбрать опцию «Просмотр документа» и на кнопку «Продолжить»

.        Выбрать из списка документ

Ожидаемый результат

Отображается форма просмотра документа с заполненными полями «СНИЛС», «Фамилия», «Имя», «Отчество», «Дата рождения», «Место рождения», «Пол», «Дата выдачи». Поля заполнены такими же данными, какие были при сохранении.

Тест-кейс №10. Удаление документа.

Предварительные условия

Для пользователя «user» с паролем «pswd» создан документ любого типа

Шаги

.                    Запустить приложение

2.      Ввести имя пользователя «user».

.        Ввести пароль «pswd»

.        Нажать на кнопку войти/зарегистрироваться

.        Выбрать опцию «Просмотр документа» и на кнопку «Продолжить»

.        Осуществить долгое нажатие на документ в списке

Ожидаемый результат

Документ удален из списка

Также необходимо произвести тестирование производительности операции распознавания текста полей документа. Для этого необходимо программно замерить время между началом и окончанием операции распознавания. Текущее время в миллисекундах может быть получено при помощи метода System. currentTimeMillis().

Будет проведен эксперимент по сравнению среднего времени внесения данных документа при помощи разработанной системы и ручного ввод. Внесение документа кроме непосредственно ввода данных, включает в себя их верификацию и коррекцию.

 

5.2 Результаты тестирования


Результаты тестирования отражены в таблице 1.

Таблица 1 - результаты тестирования

№ п/п

№ тест-кейса

Дополнительные условия

Результат

1

1


пройден

2

2


пройден

3

3


пройден

4

4


пройден

5

5


пройден

6

6

Естественное боковое освещение, Oneplus One

точность - 92,97 %

7

6

Естественное боковое освещение, Xiaomi Redmi 4X

точность - 91,61 %

8

6

Естественное боковое освещение, Xiaomi Mi4c

точность - 90,95 %

9

6

Искусственное локализованное освещение, Oneplus One

точность - 97,83 %

10

6

Искусственное локализованное освещение, Xiaomi Redmi 4X

точность - 94,47 %

11

6

Искусственное локализованное освещение, Xiaomi Mi4c

точность - 93,22 %

12

7

Естественное боковое освещение, Oneplus One

точность - 92,31 %

13

7

Естественное боковое освещение, Xiaomi Redmi 4X

точность - 90,66 %

14

7

Естественное боковое освещение, Xiaomi Mi4c

точность - 90,44 %

15

7

Искусственное локализованное освещение, Oneplus One

точность - 97,98 %

16

7

Искусственное локализованное освещение, Xiaomi Redmi 4X

точность - 97,23 %

17

7

Искусственное локализованное освещение, Xiaomi Mi4c

точность - 94,21 %

18

8


пройден

19

9


пройден

20

10


пройден


Тестирование производительности показало, что среднее время распознавания данных паспорта составляет 832 мс, а среднее время распознавания данных страхового свидетельства - 581 мс, что удовлетворяет требованию к максимальному времени распознавания в 1 с.

Среднее время внесения данных при помощи оптического распознавания составило для паспорта 39,8 с, для страхового свидетельства - 23,4 с. Замер времени производился между запуском операции распознавания (RecognitionActivity) и нажатием на кнопку сохранения документа в БД.

Для оценки скорости внесения данных документов вручную была использована операция DocumentActivity с пустыми текстовыми полями. Время замерялось между началом ввода и нажатием кнопки «Сохранить» и в среднем составило: для паспорта - 1 мин 23,9 с, для страхового свидетельства - 1 мин 1 с.

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

ЗАКЛЮЧЕНИЕ


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

Была спроектирована база данных для использования в приложении. Разработаны алгоритмы обнаружения и распознавания текста в документе, использующие преимущественно возможности библиотеки OpenCV. Спроектирован пользовательский интерфейс приложения на ОС Android.

Было разработано приложение в интегрированной среде разработки Android Studio. К приложению подключена локальная БД SQLite для сохранения данных распознанных документов. Реализован простой механизм аутентификации пользователя в системе. Также была обучена библиотека Tesseract для шрифтов, используемых в паспортах РФ и страховых свидетельствах.

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

Приложение может использоваться на смартфонах с ОС Android 4.0 и выше, имеющих камеру с разрешением не менее 2 Мп с автофокусом, и не требует доступ к интернету.

СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ

1. Технологии перевода бумажных документов в электронные [Электронный ресурс] - Режим доступа: http://compress.ru/article.aspx?id=11802

2. Comparison of optical character recognition software [Электронный ресурс] - https://en.wikipedia.org/wiki/Comparison_of_optical_character_recognition_software

. Improving the quality of the output [Электронный ресурс] - https://github.com/tesseract-ocr/tesseract/wiki/ImproveQuality

4. Компьютерное зрение [Электронный ресурс] - Режим доступа: ru.wikipedia.org/wiki/Компьютерное_зрение <https://ru.wikipedia.org/wiki/Компьютерное_зрение>

. Выделение границ [Электронный ресурс] - Режим доступа: ru.wikipedia.org/wiki/Выделение <https://ru.wikipedia.org/wiki/Компьютерное_зрение>_границ

. Оператор Кэнни [Электронный ресурс] - Режим доступа: ru.wikipedia.org/wiki/ <https://ru.wikipedia.org/wiki/Компьютерное_зрение>Оператор_Кэнни

. Математическая морфология [Электронный ресурс] - Режим доступа: ru.wikipedia.org/wiki/ <https://ru.wikipedia.org/wiki/Компьютерное_зрение>Математическая_морфология

. Операции [Электронный ресурс] - Режим доступа: https://developer.android.com/guide/components/activities.html?hl=ru

. Макеты [Электронный ресурс] - Режим доступа: https://developer.android.com/guide/topics/ui/declaring-layout.html?hl=ru

. jTessBoxEditor [Электронный ресурс] - Режим доступа: http://vietocr.sourceforge.net/training.html

. Dashboards [Электронный ресурс] - Режим доступа: https://developer.android.com/about/dashboards/index.html

. Bytedeco javacv-android-camera-preview [Электронный ресурс] - Режим доступа: https://github.com/bytedeco/sample-projects/tree/master/javacv-android-camera-preview

. Mat - The Basic Image Container [Электронный ресурс] - Режим доступа: http://docs.opencv.org/3.2.0/d6/d6d/tutorial_mat_the_basic_image_container.html

. Паспорт гражданина Российской Федерации [Электронный ресурс] - Режим доступа: https://ru.wikipedia.org/wiki/Паспорт_гражданина_Российской_Федерации

. Geometric Image Transformations [Электронный ресурс] - Режим доступа: http://docs.opencv.org/3.2.0/da/d54/group__imgproc__transform.html#gga5bb5a1fea74ea38e1a5445ca803ff121acf959dca2480cc694ca016b81b442ceb

. Image Thresholding [Электронный ресурс] - Режим доступа: http://docs.opencv.org/trunk/d7/d4d/tutorial_py_thresholding.html

. Saving Data in SQL Databases [Электронный ресурс] - Режим доступа: https://developer.android.com/training/basics/data-storage/databases.html

18. Defining Global Variables in Android [Электронный ресурс] - Режим доступа: https://androidresearch.wordpress.com/2012/03/22/defining-global-variables-in-android/

19. Training Tesseract [Электронный ресурс] - Режим доступа: https://github.com/tesseract-ocr/tesseract/wiki/Training-Tesseract

. String similarity test [Электронный ресурс] - Режим доступа: https://www.tools4noobs.com/online_tools/string_similarity

ПРИЛОЖЕНИЕ 1

(обязательное)


Листинг файла манифеста AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"="demidov.docrecognition">

<uses-permission android:name="android.permission.CAMERA" />

<uses-feature android:name="android.hardware.camera" android:required="true" />

<uses-feature android:name="android.hardware.camera.autofocus" />

<application:name=".DocRecogApplication":allowBackup="true":icon="@mipmap/ic_launcher":label="@string/app_name":roundIcon="@mipmap/ic_launcher_round":supportsRtl="true":theme="@style/AppTheme">

<activity:name=".RecognitionActivity":screenOrientation="portrait"></activity>

<activity:name=".LoginActivity":label="@string/title_activity_login">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

<activity:name=".OperationActivity":label="@string/title_activity_operation" />

<activity:name=".DocumentActivity":label="@string/title_activity_document" />

<activity:name=".DocListActivity":label="@string/title_activity_doclist" />

</application>

</manifest>

ПРИЛОЖЕНИЕ 2

(обязательное)


Листинг макета activity_login.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android":tools="http://schemas.android.com/tools":layout_width="match_parent":layout_height="match_parent":gravity="center_horizontal":orientation="vertical":paddingBottom="@dimen/activity_vertical_margin":paddingLeft="@dimen/activity_horizontal_margin":paddingRight="@dimen/activity_horizontal_margin":paddingTop="@dimen/activity_vertical_margin":context="demidov.docrecognition.LoginActivity">

<!-- Login progress -->

<ProgressBar:id="@+id/login_progress"="?android:attr/progressBarStyleLarge":layout_width="wrap_content":layout_height="wrap_content":layout_marginBottom="8dp":visibility="gone" />

<ScrollView:id="@+id/login_form":layout_width="match_parent":layout_height="match_parent">

<LinearLayout:id="@+id/username_login_form":layout_width="match_parent":layout_height="wrap_content":orientation="vertical">

<android.support.design.widget.TextInputLayout:layout_width="match_parent":layout_height="wrap_content">

<AutoCompleteTextView:id="@+id/username":layout_width="match_parent":layout_height="wrap_content":hint="@string/prompt_username":maxLines="1":singleLine="true" />

</android.support.design.widget.TextInputLayout>

<android.support.design.widget.TextInputLayout:layout_width="match_parent":layout_height="wrap_content">

<EditText:id="@+id/password":layout_width="match_parent":layout_height="wrap_content":hint="@string/prompt_password":imeActionId="@+id/login":imeActionLabel="@string/action_sign_in_short":imeOptions="actionUnspecified":inputType="textPassword":maxLines="1":singleLine="true" />

</android.support.design.widget.TextInputLayout>

<Button:id="@+id/sign_in_button"="?android:textAppearanceSmall":layout_width="match_parent":layout_height="wrap_content":layout_marginTop="16dp":text="@string/action_sign_in":textStyle="bold" />

</LinearLayout>

</ScrollView>

</LinearLayout>

ПРИЛОЖЕНИЕ 3

(обязательное)


Листинг макета activity_operation.xml

<?xml version="1.0" encoding="utf-8"?>

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android":app="http://schemas.android.com/apk/res-auto":tools="http://schemas.android.com/tools":layout_width="match_parent":layout_height="match_parent":context="demidov.docrecognition.OperationActivity">

<RadioGroup:layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toTopOf="parent":layout_marginTop="8dp":id="@+id/operations">

<RadioButton:id="@+id/createDoc":layout_width="wrap_content":layout_height="wrap_content":text="@string/create_document":onClick="selectOperation" />

<RadioButton:id="@+id/existingDoc":layout_width="wrap_content":layout_height="wrap_content":text="@string/view_existing_document":onClick="selectOperation" />

</RadioGroup>

<RadioGroup:layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="0dp":layout_marginTop="8dp":visibility="gone":layout_constraintTop_toBottomOf="@+id/operations":layout_constraintLeft_toLeftOf="@+id/operations":id="@+id/docTypes">

<RadioButton:id="@+id/passport":layout_width="wrap_content":layout_height="wrap_content":onClick="selectDocType":text="@string/passport" />

<RadioButton:id="@+id/socialSecurity":layout_width="wrap_content":layout_height="wrap_content":onClick="selectDocType":text="@string/social_security" />

</RadioGroup>

<Button:id="@+id/continueButton":layout_width="wrap_content":layout_height="wrap_content":text="@string/continue_button":visibility="invisible":layout_marginTop="8dp":layout_constraintTop_toBottomOf="@+id/docTypes":layout_marginLeft="0dp":layout_constraintLeft_toLeftOf="@+id/docTypes":onClick="next" />

</android.support.constraint.ConstraintLayout>

ПРИЛОЖЕНИЕ 4

(обязательное)


Листинг макета activity_recognition.xml

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android":app="http://schemas.android.com/apk/res-auto":layout_width="match_parent":layout_height="match_parent":background="@android:color/black">

<Button:id="@+id/recorder_control":layout_width="64dp":layout_height="64dp":layout_alignParentBottom="true":layout_centerHorizontal="true":layout_gravity="bottom":layout_marginBottom="16dp":text="record" />

<demidov.docrecognition.CvCameraPreview:id="@+id/camera_view":layout_width="match_parent":layout_height="417dp":camera_type="back":scale_type="fit" />

</FrameLayout>

ПРИЛОЖЕНИЕ 5

(обязательное)

 

Листинг макета activity_document.xml

<?xml version="1.0" encoding="utf-8"?>

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android":app="http://schemas.android.com/apk/res-auto":tools="http://schemas.android.com/tools":layout_width="match_parent":layout_height="match_parent":context="demidov.docrecognition.DocumentActivity">

<Button:id="@+id/saveButton":layout_width="wrap_content":layout_height="wrap_content":layout_marginBottom="8dp":layout_marginLeft="8dp":onClick="saveData":text="@string/saveDocData":layout_constraintBottom_toBottomOf="parent":layout_constraintLeft_toLeftOf="parent" />

<ScrollView:id="@+id/passportVerification":layout_width="0dp":layout_height="0dp":layout_marginBottom="8dp":layout_constraintBottom_toTopOf="@+id/saveButton":layout_constraintHorizontal_bias="0.0":layout_constraintLeft_toLeftOf="parent":layout_constraintRight_toRightOf="parent":layout_constraintTop_toTopOf="parent":layout_constraintVertical_bias="0.0">

<android.support.constraint.ConstraintLayout:layout_width="wrap_content":layout_height="wrap_content":layout_editor_absoluteX="0dp":layout_editor_absoluteY="-163dp">

<TextView:id="@+id/textView":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/pass_number":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toTopOf="parent" />

<EditText:id="@+id/passNumber":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView" />

<TextView:id="@+id/textView5":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/pass_issuer_name":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/passNumber" />

<EditText:id="@+id/passIssuerName":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView5" />

<TextView:id="@+id/textView3":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/pass_issuing_date":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/passIssuerName" />

<EditText:id="@+id/passIssuingDate":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView3" />

<TextView:id="@+id/textView4":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/pass_issuer_code":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/passIssuingDate" />

<EditText:id="@+id/passIssuerCode":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView4" />

<TextView:id="@+id/textView6":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/last_name":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/passIssuerCode" />

<EditText:id="@+id/lastName":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView6" />

<TextView:id="@+id/textView7":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/first_name":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/lastName" />

<EditText:id="@+id/firstName":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView7" />

<TextView:id="@+id/textView8":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/middle_name":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/firstName" />

<EditText:id="@+id/middleName":layout_width="216dp":layout_height="43dp":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView8":layout_editor_absoluteX="7dp" />

<TextView:id="@+id/textView9":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/gender":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/middleName" />

<EditText:id="@+id/gender":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView9" />

<TextView:id="@+id/textView10":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/birth_date":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/gender" />

<EditText:id="@+id/birthDate":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView10" />

<TextView:id="@+id/textView11":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/birth_place":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/birthDate" />

<EditText:id="@+id/birthPlace":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginStart="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView11" />

</android.support.constraint.ConstraintLayout>

</ScrollView>

<ScrollView:id="@+id/snilsVerification":layout_width="0dp":layout_height="0dp":layout_marginBottom="8dp":visibility="gone":layout_constraintBottom_toTopOf="@+id/saveButton":layout_constraintHorizontal_bias="0.0":layout_constraintLeft_toLeftOf="parent":layout_constraintRight_toRightOf="parent":layout_constraintTop_toTopOf="parent":layout_constraintVertical_bias="0.0">

<android.support.constraint.ConstraintLayout:layout_width="wrap_content":layout_height="wrap_content":layout_editor_absoluteX="0dp":layout_editor_absoluteY="-163dp">

<TextView:id="@+id/textView12":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/snils_number":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toTopOf="parent" />

<EditText:id="@+id/snilsNumber":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView12" />

<TextView:id="@+id/textView13":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/last_name":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/snilsNumber" />

<EditText:id="@+id/snilsLastName":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView13" />

<TextView:id="@+id/textView14":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/first_name":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/snilsLastName" />

<EditText:id="@+id/snilsFirstName":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView14" />

<TextView:id="@+id/textView15":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/middle_name":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/snilsFirstName" />

<EditText:id="@+id/snilsMiddleName":layout_width="216dp":layout_height="43dp":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView15":layout_editor_absoluteX="7dp" />

<TextView:id="@+id/textView16":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/birth_date":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/snilsMiddleName" />

<EditText:id="@+id/snilsBirthDate":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView16" />

<TextView:id="@+id/textView17":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/birth_place":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/snilsBirthDate" />

<EditText:id="@+id/snilsBirthPlace":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginStart="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView17" />

<TextView:id="@+id/textView18":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/gender":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/snilsBirthPlace" />

<EditText:id="@+id/snilsGender":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView18" />

<TextView:id="@+id/textView19":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":text="@string/pass_issuing_date":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/snilsGender" />

<EditText:id="@+id/snilsIssuingDate":layout_width="wrap_content":layout_height="wrap_content":layout_marginLeft="8dp":layout_marginTop="8dp":ems="10":inputType="text":text="":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintTop_toBottomOf="@+id/textView19" />

</android.support.constraint.ConstraintLayout>

</ScrollView>

</android.support.constraint.ConstraintLayout>

ПРИЛОЖЕНИЕ 6

(обязательное)


Листинг макета activity_doc_list.xml

<?xml version="1.0" encoding="utf-8"?>

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android":app="http://schemas.android.com/apk/res-auto":tools="http://schemas.android.com/tools":layout_width="match_parent":layout_height="match_parent":context="demidov.docrecognition.DocListActivity">

<ListView:id="@+id/documentList":layout_width="0dp":layout_height="0dp":layout_marginBottom="8dp":layout_marginLeft="8dp":layout_marginRight="8dp":layout_marginTop="8dp":layout_constraintBottom_toBottomOf="parent":layout_constraintLeft_toLeftOf="parent":layout_constraintRight_toRightOf="parent":layout_constraintTop_toTopOf="parent":layout_marginStart="8dp":layout_marginEnd="8dp" />

</android.support.constraint.ConstraintLayout>

ПРИЛОЖЕНИЕ 7

(обязательное)

 

Листинг макета list_item.xml

<?xml version="1.0" encoding="utf-8"?>

<android.support.constraint.ConstraintLayout:android="http://schemas.android.com/apk/res/android":app="http://schemas.android.com/apk/res-auto":tools="http://schemas.android.com/tools":layout_width="match_parent":layout_height="match_parent">

<TextView:id="@+id/textView2":layout_width="match_parent":layout_height="wrap_content":layout_marginLeft="0dp":layout_marginRight="0dp":layout_marginTop="0dp":text="TextView":textSize="18sp":layout_constraintLeft_toLeftOf="parent":layout_constraintRight_toRightOf="parent":layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

ПРИЛОЖЕНИЕ 8

(обязательное)

 

Листинг операции LoginActivity

package demidov.docrecognition;

 android.animation.Animator;

import android.animation.AnimatorListenerAdapter;

import android.annotation.TargetApi;

import android.content.ContentValues;

import android.content.Intent;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

import android.os.AsyncTask;

import android.os.Build;

import android.os.Bundle;

import android.support.v7.app.AppCompatActivity;

import android.text.TextUtils;

import android.view.KeyEvent;

import android.view.View;

import android.view.View.OnClickListener;

import android.view.inputmethod.EditorInfo;

import android.widget.AutoCompleteTextView;

import android.widget.Button;

import android.widget.EditText;

import android.widget.TextView;

import com.google.common.hash.Hashing;

import demidov.docrecognition.database.DbHelper;

import demidov.docrecognition.database.DocRecognizerContract;

public class LoginActivity extends AppCompatActivity {

private UserLoginTask mAuthTask = null;

private AutoCompleteTextView mUsernameView;

private EditText mPasswordView;

private View mProgressView;

private View mLoginFormView;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);(R.layout.activity_login);

= (AutoCompleteTextView) findViewById(R.id.username);

= (EditText) findViewById(R.id.password);.setOnEditorActionListener(new TextView.OnEditorActionListener() {

@Override

public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {

if (id == R.id.login || id == EditorInfo.IME_NULL) {();

return true;

}

return false;

}

});

Button signInButton = (Button) findViewById(R.id.sign_in_button);.setOnClickListener(new OnClickListener() {

@Override

public void onClick(View view) {();

}

});

= findViewById(R.id.login_form);= findViewById(R.id.login_progress);

}

private void attemptLogin() {

if (mAuthTask != null) {

return;

.setError(null);.setError(null);

String username = mUsernameView.getText().toString();

String password = mPasswordView.getText().toString();

cancel = false;focusView = null;

if (TextUtils.isEmpty(password) || !isPasswordValid(password)) {.setError(getString(R.string.password_invalid));= mPasswordView;= true;

}

if (TextUtils.isEmpty(username) || !isEmailValid(username)) {.setError(getString(R.string.username_invalid));= mUsernameView;= true;

}

if (cancel) {.requestFocus();

} else {(true);= new UserLoginTask(username, password);.execute((Void) null);

}

}

private boolean isEmailValid(String username) {

return username.length() >= 4;

}

private boolean isPasswordValid(String password) {

return password.length() >= 4;

}

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)

private void showProgress(final boolean show) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime);

.setVisibility(show ? View.GONE : View.VISIBLE);.animate().setDuration(shortAnimTime).alpha(? 0 : 1).setListener(new AnimatorListenerAdapter() {

@Override

public void onAnimationEnd(Animator animation) {.setVisibility(show ? View.GONE : View.VISIBLE);

}

});

.setVisibility(show ? View.VISIBLE : View.GONE);.animate().setDuration(shortAnimTime).alpha(? 1 : 0).setListener(new AnimatorListenerAdapter() {

@Override

public void onAnimationEnd(Animator animation) {.setVisibility(show ? View.VISIBLE : View.GONE);

}

});

} else {.setVisibility(show ? View.VISIBLE : View.GONE);.setVisibility(show ? View.GONE : View.VISIBLE);

}

}

public class UserLoginTask extends AsyncTask<Void, Void, Long> {

private final String username;

private final String password;

private final DbHelper dbHelper = new DbHelper(LoginActivity.this);

(String username, String password) {

this.username = username;

this.password = password;

}

@Override

protected Long doInBackground(Void... params) {

String passwordHash = Hashing.sha256().hashUnencodedChars(password).toString();

db = dbHelper.getWritableDatabase();

Cursor cursor = db.query(DocRecognizerContract.User.TABLE_NAME,

new String[]{DocRecognizerContract.User._ID, DocRecognizerContract.User.COLUMN_NAME_PASSWORD},.User.COLUMN_NAME_USERNAME + " = ?",

new String[]{username}, null, null, null);

userExists = cursor.moveToNext();

userId;

if (userExists) {passwordValid = cursor.getString(1).equals(passwordHash);

if (!passwordValid) {

return null;

}= cursor.getLong(0);

} else {contentValues = new ContentValues();.put(DocRecognizerContract.User.COLUMN_NAME_USERNAME,);.put(DocRecognizerContract.User.COLUMN_NAME_PASSWORD,);= db.insert(DocRecognizerContract.User.TABLE_NAME, null, contentValues);

}

.close();

return userId;

}

@Override

protected void onPostExecute(final Long userId) {= null;(false);

if (userId != null) {

((DocRecogApplication) getApplication()).setUserId(userId);intent = new Intent(LoginActivity.this, OperationActivity.class);(intent);

} else {.setError(getString(R.string.error_incorrect_password));.requestFocus();

}

}

@Override

protected void onCancelled() {= null;(false);

}

}

}

ПРИЛОЖЕНИЕ 9

(обязательное)


Листинг операции OperationActivity

package demidov.docrecognition;

import android.Manifest;

import android.content.Intent;

import android.content.pm.PackageManager;

import android.os.Bundle;

import android.support.annotation.NonNull;

import android.support.v4.app.ActivityCompat;

import android.support.v4.content.ContextCompat;

import android.support.v7.app.AppCompatActivity;

import android.view.View;

import android.widget.RadioButton;

import android.widget.RadioGroup;

public class OperationActivity extends AppCompatActivity {

private static final int PERMISSIONS_REQUEST_CAMERA = 1;

private long userId;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);(R.layout.activity_operation);

}

public void selectOperation(View view) {operation = (RadioButton) view;checked = operation.isChecked();

switch (operation.getId()) {

case R.id.createDoc:

if (checked) {(R.id.docTypes).setVisibility(View.VISIBLE);(R.id.continueButton).setVisibility(View.INVISIBLE);

}

break;

case R.id.existingDoc:

if (checked) {(R.id.docTypes).setVisibility(View.GONE);(R.id.continueButton).setVisibility(View.VISIBLE);

}

break;

}

}

public void next(View view) {operation = (RadioButton) findViewById(

((RadioGroup) findViewById(R.id.operations)).getCheckedRadioButtonId());

switch (operation.getId()) {

case R.id.createDoc:();

break;

case R.id.existingDoc:intent = new Intent(this, DocListActivity.class);.putExtra("userId", userId);(intent);

break;

}

}

public void selectDocType(View view) {docType = (RadioButton) view;checked = docType.isChecked();

if (checked) {(R.id.continueButton).setVisibility(View.VISIBLE);

}

}

private void requestPermissions() {permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);

if (permissionCheck == PackageManager.PERMISSION_GRANTED) {();

} else {.requestPermissions(this, new String[] {Manifest.permission.CAMERA}, PERMISSIONS_REQUEST_CAMERA);

}

}

@Override

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

if (requestCode == PERMISSIONS_REQUEST_CAMERA) {

if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {();

}

}

}

private void processToRecognition() {buttonId = ((RadioGroup) findViewById(R.id.docTypes)).getCheckedRadioButtonId();intent = new Intent(this, RecognitionActivity.class);.putExtra("documentType", buttonId);.putExtra("userId", userId);(intent);

}

}

ПРИЛОЖЕНИЕ 10

(обязательное)

 

Листинг операции DocumentActivity

package demidov.docrecognition;

import android.content.ContentValues;

import android.content.Intent;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

import android.os.Bundle;

import android.support.v7.app.AppCompatActivity;

import android.view.View;

import android.widget.EditText;

import com.google.common.collect.ImmutableMap;

import com.google.gson.Gson;

import com.google.gson.GsonBuilder;

import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;

import java.util.HashMap;

import java.util.Map;

import demidov.docrecognition.database.DbHelper;

import demidov.docrecognition.database.DocRecognizerContract;

public class DocumentActivity extends AppCompatActivity {

private static final Map<PassportField, Integer> passFieldsToViewIds = ImmutableMap.<PassportField, Integer>builder()

.put(PassportField.ISSUER_NAME, R.id.passIssuerName)

.put(PassportField.ISSUER_CODE, R.id.passIssuerCode)

.put(PassportField.ISSUING_DATE, R.id.passIssuingDate)

.put(PassportField.NUMBER, R.id.passNumber)

.put(PassportField.LAST_NAME, R.id.lastName)

.put(PassportField.FIRST_NAME, R.id.firstName)

.put(PassportField.MIDDLE_NAME, R.id.middleName)

.put(PassportField.GENDER, R.id.gender)

.put(PassportField.BIRTH_DATE, R.id.birthDate)

.put(PassportField.BIRTH_PLACE, R.id.birthPlace)

.build();

private static final Map<SnilsField, Integer> snilsFieldsToViewIds = ImmutableMap.<SnilsField, Integer>builder()

.put(SnilsField.SNILS, R.id.snilsNumber)

.put(SnilsField.LAST_NAME, R.id.snilsLastName)

.put(SnilsField.FIRST_NAME, R.id.snilsFirstName)

.put(SnilsField.MIDDLE_NAME, R.id.snilsMiddleName)

.put(SnilsField.BIRTH_DATE, R.id.snilsBirthDate)

.put(SnilsField.BIRTH_PLACE, R.id.snilsBirthPlace)

.put(SnilsField.GENDER, R.id.snilsGender)

.put(SnilsField.REGISTRATION_DATE, R.id.snilsIssuingDate)

.build();

private final DbHelper dbHelper = new DbHelper(this);

private DocumentType documentType;

private long userId;

private Map<DocumentField, String> fields;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);(R.layout.activity_document);

bundle = getIntent().getExtras();documentFields = bundle.getBundle("documentFields");

= DocumentType.valueOf(bundle.getString("documentType"));= ((DocRecogApplication) getApplication()).getUserId();

Long documentId = (Long) getIntent().getExtras().get("documentId");

readOnly = true;

if (documentId == null) {= new HashMap<>();

if (documentType.equals(DocumentType.PASSPORT)) {

for (String key : documentFields.keySet()) {field = PassportField.valueOf(key);.put(field, documentFields.getString(key));

}

} else if (documentType.equals(DocumentType.SOCIAL_SECURITY)) {

for (String key : documentFields.keySet()) {field = SnilsField.valueOf(key);.put(field, documentFields.getString(key));

}

}= false;

} else {= loadDocument(documentId);= getDocumentType(documentId);

}

if (readOnly) {(R.id.saveButton).setVisibility(View.GONE);

}

if (documentType.equals(DocumentType.PASSPORT)) {(R.id.passportVerification).setVisibility(View.VISIBLE);(R.id.snilsVerification).setVisibility(View.GONE);

} else if (documentType.equals(DocumentType.SOCIAL_SECURITY)) {(R.id.snilsVerification).setVisibility(View.VISIBLE);(R.id.passportVerification).setVisibility(View.GONE);

}

for (Map.Entry<DocumentField, String> entry : fields.entrySet()) {fieldId;

if (documentType.equals(DocumentType.PASSPORT)) {= passFieldsToViewIds.get(entry.getKey());

} else {= snilsFieldsToViewIds.get(entry.getKey());

}textView = (EditText) findViewById(fieldId);.setText(entry.getValue());.setFocusable(!readOnly);

}

}

public void saveData(View view) {db = dbHelper.getWritableDatabase();

String documentType = null;

switch (this.documentType) {

case PASSPORT:= DocRecognizerContract.DocumentType.VALUE_CODE_PASSPORT;

break;

case SOCIAL_SECURITY:= DocRecognizerContract.DocumentType.VALUE_CODE_SNILS;

break;

}

Cursor cursor = db.query(DocRecognizerContract.DocumentType.TABLE_NAME,

new String[]{DocRecognizerContract.DocumentType._ID},.DocumentType.COLUMN_NAME_CODE + " = ?",

new String[] {documentType}, null, null, null);.moveToFirst();docTypeId = cursor.getLong(.getColumnIndexOrThrow(DocRecognizerContract.DocumentType._ID)

);.close();

gson = new GsonBuilder().create();

String json = gson.toJson(fields);docValues = new ContentValues();.put(DocRecognizerContract.Document.COLUMN_NAME_DOC_TYPE_ID, docTypeId);.put(DocRecognizerContract.Document.COLUMN_NAME_DATA, json);.put(DocRecognizerContract.Document.COLUMN_NAME_USER_ID, userId);.insert(DocRecognizerContract.Document.TABLE_NAME, null, docValues);

intent = new Intent(this, OperationActivity.class);.putExtra("userId", userId);(intent);

}

private Map<DocumentField, String> loadDocument(long documentId) {db = dbHelper.getWritableDatabase();

Cursor cursor = db.rawQuery("SELECT " + DocRecognizerContract.Document.TABLE_NAME + "."

+ DocRecognizerContract.Document.COLUMN_NAME_DATA + ", " + DocRecognizerContract.DocumentType.TABLE_NAME

+ "." + DocRecognizerContract.DocumentType.COLUMN_NAME_CODE

+ " FROM " + DocRecognizerContract.Document.TABLE_NAME + " JOIN "

+ DocRecognizerContract.DocumentType.TABLE_NAME + " ON " + DocRecognizerContract.Document.TABLE_NAME

+ "." + DocRecognizerContract.Document.COLUMN_NAME_DOC_TYPE_ID + " = "

+ DocRecognizerContract.DocumentType.TABLE_NAME + "." + DocRecognizerContract.DocumentType._ID

+ " WHERE " + DocRecognizerContract.Document.TABLE_NAME + "." + DocRecognizerContract.Document._ID

+ " = ?", new String[] {String.valueOf(documentId)});

.moveToNext();

String documentData = cursor.getString(0);

String documentType = cursor.getString(1);.close();

type = null;

switch (documentType) {

case DocRecognizerContract.DocumentType.VALUE_CODE_PASSPORT:= new TypeToken<Map<PassportField, String>>(){}.getType();

break;

case DocRecognizerContract.DocumentType.VALUE_CODE_SNILS:= new TypeToken<Map<SnilsField, String>>(){}.getType();

break;

}

gson = new GsonBuilder().create();

<DocumentField, String> result = gson.fromJson(documentData, type);

return result;

}

private DocumentType getDocumentType(Long documentId) {db = dbHelper.getReadableDatabase();

Cursor cursor = db.rawQuery("SELECT " + DocRecognizerContract.DocumentType.TABLE_NAME

+ "." + DocRecognizerContract.DocumentType.COLUMN_NAME_CODE

+ " FROM " + DocRecognizerContract.Document.TABLE_NAME + " JOIN "

+ DocRecognizerContract.DocumentType.TABLE_NAME + " ON " + DocRecognizerContract.Document.TABLE_NAME

+ "." + DocRecognizerContract.Document.COLUMN_NAME_DOC_TYPE_ID + " = "

+ DocRecognizerContract.DocumentType.TABLE_NAME + "." + DocRecognizerContract.DocumentType._ID

+ " WHERE " + DocRecognizerContract.Document.TABLE_NAME + "." + DocRecognizerContract.Document._ID

+ " = ?", new String[] {String.valueOf(documentId)});

documentType = null;.moveToNext();

switch (cursor.getString(0)) {

case DocRecognizerContract.DocumentType.VALUE_CODE_PASSPORT:= DocumentType.PASSPORT;

break;

case DocRecognizerContract.DocumentType.VALUE_CODE_SNILS:= DocumentType.SOCIAL_SECURITY;

break;

}

return documentType;

}

}

ПРИЛОЖЕНИЕ 11

(обязательное)

 

Листинг операции DocListActivity

package demidov.docrecognition;

import android.content.Intent;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

import android.os.Bundle;

import android.support.v7.app.AppCompatActivity;

import android.view.View;

import android.widget.AdapterView;

import android.widget.ListView;

import android.widget.SimpleCursorAdapter;

import demidov.docrecognition.database.DbHelper;

import demidov.docrecognition.database.DocRecognizerContract;

public class DocListActivity extends AppCompatActivity {

private final DbHelper dbHelper = new DbHelper(this);

private long userId;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);(R.layout.activity_doc_list);

listView = (ListView) findViewById(R.id.documentList);

final SQLiteDatabase db = dbHelper.getWritableDatabase();

final String alias = "DOC_NAME";

final Cursor cursor = db.rawQuery("SELECT " + DocRecognizerContract.Document.TABLE_NAME + "."

+ DocRecognizerContract.Document._ID + ", " + DocRecognizerContract.DocumentType.TABLE_NAME

+ "." + DocRecognizerContract.DocumentType.COLUMN_NAME_NAME + " || \" \" || "

+ DocRecognizerContract.Document.TABLE_NAME + "." + DocRecognizerContract.Document._ID

+ " AS " + alias + " FROM " + DocRecognizerContract.Document.TABLE_NAME + " JOIN "

+ DocRecognizerContract.DocumentType.TABLE_NAME + " ON " + DocRecognizerContract.Document.TABLE_NAME

+ "." + DocRecognizerContract.Document.COLUMN_NAME_DOC_TYPE_ID + " = "

+ DocRecognizerContract.DocumentType.TABLE_NAME + "." + DocRecognizerContract.DocumentType._ID

+ " WHERE " + DocRecognizerContract.Document.TABLE_NAME + "."

+ DocRecognizerContract.Document.COLUMN_NAME_USER_ID + " = ?", new String[]{String.valueOf(userId)});

String[] from = {alias};[] to = {R.id.textView2};

final SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.list_item, cursor, from, to);.setAdapter(adapter);

.setOnItemClickListener(new AdapterView.OnItemClickListener() {

@Override

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

intent = new Intent(getApplicationContext(), DocumentActivity.class);.putExtra("userId", userId);.putExtra("documentId", id);

(intent);

}

});

.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {

@Override

public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {.delete(DocRecognizerContract.Document.TABLE_NAME, DocRecognizerContract.Document._ID

+ " = ?", new String[] {String.valueOf(id)});.changeCursor(db.rawQuery("SELECT " + DocRecognizerContract.Document.TABLE_NAME + "."

+ DocRecognizerContract.Document._ID + ", " + DocRecognizerContract.DocumentType.TABLE_NAME

+ "." + DocRecognizerContract.DocumentType.COLUMN_NAME_NAME + " || \" \" || "

+ DocRecognizerContract.Document.TABLE_NAME + "." + DocRecognizerContract.Document._ID

+ " AS " + alias + " FROM " + DocRecognizerContract.Document.TABLE_NAME + " JOIN "

+ DocRecognizerContract.DocumentType.TABLE_NAME + " ON " + DocRecognizerContract.Document.TABLE_NAME

+ "." + DocRecognizerContract.Document.COLUMN_NAME_DOC_TYPE_ID + " = "

+ DocRecognizerContract.DocumentType.TABLE_NAME + "." + DocRecognizerContract.DocumentType._ID, null));

return true;

}

});

}

}

ПРИЛОЖЕНИЕ 12

(обязательное)

 

Листинг операции RecognitionActivity

package demidov.docrecognition;

import android.app.Activity;

import android.hardware.Camera;

import android.os.Bundle;

import android.support.annotation.Nullable;

import android.util.Log;

import android.view.View;

import org.bytedeco.javacpp.tesseract;

import java.io.File;

import java.io.FileOutputStream;

import java.io.InputStream;

import static org.bytedeco.javacpp.opencv_core.Mat;

public class RecognitionActivity extends Activity implements CvCameraPreview.CvCameraViewListener, View.OnClickListener {

final String TAG = "RecognitionActivity";

private CvCameraPreview cameraView;

private tesseract.TessBaseAPI api;

private BaseDocRecognitionService documentExtractor;

private int documentTypeId;

@Override

protected void onCreate(@Nullable Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

(R.layout.activity_recognition);

();= (CvCameraPreview) findViewById(R.id.camera_view);.setCvCameraViewListener(this);(R.id.recorder_control).setOnClickListener(this);

= getIntent().getExtras().getInt("documentType");

}

@Override

protected void onPause() {

super.onPause();

.setVisibility(View.GONE);

}

@Override

switch (documentTypeId) {

case (R.id.passport):= new PassportRecognitionService(height, width, camera, api, getApplicationContext(), this, cameraView);

break;

case (R.id.socialSecurity):= new SnilsRecognitionService(height, width, camera, api, getApplicationContext(), this, cameraView);

break;

}

}

@Override

public void onCameraViewStopped() {

}

@Override

public Mat onCameraFrame(Mat rgbaMat) {

return documentExtractor.processPreview(rgbaMat);

}

@Override

public void onClick(View v) {.takeFullSizePicture();

}

private void initTesseract() {

File f = new File(getCacheDir(), "tessdata/rus.traineddata");

if (!f.exists()) try {

InputStream is = getAssets().open("tessdata/rus.traineddata");size = is.available();[] buffer = new byte[size];.read(buffer);.close();

.getParentFile().mkdirs();

FileOutputStream fos = new FileOutputStream(f);.write(buffer);.close();

} catch (Exception e) {

throw new RuntimeException(e);

}= new tesseract.TessBaseAPI();

if (api.Init(getCacheDir().getPath(), "rus") != 0) {.d(TAG, "Could not initialize tesseract");

}

}

}

ПРИЛОЖЕНИЕ 13

(обязательное)

 

Листинг класса BaseDocRecognitionService

package demidov.docrecognition;

import android.content.Context;

import android.content.Intent;

import android.hardware.Camera;

import android.os.Bundle;

import com.google.common.collect.ArrayListMultimap;

import com.google.common.collect.Multimap;

import com.google.common.collect.SortedSetMultimap;

import com.google.common.collect.TreeMultimap;

import org.bytedeco.javacpp.opencv_core;

import org.bytedeco.javacpp.opencv_core.Mat;

import org.bytedeco.javacpp.opencv_core.MatVector;

import org.bytedeco.javacpp.opencv_core.Rect;

import org.bytedeco.javacv.FFmpegFrameFilter;

import org.bytedeco.javacv.Frame;

import org.bytedeco.javacv.FrameFilter;

import org.bytedeco.javacv.OpenCVFrameConverter;

import java.nio.ByteBuffer;

import java.util.Collection;

import java.util.Comparator;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import static org.bytedeco.javacpp.avutil.AV_PIX_FMT_NV21;

import static org.bytedeco.javacpp.opencv_imgproc.CHAIN_APPROX_NONE;

import static org.bytedeco.javacpp.opencv_imgproc.CV_RGBA2GRAY;

import static org.bytedeco.javacpp.opencv_imgproc.INTER_AREA;

import static org.bytedeco.javacpp.opencv_imgproc.RETR_EXTERNAL;

import static org.bytedeco.javacpp.opencv_imgproc.approxPolyDP;

import static org.bytedeco.javacpp.opencv_imgproc.boundingRect;

import static org.bytedeco.javacpp.opencv_imgproc.cvtColor;

import static org.bytedeco.javacpp.opencv_imgproc.findContours;

import static org.bytedeco.javacpp.opencv_imgproc.rectangle;

import static org.bytedeco.javacpp.opencv_imgproc.resize;

public abstract class BaseDocRecognitionService {

private final Camera camera;

private final Context applicationContext;

private final Context context;

private final CvCameraPreview cameraView;

final Map<DocumentField, Rect> fieldsSearchingAreas;

final double previewToPictureRatio;

final Rect mask;

public BaseDocRecognitionService(int width, int height, Camera camera, Context applicationContext, Context context, CvCameraPreview cameraView) {

this.mask = getMask(width, height);

this.camera = camera;

this.previewToPictureRatio = ((double) camera.getParameters().getPreviewSize().height) /.getParameters().getPictureSize().height;

this.fieldsSearchingAreas = calculateSearchingAreas(mask.size());

this.applicationContext = applicationContext;

this.context = context;

this.cameraView = cameraView;

}

public Mat processPreview(Mat image) {<DocumentField, Rect> detectedFields =  detect(image);

if (isAllFieldsFound(detectedFields.keySet())) {();

}

preview = visualizeRects(image, detectedFields);

return preview;

}

public Multimap<DocumentField, Rect> detect(Mat image) {croppedImage = image.apply(mask);(croppedImage, croppedImage, CV_RGBA2GRAY);(croppedImage);(croppedImage);

return detectFields(croppedImage);

}

public Mat visualizeRects(Mat input, Multimap<DocumentField, Rect> fields) {croppedImage = input.apply(mask);(croppedImage, fields);(croppedImage, fieldsSearchingAreas);

return recoverToSize(input.size(), mask, croppedImage,.bytedeco.javacpp.helper.opencv_core.AbstractScalar.WHITE);

}

private boolean isAllFieldsFound(Collection<DocumentField> fields) {

return fields.containsAll(getAllFields());

}

takeFullSizePicture() {

try {.takePicture(null, null, new Camera.PictureCallback() {

@Override

public void onPictureTaken(byte[] data, Camera camera) {.Size pictureSize = camera.getParameters().getPictureSize();

Frame filteredFrame = filterFrame(data, pictureSize.width, pictureSize.height);.ToMat converterToMat = new OpenCVFrameConverter.ToMat();mat = converterToMat.convert(filteredFrame);

smallImage = new Mat();(mat, smallImage, new opencv_core.Size(),, previewToPictureRatio, INTER_AREA);

<DocumentField, Rect> detectedFields =  detect(smallImage);<DocumentField, String> fieldValues = recognize(mat, sortFields(detectedFields));

(fieldValues);

}

});

} catch (RuntimeException e) {

}

}

private Frame filterFrame(byte[] data, int width, int height) {

Frame frame = new Frame(width, height, Frame.DEPTH_UBYTE, 2);

((ByteBuffer) frame.image[0].position(0)).put(data);

filter = new FFmpegFrameFilter("transpose=1,format=pix_fmts=rgba", width, height);.setPixelFormat(AV_PIX_FMT_NV21);

Frame filteredFrame = null;

try {.start();.push(frame);= filter.pull();.stop();

} catch (FrameFilter.Exception e) {.printStackTrace();

}

return filteredFrame;

}

processToVerification(Map<DocumentField, String> fieldValues) {intent = new Intent(applicationContext, DocumentActivity.class);bundle = new Bundle();

for (Map.Entry<DocumentField, String> entry : fieldValues.entrySet()) {.putString(entry.getKey().getName(), entry.getValue());

}.putExtra("documentFields", bundle);.putExtra("documentType", getDocumentType().name());.startActivity(intent);

}

abstract Map<DocumentField, String> recognize(Mat image, Multimap<DocumentField, Rect> fields);

abstract DocumentType getDocumentType();

scaleRect(Rect input) {x = (int) ((mask.x() + input.x()) / previewToPictureRatio);y = (int) ((mask.y() + input.y()) / previewToPictureRatio);width = (int) (input.width() / previewToPictureRatio);height = (int) (input.height() / previewToPictureRatio);

return new Rect(x, y, width, height);

}

private Rect getMask(int width, int height) {docRatio = getRatio();x = 0, y = 0;

if (((double) width)/height > docRatio) {desiredWidth = height * docRatio;margin = (int) ((width - desiredWidth) / 2);=  margin;= (int) desiredWidth;

} else {desiredHeight = width / docRatio;margin = (int) ((height - desiredHeight) / 2);=  margin;= (int) desiredHeight;

}

return new Rect(x, y, width, height);

}

abstract void findEdges(Mat image);

abstract void applyMorphology(Mat input);

private Multimap<DocumentField, Rect> detectFields(Mat input) {<DocumentField, Rect> detectedFields = ArrayListMultimap.create();contours = new MatVector();(input, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);

for (int i = 0; i < contours.size(); i++) {contour = new Mat();(contours.get(i), contour, 3, true);rect = boundingRect(contour);.x(rect.x() - 3);.y(rect.y() - 3);.width(rect.width() + 6);.height(rect.height() + 6);field = getFieldCorrespondingTo(rect);

if (field != null) {.put(field, rect);

}

}

return detectedFields;

}

private void drawFieldRects(Mat originalImage, Multimap<DocumentField, Rect> fields) {

for (Rect rect : fields.values()) {(originalImage, rect, new opencv_core.Scalar(255, 0, 0, 0));

}

}

private void drawSearchingAreas(Mat originalImage, Map<DocumentField, Rect> areas) {

for (Rect rect : areas.values()) {(originalImage, rect, new opencv_core.Scalar(0, 255, 0, 0));

}

}

private DocumentField getFieldCorrespondingTo(Rect textRect) {

for (Map.Entry<DocumentField, Rect> entry : fieldsSearchingAreas.entrySet()) {value = entry.getValue();

if (value.contains(textRect.br()) && value.contains(textRect.tl())) {

return entry.getKey();

}

}

return null;

}

private Mat recoverToSize(opencv_core.Size size, Rect mask,image, opencv_core.Scalar backgroundColor) {result = new Mat(size, image.type(), backgroundColor);.copyTo(result.apply(mask));

return result;

}

abstract double getRatio();

abstract List<DocumentField> getAllFields();

<DocumentField, Rect> sortFields(Multimap<DocumentField, Rect> fieldsToSort) {<DocumentField, Rect> result = TreeMultimap.create(new Comparator<DocumentField>() {

@Override

public int compare(DocumentField o1, DocumentField o2) {

if (o1.getRelativeSearchingArea().y() > o2.getRelativeSearchingArea().y()) {

return 1;

} else if (o1.getRelativeSearchingArea().y() < o2.getRelativeSearchingArea().y()) {

return -1;

} else if (o1.getRelativeSearchingArea().x() > o2.getRelativeSearchingArea().x()) {

return 1;

} else if (o1.getRelativeSearchingArea().x() < o2.getRelativeSearchingArea().x()) {

return -1;

} else {

return 0;

}

}

}, new Comparator<Rect>() {

@Override

public int compare(Rect o1, Rect o2) {

return o1.y() - o2.y();

}

});.putAll(fieldsToSort);

return result;

}

private Map<DocumentField, Rect> calculateSearchingAreas(opencv_core.Size imageSize) {<DocumentField, Rect> result = new HashMap<>();

for (DocumentField field : getAllFields()) {_core.Rectd relativeSearchingArea = field.getRelativeSearchingArea();x = (int) (imageSize.width() * relativeSearchingArea.x());y = (int) (imageSize.height() * relativeSearchingArea.y());width = (int) (imageSize.width() * relativeSearchingArea.width());height = (int) (imageSize.height() * relativeSearchingArea.height());.put(field, new Rect(x, y, width, height));

}

return result;

}

}

ПРИЛОЖЕНИЕ 14

(обязательное)

 

Листинг класса PassportRecognitionService

package demidov.docrecognition;

import android.content.Context;

import android.hardware.Camera;

import com.google.common.collect.Multimap;

import org.bytedeco.javacpp.BytePointer;

import org.bytedeco.javacpp.opencv_core.Mat;

import org.bytedeco.javacpp.opencv_core.Rect;

import org.bytedeco.javacpp.opencv_core.Size;

import org.bytedeco.javacpp.tesseract.TessBaseAPI;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import static org.bytedeco.javacpp.opencv_imgproc.ADAPTIVE_THRESH_MEAN_C;

import static org.bytedeco.javacpp.opencv_imgproc.CV_MOP_CLOSE;

import static org.bytedeco.javacpp.opencv_imgproc.CV_MOP_OPEN;

import static org.bytedeco.javacpp.opencv_imgproc.CV_RGBA2GRAY;

import static org.bytedeco.javacpp.opencv_imgproc.Canny;

import static org.bytedeco.javacpp.opencv_imgproc.MORPH_ELLIPSE;

import static org.bytedeco.javacpp.opencv_imgproc.MORPH_RECT;

import static org.bytedeco.javacpp.opencv_imgproc.THRESH_BINARY;

import static org.bytedeco.javacpp.opencv_imgproc.adaptiveThreshold;

import static org.bytedeco.javacpp.opencv_imgproc.cvtColor;

import static org.bytedeco.javacpp.opencv_imgproc.getStructuringElement;

import static org.bytedeco.javacpp.opencv_imgproc.morphologyEx;

public class CardRecognitionService extends BaseDocRecognitionService {

private final Size closingKernelSize;

private final Size openingKernelSize;

private final int thresholdingKernelSize;

private final Size recognitionClosingKernelSize;

private final TessBaseAPI tesseractApi;

public CardRecognitionService(int width, int height, Camera camera, TessBaseAPI tesseractApi, Context applicationContext, Context context, CvCameraPreview cameraView) {

super(width, height, camera, applicationContext, context, cameraView);

this.tesseractApi = tesseractApi;

fontSize = mask.height() * 14.0 / 185;

this.closingKernelSize = new Size((int) (2 * fontSize), (int) (0.1 * fontSize));

this.openingKernelSize = new Size((int) (0.9 * fontSize), (int) (0.9 * fontSize));

thresholdingKernelSize = (int) (2 * fontSize / previewToPictureRatio);

this.thresholdingKernelSize = thresholdingKernelSize % 2 == 0 ? thresholdingKernelSize + 1

: thresholdingKernelSize;

this.recognitionClosingKernelSize = new Size((int) (0.1 * fontSize / previewToPictureRatio),

(int) (0.08 * fontSize / previewToPictureRatio));

}

@Override<DocumentField, String> recognize(Mat image, Multimap<DocumentField, Rect> fields) {<DocumentField, String> recognizedFields = new HashMap<>();(image, image, CV_RGBA2GRAY);

for (Map.Entry<DocumentField, Rect> entry : fields.entries()) {wordRect = scaleRect(entry.getValue());word = new Mat();.apply(wordRect).copyTo(word);

(word, word, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, thresholdingKernelSize, 10);(word, word, CV_MOP_CLOSE, getStructuringElement(MORPH_ELLIPSE, recognitionClosingKernelSize));

.TesseractRect(word.data(), 1, word.cols(), 0, 0, word.cols(), word.rows());text = tesseractApi.GetUTF8Text();

String fieldValue = recognizedFields.get(entry.getKey());

if (text != null) {

if (fieldValue == null) {.put(entry.getKey(), text.getString());

} else {.put(entry.getKey(), fieldValue + " " + text.getString());

}

}

}

return recognizedFields;

}

@OverrideapplyMorphology(Mat image) {(image, image, CV_MOP_CLOSE,(MORPH_RECT, closingKernelSize));(image, image, CV_MOP_OPEN, getStructuringElement(MORPH_RECT, openingKernelSize));

}

@OverridegetRatio() {

return 85.6 / 53.98;

}

@Override

List<DocumentField> getAllFields() {

return new ArrayList<DocumentField>(Arrays.asList(CardField.values()));

}

@OverridefindEdges(Mat image) {(image, image, CV_RGBA2GRAY);(image, image, 150, 400, 3, false);

}

@OverridegetDocumentType() {

return DocumentType.CARD;

}

}

ПРИЛОЖЕНИЕ 15

(обязательное)

 

Листинг класса SnilsRecognitionService

package demidov.docrecognition;

import android.content.Context;

import android.hardware.Camera;

import com.google.common.collect.Multimap;

import org.bytedeco.javacpp.BytePointer;

import org.bytedeco.javacpp.opencv_core.Mat;

import org.bytedeco.javacpp.opencv_core.Rect;

import org.bytedeco.javacpp.opencv_core.Size;

import org.bytedeco.javacpp.tesseract.TessBaseAPI;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import static org.bytedeco.javacpp.opencv_imgproc.ADAPTIVE_THRESH_MEAN_C;

import static org.bytedeco.javacpp.opencv_imgproc.CV_MOP_CLOSE;

import static org.bytedeco.javacpp.opencv_imgproc.CV_MOP_OPEN;

import static org.bytedeco.javacpp.opencv_imgproc.CV_RGBA2GRAY;

import static org.bytedeco.javacpp.opencv_imgproc.Canny;

import static org.bytedeco.javacpp.opencv_imgproc.MORPH_ELLIPSE;

import static org.bytedeco.javacpp.opencv_imgproc.MORPH_RECT;

import static org.bytedeco.javacpp.opencv_imgproc.THRESH_BINARY;

import static org.bytedeco.javacpp.opencv_imgproc.adaptiveThreshold;

import static org.bytedeco.javacpp.opencv_imgproc.cvtColor;

import static org.bytedeco.javacpp.opencv_imgproc.getStructuringElement;

import static org.bytedeco.javacpp.opencv_imgproc.morphologyEx;

public class SnilsRecognitionService extends BaseDocRecognitionService {

private final Size closingKernelSize;

private final Size openingKernelSize;

private final int thresholdingKernelSize;

private final Size recognitionClosingKernelSize;

private final TessBaseAPI tesseractApi;

public SnilsRecognitionService(int width, int height, Camera camera, TessBaseAPI tesseractApi, Context applicationContext, Context context, CvCameraPreview cameraView) {

super(width, height, camera, applicationContext, context, cameraView);

this.tesseractApi = tesseractApi;

fontSize = mask.height() * 7.0 / 200;

this.closingKernelSize = new Size((int) (2 * fontSize), (int) (0.1 * fontSize));

this.openingKernelSize = new Size((int) (0.9 * fontSize), (int) (0.9 * fontSize));

thresholdingKernelSize = (int) (2 * fontSize / previewToPictureRatio);

this.thresholdingKernelSize = thresholdingKernelSize % 2 == 0 ? thresholdingKernelSize + 1

: thresholdingKernelSize;

this.recognitionClosingKernelSize = new Size((int) (0.1 * fontSize / previewToPictureRatio),

(int) (0.08 * fontSize / previewToPictureRatio));

}

@Override<DocumentField, String> recognize(Mat image, Multimap<DocumentField, Rect> fields) {<DocumentField, String> recognizedFields = new HashMap<>();(image, image, CV_RGBA2GRAY);

for (Map.Entry<DocumentField, Rect> entry : fields.entries()) {wordRect = scaleRect(entry.getValue());word = new Mat();.apply(wordRect).copyTo(word);

(word, word, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, thresholdingKernelSize, 10);(word, word, CV_MOP_CLOSE, getStructuringElement(MORPH_ELLIPSE, recognitionClosingKernelSize));

.TesseractRect(word.data(), 1, word.cols(), 0, 0, word.cols(), word.rows());text = tesseractApi.GetUTF8Text();

String fieldValue = recognizedFields.get(entry.getKey());

if (text != null) {

if (fieldValue == null) {.put(entry.getKey(), text.getString());

} else {.put(entry.getKey(), fieldValue + " " + text.getString());

}

}

}

return recognizedFields;

}

@OverrideapplyMorphology(Mat image) {(image, image, CV_MOP_CLOSE,(MORPH_RECT, closingKernelSize));(image, image, CV_MOP_OPEN, getStructuringElement(MORPH_RECT, openingKernelSize));

}

@OverridegetRatio() {

return 85.0/55;

}

@Override

List<DocumentField> getAllFields() {

return new ArrayList<DocumentField>(Arrays.asList(SnilsField.values()));

}

@OverridefindEdges(Mat image) {(image, image, 100, 250, 3, false);

}

@OverridegetDocumentType() {

return DocumentType.SOCIAL_SECURITY;

}

}

ПРИЛОЖЕНИЕ 16

(обязательное)

 

Листинг класса DocumentField

package demidov.docrecognition;

import org.bytedeco.javacpp.opencv_core;

public interface DocumentField {

String getName();

_core.Rectd getRelativeSearchingArea();

}

ПРИЛОЖЕНИЕ 17

(обязательное)

 

Листинг класса PassportField

package demidov.docrecognition;

import org.bytedeco.javacpp.opencv_core;

public enum PassportField implements DocumentField {

_NAME("Кем выдан", new opencv_core.Rectd(29.0/426, 40.0/600, 371.0/426, 85.0/600)),_DATE("Дата выдачи", new opencv_core.Rectd(29.0/426, 110.0/600, 154.0/426, 40.0/600)),_CODE("Код подразделения", new opencv_core.Rectd(187.0/426, 110.0/600, 209.0/426, 40.0/600)),_NAME("Фамилия", new opencv_core.Rectd(149.0/426, 341.0/600, 216.0/426, 45.0/600)),_NAME("Имя", new opencv_core.Rectd(149.0/426, 380.0/600, 239.0/426, 30.0/600)),_NAME("Отчество", new opencv_core.Rectd(149.0/426, 405.0/600, 239.0/426, 30.0/600)),("Пол", new opencv_core.Rectd(149.0/426, 430.0/600, 68.0/426, 30.0/600)),_DATE("Дата рождения", new opencv_core.Rectd(241.0/426, 430.0/600, 147.0/426, 30.0/600)),_PLACE("Место рождения", new opencv_core.Rectd(149.0/426, 455.0/600, 246.0/426, 80.0/600)),("Номер паспорта", new opencv_core.Rectd(390.0/426, 63.0/600, 30.0/426, 167.0/600));

private String fieldName;

private opencv_core.Rectd relativeSearchingArea;

(String fieldName, opencv_core.Rectd relativeSearchingArea) {

this.relativeSearchingArea = relativeSearchingArea;

}

public String getName() {

return this.name();

}

@Override

public opencv_core.Rectd getRelativeSearchingArea() {

return relativeSearchingArea;

}

}

ПРИЛОЖЕНИЕ 18

(обязательное)

 

Листинг класса SnilsField

package demidov.docrecognition;

import org.bytedeco.javacpp.opencv_core;

public enum SnilsField implements DocumentField {

("СНИЛС", new opencv_core.Rectd(53.0/300, 47.0/200, 166.0/300, 29.0/200)),_NAME("Фамилия", new opencv_core.Rectd(45.0/300, 71.0/200, 141.0/300, 20.0/200)),_NAME("Имя", new opencv_core.Rectd(45.0/300, 83.0/200, 141.0/300, 20.0/200)),_NAME("Отчество", new opencv_core.Rectd(45.0/300, 94.0/200, 141.0/300, 20.0/200)),_DATE("Дата рождения", new opencv_core.Rectd(115.0/300, 100.0/200, 161.0/300, 30.0/200)),_PLACE("Место рождения", new opencv_core.Rectd(17.0/300, 121.0/200, 180.0/300, 45.0/200)),("Пол", new opencv_core.Rectd(35.0/300, 160.0/200, 64.0/300, 20.0/200)),_DATE("Дата регистрации", new opencv_core.Rectd(90.0/300, 175.0/200, 112.0/300, 20.0/200));

private String fieldName;

private opencv_core.Rectd relativeSearchingArea;(String fieldName, opencv_core.Rectd relativeSearchingArea) {

this.fieldName = fieldName;

this.relativeSearchingArea = relativeSearchingArea;

}

public String getName() {

return this.name();

}

@Override

public opencv_core.Rectd getRelativeSearchingArea() {

return relativeSearchingArea;

}

}

ПРИЛОЖЕНИЕ 19

(обязательное)

 

Листинг класса DocRecognizerContract

package demidov.docrecognition.database;

import android.provider.BaseColumns;

public final class DocRecognizerContract {

private DocRecognizerContract() {

}

public static class Document implements BaseColumns {

public static final String TABLE_NAME = "DOCUMENT";

public static final String COLUMN_NAME_DOC_TYPE_ID = "DOC_TYPE_ID";

public static final String COLUMN_NAME_USER_ID = "USER_ID";

public static final String COLUMN_NAME_INSERT_DATE = "INSERT_DATE";

public static final String COLUMN_NAME_DATA = "DATA";

}

public static class DocumentType implements BaseColumns {

public static final String TABLE_NAME = "DOCUMENT_TYPE";

public static final String COLUMN_NAME_CODE = "CODE";

public static final String COLUMN_NAME_NAME = "NAME";

public static final String VALUE_CODE_PASSPORT = "PASSPORT";

public static final String VALUE_CODE_SNILS = "SNILS";

public static final String VALUE_NAME_PASSPORT = "Паспорт";

public static final String VALUE_NAME_SNILS = "СНИЛС";

}

public static class User implements BaseColumns {

public static final String TABLE_NAME = "USER";

public static final String COLUMN_NAME_USERNAME = "USERNAME";

public static final String COLUMN_NAME_PASSWORD = "PASSWORD";

}

}

ПРИЛОЖЕНИЕ 20

(обязательное)

 

Листинг класса DbHelper

package demidov.docrecognition.database;

import android.content.ContentValues;

import android.content.Context;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteOpenHelper;

import static demidov.docrecognition.database.DocRecognizerContract.DocumentType.COLUMN_NAME_CODE;

import static demidov.docrecognition.database.DocRecognizerContract.DocumentType.COLUMN_NAME_NAME;

import static demidov.docrecognition.database.DocRecognizerContract.DocumentType.VALUE_CODE_PASSPORT;

import static demidov.docrecognition.database.DocRecognizerContract.DocumentType.VALUE_CODE_SNILS;

import static demidov.docrecognition.database.DocRecognizerContract.DocumentType.VALUE_NAME_PASSPORT;

import static demidov.docrecognition.database.DocRecognizerContract.DocumentType.VALUE_NAME_SNILS;

public class DbHelper extends SQLiteOpenHelper {

private static final int DATABASE_VERSION = 3;

private static final String DATABASE_NAME = "DocRecognizer.db";

private static final String SQL_CREATE_DOCUMENT =

"CREATE TABLE " + DocRecognizerContract.Document.TABLE_NAME + " (" +.Document._ID + " INTEGER PRIMARY KEY," +.Document.COLUMN_NAME_INSERT_DATE + " TEXT," +.Document.COLUMN_NAME_DATA + " TEXT," +.Document.COLUMN_NAME_USER_ID + " INTEGER," +.Document.COLUMN_NAME_DOC_TYPE_ID + " INTEGER)";

private static final String SQL_CREATE_DOCUMENT_TYPE =

"CREATE TABLE " + DocRecognizerContract.DocumentType.TABLE_NAME + " (" +.DocumentType._ID + " INTEGER PRIMARY KEY," +.DocumentType.COLUMN_NAME_NAME + " TEXT," +.DocumentType.COLUMN_NAME_CODE + " TEXT)";

private static final String SQL_CREATE_USER =

"CREATE TABLE " + DocRecognizerContract.User.TABLE_NAME + " (" +.User._ID + " INTEGER PRIMARY KEY," +.User.COLUMN_NAME_USERNAME + " TEXT," +.User.COLUMN_NAME_PASSWORD + " TEXT)";

private static final String SQL_DROP_DOCUMENT =

"DROP TABLE IF EXISTS " + DocRecognizerContract.Document.TABLE_NAME;

private static final String SQL_DROP_DOCUMENT_TYPE =

"DROP TABLE IF EXISTS " + DocRecognizerContract.DocumentType.TABLE_NAME;

private static final String SQL_DROP_USER =

"DROP TABLE IF EXISTS " + DocRecognizerContract.User.TABLE_NAME;

public DbHelper(Context context) {

super(context, DATABASE_NAME, null, DATABASE_VERSION);

}

@Override

public void onCreate(SQLiteDatabase db) {.execSQL(SQL_CREATE_DOCUMENT);.execSQL(SQL_CREATE_DOCUMENT_TYPE);.execSQL(SQL_CREATE_USER);

docTypeValues = new ContentValues();.put(COLUMN_NAME_CODE, VALUE_CODE_PASSPORT);.put(COLUMN_NAME_NAME, VALUE_NAME_PASSPORT);.insert(DocRecognizerContract.DocumentType.TABLE_NAME, null, docTypeValues);= new ContentValues();.put(COLUMN_NAME_CODE, VALUE_CODE_SNILS);.put(COLUMN_NAME_NAME, VALUE_NAME_SNILS);.insert(DocRecognizerContract.DocumentType.TABLE_NAME, null, docTypeValues);

}

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {.execSQL(SQL_DROP_DOCUMENT);.execSQL(SQL_DROP_DOCUMENT_TYPE);.execSQL(SQL_DROP_USER);(db);

}

@Override

public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {(db, oldVersion, newVersion);

}

}

ПРИЛОЖЕНИЕ 21

(обязательное)

 

Листинг класса DocRecogApplication

package demidov.docrecognition;

import android.app.Application;

public class DocRecogApplication extends Application {

private Long userId;

public Long getUserId() {

return userId;

}

public void setUserId(Long userId) {

this.userId = userId;

}

}

ПРИЛОЖЕНИЕ 22

(обязательное)

 

Листинг класса DocumentType

package demidov.docrecognition;

public enum DocumentType {

,_SECURITY

}

ПРИЛОЖЕНИЕ 23

(обязательное)

 

Листинг файла конфигурации Gradle build.gradle

apply plugin: 'com.android.application'

{25"25.0.2"{"demidov.docrecognition"2125true1"1.0""android.support.test.runner.AndroidJUnitRunner"

}{{falsegetDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

}

}

}

{fileTree(dir: 'libs', include: ['*.jar'])('com.android.support.test.espresso:espresso-core:2.2.2', {group: 'com.android.support', module: 'support-annotations'group: 'com.google.code.findbugs'

})group: 'org.bytedeco', name: 'javacv', version: '1.3.2'group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '3.2.0-1.3', classifier: 'android-arm'group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '3.2.1-1.3', classifier: 'android-arm'group: 'org.bytedeco.javacpp-presets', name: 'tesseract', version: '3.04.01-1.3'group: 'org.bytedeco.javacpp-presets', name: 'tesseract', version: '3.04.01-1.3', classifier: 'android-arm'group: 'org.bytedeco.javacpp-presets', name: 'leptonica', version: '1.73-1.3', classifier: 'android-arm''com.android.support:appcompat-v7:25.3.1''com.android.support.constraint:constraint-layout:1.0.2''com.android.support:design:25.3.1''com.google.guava:guava:22.0-android''com.google.code.gson:gson:2.8.0''junit:junit:4.12'

}

Похожие работы на - Программные средства для заполнения базы персональных данных

 

Не нашли материал для своей работы?
Поможем написать уникальную работу
Без плагиата!