Reconocimiento de textos sobre Android con ABBYY RTR SDK y django

Hola Mi nombre es Azat Kalmykov, soy estudiante de segundo año de la Facultad de Ciencias de la Computación, Matemática Aplicada y Ciencias de la Computación de la Escuela Superior de Economía y soy pasante en el departamento de desarrollo móvil de ABBYY. En este artículo hablaré sobre mi pequeño proyecto, que se llevó a cabo como parte de una pasantía de verano.



Imagina un pequeño transportador. Las mercancías o algunos detalles lo acompañan, en los cuales es importante reconocer el texto (tal vez este es un identificador único, o tal vez algo más interesante). Las parcelas son un buen ejemplo. La operación del transportador es controlada remotamente por un operador que monitorea el mal funcionamiento y en cuyo caso resuelve los problemas. ¿Qué puede ayudarlo con esto? Un dispositivo basado en la plataforma Android Things puede ser una buena solución: es móvil, fácil de configurar y puede funcionar a través de Wi-Fi. Decidimos intentar usar las tecnologías ABBYY y descubrir cómo son adecuadas para tales situaciones: reconocimiento de texto en una secuencia en "dispositivos no estándar" de la categoría de Internet de las cosas. Simplificaremos deliberadamente muchas cosas, ya que solo estamos construyendo un concepto. Si estás interesado, bienvenido a cat.


Cosas de Android


Una cosa maravillosa llamada Android Things Starter Kit llegó a nuestra oficina de ABBYY desde la conferencia Google I / O. La bondad no desapareció, y queríamos jugar con ella en busca de varios escenarios para usar nuestras bibliotecas de reconocimiento . Primero debe ensamblar nuestro dispositivo y luego ejecutarlo. No es difícil hacer esto, sino que sigue estrictamente las instrucciones del fabricante.


Lea más sobre la plataforma aquí y aquí .


Lo que vino a mis manos
imagen
Y al final de la publicación, te mostraré cómo se ve el dispositivo ensamblado


Que estamos haciendo


Escribiremos una aplicación para la plataforma Android Things que procesará la imagen desde la cámara, enviando texto reconocido y marcos (periódicamente) a nuestro servidor para que el operador condicional pueda entender lo que está sucediendo en la tubería. El servidor se escribirá en django.


Me apresuro a señalar que para la implementación de este proyecto no necesitará ninguna inversión, además de registro y SMS (está bien, en AWS aún necesita registrarse y obtener una cuenta gratuita).


Lanzamos cohete al espacio servidor


Asumiremos que ya tiene una cuenta gratuita de AWS. Ataremos nuestra tarjeta para que el malvado Amazon, en caso de nuestra imprudencia, deduzca un par de shekels de nosotros. Usaremos AWS EC2 y crearemos una máquina virtual en Ubuntu Server 18.04 LTS (HVM) con SSD Volume Type. Para este sistema operativo y cuando use una cuenta gratuita, solo hay una configuración disponible, selecciónela (no se preocupe, un gigabyte de RAM es suficiente para nosotros). Crearemos una clave ssh (o usaremos una que ya esté lista) e intentaremos conectarnos a nuestra máquina. También crearemos IP elástica (algo así como una IP estática) e inmediatamente la vincularemos a nuestra máquina. Tenga en cuenta que Elastic IP, que no está vinculado a ninguna máquina virtual, le costará dinero durante el desarrollo.


Estamos conectados al servidor. Instalamos el kit de herramientas necesario en la máquina.


La tercera versión de Python está preinstalada. El asunto se deja a los pequeños.


$ sudo apt-get update $ sudo apt-get install python3-pip $ sudo pip3 install virtualenv 

Instale la ventana acoplable, la necesitaremos más tarde.


 $ sudo apt-get install docker.io 

También debe abrir el puerto 8000. Lo usaremos cuando use el servicio web. El puerto 22 para ssh está abierto de forma predeterminada.


¡Hurra! Ahora tenemos una computadora remota para ejecutar nuestras aplicaciones. Escribiremos el código directamente en el servidor.


Django (+ canales)


Decidí usar django, ya que esto le permitirá crear rápida y fácilmente un pequeño servicio web. Una biblioteca adicional de canales django nos dará la oportunidad de trabajar con sockets web (es decir, hacer muleta transmitido mediante la transferencia de imágenes sin actualizar la página).


Cree un directorio en el que colocar el proyecto. Instale django junto con los canales de django, sin desviarse de las instrucciones en la documentación .


 $ mkdir Project $ cd Project $ virtualenv venv $ source venv/bin/activate $ pip install -U channels #       django $ pip install channels_redis #    Redis $ pip install djangorestframework $ django-admin startproject mysite $ cd mysite 

Creamos un proyecto. Tendremos 3 subdirectorios. El principal tendrá el mismo nombre: mysite (creado automáticamente), los otros dos, transmisión y carga. El primero será responsable de mostrar información en una página web y el segundo, de descargarlo a través de la API REST.


 $ python3 manage.py startapp streaming $ cd streaming $ rm -r migrations admin.py apps.py models.py tests.py $ cd .. $ python3 manage.py startapp uploading $ cd uploading $ rm -r migrations admin.py apps.py models.py tests.py 

Configurar canales django. Comente la línea con WSGI_APPLICATION y agregue una nueva con ASGI_APPLICATION. Ahora nuestra aplicación funcionará de forma asíncrona.


 # mysite/settings.py # ... # WSGI_APPLICATION = ... ASGI_APPLICATION = 'mysite.routing.application' # ... 

También actualizamos el valor de la lista INSTALLED_APPS.


 # mysite/settings.py # ... INSTALLED_APPS = [ 'channels', 'streaming', 'uploading', 'rest_framework', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] # ... 

Arquitectura


Escribiremos el código basado en el tutorial oficial de canales de django. La estructura de nuestro pequeño servicio se verá así:


MYIP: 8000 / frame: una página web que mostrará el resultado, condicionalmente, la página que está mirando el operador
MYIP: 8000 / upload / upload_text / - dirección para una solicitud POST, enviando texto reconocido
MYIP: 8000 / upload / upload_image / - dirección para la solicitud PUT, enviando imágenes individuales


Es necesario registrar esta lógica en los archivos urls.py de los directorios correspondientes.


 # mysite/mysite/urls.py from django.contrib import admin from django.conf.urls import include, url urlpatterns = [ url(r'^frame/', include('streaming.urls')), url(r'^upload/', include('uploading.urls')), ] 

API REST


Pasamos a la descripción de la lógica de nuestra API.


 # mysite/uploading/urls.py from django.conf.urls import url from rest_framework.urlpatterns import format_suffix_patterns from . import views urlpatterns = [ url(r'^upload_text/$', views.UploadTextView.as_view()), url(r'^upload_image/$', views.UploadImageView.as_view()), ] urlpatterns = format_suffix_patterns(urlpatterns) 

 # mysite/uploading/views.py from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from channels.layers import get_channel_layer from rest_framework.parsers import FileUploadParser from asgiref.sync import async_to_sync import base64 # Create your views here. class UploadTextView(APIView): def post(self, request, format=None): message = request.query_params['message'] if not message: raise ParseError("Empty content") channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)("chat", { "type": "chat.message", "message": message, }) return Response({'status': 'ok'}) class UploadImageView(APIView): parser_class = (FileUploadParser,) def put(self, request, format=None): if 'file' not in request.data: raise ParseError("Empty content") f = request.data['file'] channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)("chat", { "type": "chat.message", "image64": base64.b64encode(f.read()).decode("ascii"), }) return Response({'status': 'ok'}) 

Página web


Toda la información cabe en una página, por lo que la lógica será simple.


 # mysite/streaming/urls.py from django.conf.urls import url from . import views urlpatterns = [ url(r'^', views.index, name='index'), ] 

 # mysite/streaming/views.py from django.shortcuts import render from django.utils.safestring import mark_safe import json # Create your views here. def index(request): return render(request, 'index.html', {}) 

Necesitamos escribir un pequeño documento html para mostrar los resultados. Tendrá un script incorporado para conectarse a un socket web y llenarlo con contenido.


 <!-- mysite/streaming/templates/index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Live from Android Things</title> </head> <body> <textarea id="chat-log" cols="100" rows="20"></textarea><br/> <img id="frame"> </body> <script> var chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/'); chatSocket.onmessage = function(e) { var data = JSON.parse(e.data); var message = data['message']; var image64 = data['image64']; if (image64) { document.querySelector('#frame').setAttribute( 'src', 'data:image/png;base64,' + image64 ); } else if (message) { document.querySelector('#chat-log').value += (message + '\n'); } }; chatSocket.onclose = function(e) { console.error('Chat socket closed unexpectedly'); }; </script> </html> 

Configurar enrutamiento, sockets


¿Cuál es la mejor traducción del enrutamiento de palabras al ruso? Intentemos sacar esta pregunta de nuestra cabeza y establecerla (o ella).


 # mysite/mysite/settings.py # ... ALLOWED_HOSTS = ['*'] #  []  ['*'],    # ... CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, } 

Ahora debe registrar la lógica de "reenvío" (los archivos routing.py son similares a los archivos urls.py, solo que ahora para los sockets web).


 # mysite/mysite/routing.py from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import streaming.routing application = ProtocolTypeRouter({ # (http->django views is added by default) 'websocket': AuthMiddlewareStack( URLRouter( streaming.routing.websocket_urlpatterns ) ), }) 

 # mysite/streaming/routing.py from django.conf.urls import url from . import consumers websocket_urlpatterns = [ url(r'^ws/chat/$', consumers.FrameConsumer), ] 

Y ahora implementamos FrameConsumer en los consumidores .py


 # mysite/streaming/consumers.py from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer, JsonWebsocketConsumer import json class FrameConsumer(WebsocketConsumer): def connect(self): self.room_group_name = 'chat' # Join room group async_to_sync(self.channel_layer.group_add)( self.room_group_name, self.channel_name ) self.accept() def disconnect(self, close_code): # Leave room group async_to_sync(self.channel_layer.group_discard)( self.room_group_name, self.channel_name ) # Receive message from WebSocket def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # Send message to room group async_to_sync(self.channel_layer.group_send)( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # Receive message from room group def chat_message(self, event): if 'message' in event: # Send message to WebSocket self.send(text_data=json.dumps({ 'message': event['message'] })) elif 'image64' in event: self.send(text_data=json.dumps({ 'image64': event['image64'] })) 

Y finalmente, con palmas sudorosas lanzamos.


 $ docker run -p 6379:6379 -d redis:2.8 $ python manage.py runserver 0.0.0.0:8000 

Y ahora sobre Android


Utilizaremos el RTR SDK de ABBYY para el reconocimiento de texto. El paquete definitivo RTR SDK para nuestros propósitos se puede descargar aquí . Implementamos una interfaz bastante simple para procesar marcos, nuestra aplicación se basará en una muestra del archivo descargado del enlace anterior (/ sample-textcapture). Eliminaremos el exceso de partes de la aplicación, puliremos un poco para que funcione específicamente con Android Things e implementaremos la comunicación con el servidor.


El archivo .aar de la biblioteca se encuentra en el directorio libs del archivo descargado, importamos. Copiamos el contenido del directorio de activos del archivo (esencialmente los archivos necesarios para el proceso de reconocimiento en sí) en los activos de nuestro proyecto. Allí copiamos el archivo de licencia del archivo, sin él la aplicación no se iniciará.


Para implementar la funcionalidad ABBYY RTR SDK que necesitamos, necesitamos crear un objeto de tipo Engine, y usarlo ya un objeto de tipo ITextCaptureService, que luego lanzaremos.


 try { mEngine = Engine.load(this, LICENSE_FILE_NAME); mTextCaptureService = mEngine.createTextCaptureService(textCaptureCallback); return true; } // ... 

En este caso, debe pasar un objeto de tipo ITextCaptureService.Callback, crearlo directamente en nuestra clase MainActivity, debe implementar 3 métodos.


 private ITextCaptureService.Callback textCaptureCallback = new ITextCaptureService.Callback() { @Override public void onRequestLatestFrame(byte[] buffer) { //  ,       . //    . mCamera.addCallbackBuffer(buffer); } @Override public void onFrameProcessed( ITextCaptureService.TextLine[] lines, ITextCaptureService.ResultStabilityStatus resultStatus, ITextCaptureService.Warning warning) { //      ,    if (resultStatus.ordinal() >= 3) { //   ,     mSurfaceViewWithOverlay.setLines(lines, resultStatus); } else { //  ,     mSurfaceViewWithOverlay.setLines(null, ITextCaptureService.ResultStabilityStatus.NotReady); } //  warnings // ... } @Override public void onError(Exception e) { //    } }; 

Delegamos el recibo del marco al objeto de la cámara. Mostraré lo que está sucediendo adentro.


 private Camera.PreviewCallback cameraPreviewCallback = new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { //     (    ) if (!mIsUploading) { mIsUploading = true; //    new UploadImageTask(mCameraPreviewSize.width, mCameraPreviewSize.height).execute(data); } //     mTextCaptureService.submitRequestedFrame(data); } }; 

Para enviar mensajes, escribimos un par de clases, que a su vez delegarán su trabajo a un objeto de la clase Uploader.


 public static class UploadTextTask extends AsyncTask<String, Void, Void> { @Override protected Void doInBackground(String... params) { mUploader.uploadText(params[0]); return null; } } public static class UploadImageTask extends AsyncTask<byte[], Void, Void> { private int mCameraPreviewWidth; private int mCameraPreviewHeight; public UploadImageTask(int width, int height) { mCameraPreviewWidth = width; mCameraPreviewHeight = height; } @Override protected Void doInBackground(final byte[]... params) { byte[] jpegBytes = convertToJpegBytes(params[0]); if (jpegBytes != null) { mUploader.uploadImage(jpegBytes); } return null; } private byte[] convertToJpegBytes(byte[] rawBytes) { YuvImage yuvImage = new YuvImage( rawBytes, ImageFormat.NV21, mCameraPreviewWidth, mCameraPreviewHeight, null ); try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { yuvImage.compressToJpeg( new Rect(0, 0, mCameraPreviewWidth, mCameraPreviewHeight), 40, os ); return os.toByteArray(); } catch (IOException e) { Log.d(TAG, "compress error"); return null; } } // ... } 

La comunicación con la red en sí en la clase Uploader se implementa utilizando la conveniente biblioteca OkHttp3. Le permite simplificar enormemente la interacción con el servidor.


Resultado


Obtenemos una aplicación cliente-servidor que funciona con el reconocimiento de ABBYY, integrada en el Internet de las cosas.


Un dispositivo recopilado y una pequeña publicidad nativa de mi empleador
imagen


Texto reconocido
imagen


Selfie panorama con una visión general de varios dispositivos
imagen


Vidos, cómo puede verse todo en la vida real



Repositorios en github:


AndroidThingsTextRecognition-Backend
AndroidThingsTextRecognition-Android


¡Toma y usa!

Source: https://habr.com/ru/post/es432514/


All Articles