Reconnaître des textes sur Android Things avec ABBYY RTR SDK et django

Salut Je m'appelle Azat Kalmykov, je suis un étudiant de deuxième année de la Faculté de Mathématiques Appliquées et d'Informatique de la Haute Ecole d'Economie et stagiaire au département de développement mobile d'ABBYY. Dans cet article je vais vous parler de mon petit projet, qui a été réalisé dans le cadre d'un stage d'été.



Imaginez un petit convoyeur. Des marchandises ou des détails vont de pair, sur lesquels il est important de reconnaître le texte (c'est peut-être un identifiant unique, ou peut-être quelque chose de plus intéressant). Les colis en sont un bon exemple. Le fonctionnement du convoyeur est contrôlé à distance par un opérateur qui surveille les dysfonctionnements et dans ce cas résout les problèmes. Qu'est-ce qui peut l'aider avec ça? Un appareil basé sur la plate-forme Android Things peut être une bonne solution: il est mobile, facile à configurer et peut fonctionner via le Wi-Fi. Nous avons décidé d'essayer d'utiliser les technologies ABBYY et de découvrir comment elles conviennent à de telles situations - reconnaissance de texte dans un flux sur des «appareils non standard» de la catégorie de l'Internet des objets. Nous allons délibérément simplifier beaucoup de choses, car nous ne faisons que construire un concept. Si vous êtes intéressé, bienvenue au chat.


Android Things


Une merveilleuse chose appelée Android Things Starter Kit est venue à notre bureau ABBYY de la conférence Google I / O. La bonté n'a pas disparu, et nous voulions jouer avec elle à la recherche de différents scénarios d'utilisation de nos bibliothèques de reconnaissance . Vous devez d'abord assembler notre appareil, puis exécuter. Il n'est pas difficile de le faire, suivez plutôt strictement les instructions du fabricant.


En savoir plus sur la plateforme ici et ici .


Ce qui m'est venu
image
Et à la fin de l'article, je vais vous montrer à quoi ressemble l'appareil assemblé


On fait quoi?


Nous allons écrire une application pour la plate-forme Android Things qui traitera l'image de la caméra, en envoyant du texte reconnu et (périodiquement) des images à notre serveur afin que l'opérateur conditionnel puisse comprendre ce qui se passe sur le pipeline. Le serveur sera écrit en django.


Je m'empresse de noter que pour la mise en œuvre de ce projet, vous n'aurez pas besoin d'investissements, ainsi que d'enregistrement et de SMS (d'accord, sur AWS, vous devez toujours vous inscrire et obtenir un compte gratuit).


Nous lançons fusée dans l'espace serveur


Nous supposerons que vous disposez déjà d'un compte AWS gratuit. Nous lierons notre carte pour que l'Amazonie maléfique, en cas d'insouciance, nous déduise quelques shekels. Nous utiliserons AWS EC2 et créerons une machine virtuelle sur Ubuntu Server 18.04 LTS (HVM) avec le type de volume SSD. Pour cet OS et lorsque vous utilisez un compte gratuit, une seule configuration est disponible, sélectionnez-la (ne vous inquiétez pas, un gigaoctet de RAM nous suffit). Nous allons créer une clé ssh (ou en utiliser une déjà prête) et essayer de nous connecter à notre machine. Nous allons également créer une IP élastique (quelque chose comme une IP statique) et la lier immédiatement à notre machine. Notez que Elastic IP, qui n'est lié à aucune machine virtuelle, vous coûtera de l'argent pendant le développement.


Nous sommes connectés au serveur. Nous installons la boîte à outils nécessaire sur la machine.


La troisième version de Python est préinstallée. La question est laissée aux petits.


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

Installez le docker, nous en aurons besoin plus tard.


 $ sudo apt-get install docker.io 

Vous devez également ouvrir le port 8000. Nous l'utiliserons lors de l'utilisation du service Web. Le port 22 pour ssh est ouvert par défaut.


Hourra! Nous avons maintenant un ordinateur distant pour exécuter nos applications. Nous écrirons le code directement sur le serveur.


Django (+ chaînes)


J'ai décidé d'utiliser django, car cela vous permettra de créer rapidement et facilement un petit service web. Une bibliothèque de canaux django supplémentaire nous permettra de travailler avec des sockets web (à savoir béquille diffusion en transférant des images sans rafraîchir la page).


Créez un répertoire dans lequel placer le projet. Installez django avec les canaux django, sans dévier des instructions de la documentation .


 $ 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 

Nous créons un projet. Nous aurons 3 sous-répertoires. Le principal aura le même nom - mysite (créé automatiquement), les deux autres - streaming et téléchargement. Le premier sera chargé d'afficher les informations sur une page Web, et le second - de les télécharger via l'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 

Configurez les canaux Django. Mettez la ligne en commentaire avec WSGI_APPLICATION et ajoutez-en une avec ASGI_APPLICATION. Maintenant, notre application fonctionnera de manière asynchrone.


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

Nous mettons également à jour la valeur de la liste 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', ] # ... 

L'architecture


Nous allons écrire le code basé sur le tutoriel officiel des canaux django. La structure de notre petit service ressemblera à ceci:


MYIP: 8000 / frame - une page Web qui montrera le résultat, conditionnellement, la page que l'opérateur regarde
MYIP: 8000 / upload / upload_text / - adresse pour une requête POST, envoi de texte reconnu
MYIP: 8000 / upload / upload_image / - adresse pour la demande PUT, envoi d'images individuelles


Il est nécessaire d'enregistrer cette logique dans les fichiers urls.py des répertoires correspondants.


 # 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


Nous passons à la description de la logique de notre 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'}) 

Page Web


Toutes les informations tiendront sur une seule page, donc la logique sera 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', {}) 

Nous devons écrire un petit document html pour afficher les résultats. Il aura un script intégré pour se connecter à une socket Web et le remplir de contenu.


 <!-- 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> 

Configurer le routage, les sockets


Quelle est la meilleure traduction du mot routage en russe? Essayons de sortir cette question de notre tête et posons-la (ou elle).


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

Vous devez maintenant enregistrer la logique de «transfert» (les fichiers routing.py sont similaires aux fichiers urls.py, seulement maintenant pour les 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), ] 

Et maintenant, nous implémentons FrameConsumer lui-même dans consommateurs.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'] })) 

Et enfin, avec des paumes moites, nous lançons.


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

Et maintenant sur Android


Nous utiliserons le SDK RTR d'ABBYY pour la reconnaissance de texte. Le pack Ultimate RTR SDK pour nos besoins peut être téléchargé ici . Nous implémentons une interface assez simple pour le traitement des frames, notre application sera basée sur un échantillon de l'archive téléchargée depuis le lien précédent (/ sample-textcapture). Nous allons jeter les parties excédentaires de l'application, la peaufiner un peu pour qu'elle fonctionne spécifiquement avec Android Things et implémenter la communication avec le serveur.


Le fichier de bibliothèque .aar se trouve dans le répertoire libs de l'archive téléchargée, nous l'importons. Nous copions le contenu du répertoire des actifs de l'archive (il y a essentiellement les fichiers nécessaires au processus de reconnaissance lui-même) dans les actifs de notre projet. Là, nous copions le fichier de licence à partir de l'archive, sans quoi l'application ne démarre pas.


Afin d'implémenter la fonctionnalité ABBYY RTR SDK dont nous avons besoin, nous devons créer un objet de type Engine, et en l'utilisant déjà un objet de type ITextCaptureService, que nous lancerons plus tard.


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

Dans ce cas, vous devez passer un objet de type ITextCaptureService.Callback, le créer directement dans notre classe MainActivity, il doit implémenter 3 méthodes.


 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) { //    } }; 

Nous avons délégué la réception du cadre à l'objet caméra. Je vais montrer ce qui se passe à l'intérieur.


 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); } }; 

Pour envoyer des messages, nous écrivons quelques classes, qui à leur tour délégueront leur travail à un objet de la classe 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 communication avec le réseau lui-même dans la classe Uploader est implémentée à l'aide de la bibliothèque pratique OkHttp3. Il vous permet de simplifier considérablement l'interaction avec le serveur.


Résultat


Nous obtenons une application client-serveur fonctionnelle reconnue par ABBYY, intégrée à l'Internet des objets - n'est-ce pas cool?


Un appareil récupéré et une petite publicité native de mon employeur
image


Texte reconnu
image


Panorama selfie avec un aperçu de plusieurs appareils
image


Vidos, à quoi tout cela peut ressembler dans la vraie vie



Dépôts sur github:


AndroidThingsTextRecognition-Backend
AndroidThingsTextRecognition-Android


Prenez et utilisez!

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


All Articles