16.06, ООО «ВК»
16.06, ООО «ВК»
16.06, ООО «ВК»

Как встроить распознавание паспорта РФ в Android: пошаговое руководство

Логотип компании Smart Engines
Отредактировано

Разбираемся, как реализовать нативное приложение с возможностью распознавания ДУЛ прямо на устройстве

158 открытий2К показов
Как встроить распознавание паспорта РФ в Android: пошаговое руководство

Привет, Tproger!

Мы в Smart Engines занимаемся разработкой софта для распознавания самых разных документов – начиная от паспорта РФ, свидетельства о рождении и заканчивая первичкой вроде УПД или ТОРГ-12, а также банковских карт, номеров телефона и баркодов.

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

Объяснять (и хвастаться) проще всего на примере – покажем, как развернуть распознавание паспорта РФ прямо на устройстве на Android.

Зачем это всё?

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

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

Немного вводных

SDK Smart ID Engine – это библиотека, собранная под нативную платформу, конфигурационный бандл и обёртка для вызова функций библиотеки с помощью других языков программирования (в этой статье нам понадобится java).

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

  • Движок распознавания.  Он создаётся с указанием конфигурационного бандла и представляет собой что-то вроде “склада инструментов” для распознавания различных документов. Движок создаётся в самом начале, существует на всём протяжении жизни (приложения или хотя бы сценария, в котором используется распознавание). От него создаются следующие элементы интерфейса:
  • Сессия распознавания. Это сам процесс распознавания конкретного физического объекта – паспорта, ВУ, СТС, загранпаспорта и других. Всего по состоянию на сегодняшний день мы поддерживаем более 3 тысяч типов документов.  В сессию можно передать всего одно изображение, а можно - серию изображений последовательно (видеопоток), при этом после каждого нового распознавания результат будет уточняться, а так же на основе уверенности в результате будет приниматься решение, нужно ли распознавать дальше или лучше уже не будет. 

Важная оговорка: Вообще сессии бывают разных типов - помимо распознавания документов также существуют сессия сравнения лиц (сравнить два изображения на степень “похожести” или провести проверку определения живости лица. Кстати, это происходит без выявления биометрических дескрипторов). А также сессия файловой форензики (поиск признаков вмешательства в исходное изображение) и другие.

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

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

Как это встраивается и работает

А теперь немного кода - покажем, как делается распознавание документа с помощью камеры устройства на Android. Полный код можно найти, перейдя по ссылке на GitHub, а ниже – наиболее интересные моменты.

Начнём с создания движка:

Объявим класс:

			interface Engine {
    val isVideoModeAllowed:Boolean
    fun createEngine (bundle: ByteArray?) 
    fun createSession(target: SessionTarget, sessionType: SessionType): Session
}

		

И приведём его имплементацию:

			class IdEngineWrapper(
    private val signature : String
): Engine {
    lateinit var idEngine: IdEngine
        private set
    override val isVideoModeAllowed = true

    override fun createEngine(bundle: ByteArray?){
        // Create SDK engine
        idEngine = if(bundle!=null) IdEngine.Create(bundle, true)
    }

    override fun createSession(target: SessionTarget, sessionType: SessionType): Session {

        // Create new session settings object
        val sessionSettings: IdSessionSettings = idEngine.CreateSessionSettings()

        // Fill by the target
        target.fillSessionSettings(sessionSettings)

        return IdEngineSession(idEngine, sessionSettings, signature)
    }
}

		

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

			interface SessionTarget{
    fun fillSessionSettings(sessionSettings:Any)
}

		

С имплементацией:

			data class IdTarget(
    val mode : String,
    val mask : String
) : AppTarget {
    override val name: String
        get() = "$mode : $mask"
    override val description: String?
        get() = if(mode=="anydoc") "Any document" else null
    override val cropImage: Boolean
        get() = false // do not crop images for id-engine

    override fun fillSessionSettings(sessionSettings:Any){
        with(sessionSettings as IdSessionSettings) {
            // Set current mode
            SetCurrentMode(mode)

            // Set document mask
            AddEnabledDocumentTypes(mask)
        }
    }
}

		

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

			/**
* IMAGE PROCESSOR
* Implements the document recognition process from a sequence of images (the main business logic)
*/
class ImageProcessor() {

  // State
  private val _state = MutableStateFlow<ImageProcessingState>( ImageProcessingState.READY )
  private fun setState(newState:ImageProcessingState) {
      Log.w(TAG," >>> state: $newState")
      _state.value = newState
  }
  val state = _state.asStateFlow()

  //----------------------------------------------------------------------------------------------
  // EVENTS
  fun startProcessing(engine : Engine, target: SessionTarget, sessionType: SessionType, photo: Bitmap?){
      processingJob = scope.launch {
          process(engine, target, sessionType, photo)
      }
  }
  fun addFrame(frame:Frame){
      scope.launch {
          try {
              frameChannel!!.send(frame)
          } catch (e: Exception) {
              frame.close()
          }
      }
  }
  fun stopProcessing(){
      processingJob?.cancel()
  }
  fun finish(){
      setState( ImageProcessingState.READY )
  }

  //----------------------------------------------------------------------------------------------
  // IMPLEMENTATION
  private val scope : CoroutineScope = CoroutineScope(Dispatchers.IO)
  private var processingJob : Job? = null
  private var frameChannel : Channel<Frame>?=null // link to the processing coroutine
  private suspend fun process(engine : Engine, target: SessionTarget, sessionType: SessionType, photo: Bitmap?){
      Log.w(TAG,"processing thread started:  ${Thread.currentThread().id} ${Thread.currentThread().name}")
      var session:Session?=null
      try {
          // SET PROCESSING STATE
          setState(
              if(photo!=null) ImageProcessingState.PHOTO_PROCESSING(
                  target = target,
                  visualization = null,
                  photo = photo
              )
              else ImageProcessingState.VIDEO_PROCESSING(
                  target = target,
                  visualization = null,
              )
          )

          // CREATE SESSION
          session = engine.createSession(
              target = target,
              sessionType
          )

          // Session visualization
          var lastFrameSize = android.util.Size(1,1)
          val visualization = Visualization(
                  quadsPrimary = MutableStateFlow<List<QuadFrame>>(emptyList()).apply {
                      // Subscribe to the session data
                      scope.launch {
                          session.quadsPrimary?.collect{
                              emit(value + QuadFrame(
                                  quads = it,
                                  imageSize = lastFrameSize
                              ))
                          }
                      }
                  },
                  quadsSecondary = MutableStateFlow<List<QuadFrame>>(emptyList()).apply {
                      // Subscribe to the session data
                      scope.launch {
                          session.quadsSecondary?.collect{
                              emit(value + QuadFrame(
                                  quads = it,
                                  imageSize = lastFrameSize
                              ))
                          }
                      }
                  },
                 instruction   = session.instruction
              )
          // PREPARE CHANNEL Image queue (for the "thread")
          frameChannel = Channel<Frame>(capacity = photo?.let { 1 }?: 0).apply {
              // Fill channel immediately for photo mode
              photo?.let { send( Frame(bitmap = photo, imageProxy = null)) }
          }
          val framesLimit = photo?.let { 1 }?: Int.MAX_VALUE
          var framesProcessed = 0

          // SET PROCESSING STATE
          setState(
              if(photo!=null) ImageProcessingState.PHOTO_PROCESSING(target, visualization, photo)
              else            ImageProcessingState.VIDEO_PROCESSING(target, visualization)
          )

          // THE MAIN LOOP
          while (!session.isTerminal && !session.isSelfieCheckRequired && framesProcessed<framesLimit){

              // Receive the next frame
              val frame = frameChannel!!.receive() // BLOCKING CODE!!!
              framesProcessed++
              lastFrameSize = with(frame.bitmap){android.util.Size(width,height)}

              val image = frame.bitmap

              // DO PROCESS
              val timeStart  = System.currentTimeMillis()
              session.processImage(image)
              val timeFinish = System.currentTimeMillis()

              // Free resources
              frame.close()
          }

          // SET FINISHED STATE
          setState(
              if(session.isSelfieCheckRequired) ImageProcessingState.SELFIE_CHECKING(session)
              else                              ImageProcessingState.FINISHED(session)
          )

      } catch (e: kotlinx.coroutines.CancellationException) {
          setState(
              if(session!=null) ImageProcessingState.FINISHED(session)
              else ImageProcessingState.READY// session is not created yet
          )
      }catch (e:Exception){
          setState(ImageProcessingState.ERROR(e))
      }
      processingJob = null
      frameChannel = null
  }
}

		

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

			/**
* PARSE IdResult => IdResultData
*/
fun IdResult.parse(): IdResultData?{
   // DOC TYPE
   val docType = GetDocumentType()
   if(docType.isNullOrEmpty()) return null
   return IdResultData(
       docType = docType,
       textFields     = parseTextFields(),
       images         = parseImages(),
       forensicCheckFields = parseForensicCheckFields(),
       forensicTextFields  = parseForensicTextFields(),
       forensicImages      = parseForensicImages(),
   )
}
//--------------------------------------------------------------------------------------------------
// ATTRIBUTES
/**
* PARSE StringsMapIterator => Attribute
*/
fun IdBaseFieldInfo.parseAttributes():List<Attribute>{
   return ArrayList<Attribute>().apply {
       val iterator = AttributesBegin()
       val iterEnd  = AttributesEnd()
       while (!iterator.Equals(iterEnd)) {
           add(
               Attribute(
                   key   = iterator.GetKey(),
                   value = iterator.GetValue()
               )
           )
           iterator.Advance()
       }
   }
}
//--------------------------------------------------------------------------------------------------
// TEXT FIELDS
/**
* IdResult => TEXT FIELDS
*/
fun IdResult.parseTextFields():List<TextField>{
   return ArrayList<TextField>().apply {
       val iterator = TextFieldsBegin()
       val iterEnd  = TextFieldsEnd()
       while (!iterator.Equals(iterEnd)) {
           add(iterator.parseTextField())
           iterator.Advance()
       }
   }
}
/**
* PARSE IdTextFieldsMapIterator => FieldInfo
*/
fun IdTextFieldsMapIterator.parseTextField(): TextField {
   val info = GetValue().GetBaseFieldInfo()
   return  TextField(
       key         = GetKey(),
       value       = GetValue().GetValue().GetFirstString().GetCStr(),
       isAccepted  = info.GetIsAccepted(),
       attr        = info.parseAttributes()
   )
}
//--------------------------------------------------------------------------------------------------
// IMAGES
/**
* IdResult => IMAGES
*/
fun IdResult.parseImages():List<ImageField>{
   return ArrayList<ImageField>().apply {
       val iterator = ImageFieldsBegin()
       val iterEnd  = ImageFieldsEnd()
       while (!iterator.Equals(iterEnd)) {
           try {
               add(iterator.parseImage())
           }catch(e:Exception){
               Log.e(TAG,"IdResult.parseImages",e)
           }
           iterator.Advance()
       }
   }
}
/**
* PARSE IdTextFieldsMapIterator => ImageInfo
*/
fun IdImageFieldsMapIterator.parseImage(): ImageField {
   val info = GetValue().GetBaseFieldInfo()
   val base64String = GetValue().GetValue().GetBase64String().GetCStr()
   val base64Buf = Base64.decode(base64String, Base64.DEFAULT)
   return  ImageField(
       key         = GetKey(),
       value       = BitmapFactory.decodeByteArray(base64Buf, 0, base64Buf.size),
       isAccepted  = info.GetIsAccepted(),
       attr        = info.parseAttributes()
   )
}
//--------------------------------------------------------------------------------------------------
// FORENSICS
/**
* IdResult => FORENSIC CHECK FIELDS
*/
fun IdResult.parseForensicCheckFields():List<TextField>{
   return ArrayList<TextField>().apply {
       val iterator = ForensicCheckFieldsBegin()
       val iterEnd  = ForensicCheckFieldsEnd()
       while (!iterator.Equals(iterEnd)) {
           add(iterator.parseForensicCheckField())
           iterator.Advance()
       }
   }
}
/**
* PARSE IdCheckFieldsMapIterator => FieldInfo
*/
fun IdCheckFieldsMapIterator.parseForensicCheckField(): TextField {
   val info = GetValue().GetBaseFieldInfo()
   return  TextField(
       key         = GetKey(),
       value       = GetValue().GetValue().toString(),
       isAccepted  = info.GetIsAccepted(),
       attr        = info.parseAttributes()
   )
}
/**
* IdResult => FORENSIC TEXT FIELDS
*/
fun IdResult.parseForensicTextFields():List<TextField>{
   return ArrayList<TextField>().apply {
       val iterator = ForensicTextFieldsBegin()
       val iterEnd  = ForensicTextFieldsEnd()
       while (!iterator.Equals(iterEnd)) {
           add(iterator.parseTextField())
           iterator.Advance()
       }
   }
}
/**
* IdResult => IMAGES
*/
fun IdResult.parseForensicImages():List<ImageField>{
   return ArrayList<ImageField>().apply {
       val iterator = ForensicImageFieldsBegin()
       val iterEnd  = ForensicImageFieldsEnd()
       while (!iterator.Equals(iterEnd)) {
           add(iterator.parseImage())
           iterator.Advance()
       }
   }
}

		

Теперь вы знаете, что встроить распознавание паспорта РФ в приложение для Android — намного проще, чем может показаться на первый взгляд.

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

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

Если заинтересовались – добро пожаловать на наш сайт. И не расходимся, скоро расскажем еще много чего интересного!

Следите за новыми постами
Следите за новыми постами по любимым темам
158 открытий2К показов