Hallo! Mein Name ist Azat Kalmykov, ich bin ein Student im zweiten Jahr der Fakultät für Angewandte Mathematik und Informatik der Informatik an der Higher School of Economics und ein Praktikant in der Abteilung für mobile Entwicklung von ABBYY. In diesem Artikel werde ich über mein kleines Projekt sprechen, das im Rahmen eines Sommerpraktikums durchgeführt wurde.

Stellen Sie sich ein kleines Förderband vor. Es gibt Waren oder einige Details, auf denen es wichtig ist, den Text zu erkennen (vielleicht ist dies eine eindeutige Kennung oder vielleicht etwas Interessanteres). Pakete sind ein gutes Beispiel. Der Betrieb des Förderers wird von einem Bediener ferngesteuert, der Störungen überwacht und in diesem Fall Probleme löst. Was kann ihm dabei helfen? Ein Gerät, das auf der Android Things-Plattform basiert, kann eine gute Lösung sein: Es ist mobil, einfach zu konfigurieren und kann über WLAN funktionieren. Wir haben uns entschlossen, ABBYY-Technologien zu verwenden und herauszufinden, wie sie für solche Situationen geeignet sind - Erkennung von Text in einem Stream auf „Nicht-Standardgeräten“ aus der Kategorie Internet der Dinge. Wir werden viele Dinge bewusst vereinfachen, da wir nur ein Konzept erstellen. Wenn Sie interessiert sind, willkommen bei Katze.
Android Dinge
Eine wunderbare Sache namens Android Things Starter Kit kam von der Google I / O- Konferenz in unser ABBYY-Büro. Die Güte verschwand nicht und wir wollten damit spielen, um nach verschiedenen Szenarien für die Verwendung unserer Erkennungsbibliotheken zu suchen. Zuerst müssen Sie unser Gerät zusammenbauen und dann ausführen. Es ist nicht schwierig, dies zu tun, sondern befolgen Sie die Anweisungen des Herstellers genau.
Lesen Sie hier und hier mehr über die Plattform.
Was kam in meine Hände

Und am Ende des Beitrags werde ich Ihnen zeigen, wie das zusammengebaute Gerät aussieht
Was machen wir
Wir werden eine Anwendung für die Android Things-Plattform schreiben, die das Bild von der Kamera verarbeitet und erkannten Text und (regelmäßig) Frames an unseren Server sendet, damit der bedingte Bediener verstehen kann, was in der Pipeline geschieht. Der Server wird in Django geschrieben.
Ich beeile mich zu bemerken, dass Sie für die Implementierung dieses Projekts keine Investitionen sowie Registrierung und SMS benötigen (okay, bei AWS müssen Sie sich noch registrieren und ein kostenloses Konto erhalten).
Wir starten Rakete in den Weltraum Server
Wir gehen davon aus, dass Sie bereits ein kostenloses AWS-Konto haben. Wir werden unsere Karte binden, damit der böse Amazonas im Falle unserer Rücksichtslosigkeit ein paar Schekel von uns abzieht. Wir werden AWS EC2 verwenden und eine virtuelle Maschine unter Ubuntu Server 18.04 LTS (HVM) mit SSD-Volume-Typ erstellen. Wählen Sie für dieses Betriebssystem und bei Verwendung eines kostenlosen Kontos nur eine Konfiguration aus (keine Sorge, ein Gigabyte RAM reicht für uns aus). Wir erstellen einen SSH-Schlüssel (oder verwenden einen bereits bereitgestellten) und versuchen, eine Verbindung zu unserem Computer herzustellen. Wir werden auch eine elastische IP (so etwas wie eine statische IP) erstellen und diese sofort an unseren Computer binden. Beachten Sie, dass Elastic IP, das nicht an eine virtuelle Maschine gebunden ist, Sie während der Entwicklung Geld kostet.
Wir sind mit dem Server verbunden. Wir installieren das notwendige Toolkit auf der Maschine.
Die dritte Python-Version ist vorinstalliert. Die Sache bleibt dem Kleinen überlassen.
$ sudo apt-get update $ sudo apt-get install python3-pip $ sudo pip3 install virtualenv
Installieren Sie den Docker, wir werden ihn später brauchen.
$ sudo apt-get install docker.io
Sie müssen auch Port 8000 öffnen. Wir werden ihn verwenden, wenn Sie den Webdienst verwenden. Port 22 für ssh ist standardmäßig geöffnet.
Hurra! Jetzt haben wir einen Remotecomputer, auf dem unsere Anwendungen ausgeführt werden können. Wir werden den Code direkt auf den Server schreiben.
Django (+ Kanäle)
Ich habe mich für Django entschieden, da Sie so schnell und einfach einen kleinen Webdienst erstellen können. Eine zusätzliche Django-Kanalbibliothek gibt uns die Möglichkeit, mit Web-Sockets zu arbeiten (nämlich zu tun) Krücke Sendung durch Übertragung von Bildern ohne Aktualisierung der Seite).
Erstellen Sie ein Verzeichnis, in dem das Projekt abgelegt werden soll. Installieren Sie django zusammen mit django-Kanälen, ohne von den Anweisungen in der Dokumentation abzuweichen.
$ mkdir Project $ cd Project $ virtualenv venv $ source venv/bin/activate $ pip install -U channels
Wir erstellen ein Projekt. Wir werden 3 Unterverzeichnisse haben. Die Hauptversion hat den gleichen Namen - mysite (automatisch erstellt), die anderen beiden - Streaming und Upload. Der erste ist für die Anzeige von Informationen auf einer Webseite verantwortlich und der zweite für das Herunterladen über die REST-API.
$ 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
Konfigurieren Sie Django-Kanäle. Kommentieren Sie die Zeile mit WSGI_APPLICATION aus und fügen Sie mit ASGI_APPLICATION eine neue hinzu. Jetzt arbeitet unsere Anwendung asynchron.
# mysite/settings.py # ... # WSGI_APPLICATION = ... ASGI_APPLICATION = 'mysite.routing.application' # ...
Wir aktualisieren auch den Wert der 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', ] # ...
Architektur
Wir werden den Code basierend auf dem offiziellen Django-Kanal- Tutorial schreiben. Die Struktur unseres kleinen Dienstes wird folgendermaßen aussehen:
MYIP: 8000 / Frame - Eine Webseite, auf der unter bestimmten Bedingungen das Ergebnis der Seite angezeigt wird, die der Bediener betrachtet
MYIP: 8000 / upload / upload_text / - Adresse für eine POST-Anfrage, die erkannten Text sendet
MYIP: 8000 / upload / upload_image / - Adresse für die PUT-Anfrage, die einzelne Bilder sendet
Sie müssen diese Logik in den urls.py-Dateien der entsprechenden Verzeichnisse registrieren.
# 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')), ]
REST-API
Wir wenden uns der Beschreibung der Logik unserer API zu.
# 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'})
Webseite
Alle Informationen passen auf eine Seite, sodass die Logik einfach ist.
# 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', {})
Wir müssen ein kleines HTML-Dokument schreiben, um die Ergebnisse anzuzeigen. Es verfügt über ein integriertes Skript, mit dem Sie eine Verbindung zu einem Web-Socket herstellen und ihn mit Inhalten füllen können.
<!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>
Konfigurieren Sie Routing und Sockets
Was ist die beste Übersetzung des Wortes Routing ins Russische? Lassen Sie uns versuchen, diese Frage aus unserem Kopf zu bekommen und sie (oder sie) einfach zu stellen.
# mysite/mysite/settings.py # ... ALLOWED_HOSTS = ['*'] # [] ['*'], # ... CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, }
Jetzt müssen Sie die Weiterleitungslogik registrieren (routing.py-Dateien ähneln urls.py-Dateien, nur jetzt für Web-Sockets).
# 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), ]
Und jetzt implementieren wir FrameConsumer selbst in consumer.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'] }))
Und schließlich starten wir mit verschwitzten Handflächen.
$ docker run -p 6379:6379 -d redis:2.8 $ python manage.py runserver 0.0.0.0:8000
Und jetzt zu Android
Wir werden das RTR SDK von ABBYY zur Texterkennung verwenden. Das Ultimate Pack RTR SDK für unsere Zwecke kann hier heruntergeladen werden . Wir implementieren eine ziemlich einfache Schnittstelle für die Verarbeitung von Frames. Unsere Anwendung basiert auf einem Beispiel aus dem Archiv, das vom vorherigen Link heruntergeladen wurde (/ sample-textcapture). Wir werden die überschüssigen Teile aus der Anwendung entfernen, sie ein wenig polieren, um speziell mit Android Things zu arbeiten, und die Kommunikation mit dem Server implementieren.
Die .aar-Datei der Bibliothek befindet sich im libs-Verzeichnis des heruntergeladenen Archivs, das wir importieren. Wir kopieren den Inhalt des Assets-Verzeichnisses des Archivs (dort im Wesentlichen die für den Erkennungsprozess selbst erforderlichen Dateien) in die Assets unseres Projekts. Dort kopieren wir die Lizenzdatei aus dem Archiv, ohne sie startet die Anwendung nicht.
Um die von uns benötigte ABBYY RTR SDK-Funktionalität zu implementieren, müssen wir ein Objekt vom Typ Engine erstellen und es bereits als Objekt vom Typ ITextCaptureService verwenden, das wir später starten werden.
try { mEngine = Engine.load(this, LICENSE_FILE_NAME); mTextCaptureService = mEngine.createTextCaptureService(textCaptureCallback); return true; }
In diesem Fall müssen Sie ein Objekt vom Typ ITextCaptureService.Callback übergeben, es direkt in unserer MainActivity-Klasse erstellen und 3 Methoden implementieren.
private ITextCaptureService.Callback textCaptureCallback = new ITextCaptureService.Callback() { @Override public void onRequestLatestFrame(byte[] buffer) {
Wir haben den Empfang des Rahmens an das Kameraobjekt delegiert. Ich werde zeigen, was drinnen passiert.
private Camera.PreviewCallback cameraPreviewCallback = new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) {
Um Nachrichten zu senden, schreiben wir einige Klassen, die ihre Arbeit wiederum an ein Objekt der Uploader-Klasse delegieren.
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; } }
Die Kommunikation mit dem Netzwerk selbst in der Uploader-Klasse wird mithilfe der praktischen OkHttp3-Bibliothek implementiert. Dadurch können Sie die Interaktion mit dem Server erheblich vereinfachen.
Ergebnis
Wir erhalten eine funktionierende Client-Server-Anwendung mit Anerkennung von ABBYY, die in das Internet der Dinge integriert ist - ist das nicht cool?
Ein gesammeltes Gerät und eine kleine native Werbung meines Arbeitgebers

Text erkannt

Selfie-Panorama mit einer Übersicht über mehrere Geräte

Vidos, wie das alles im wirklichen Leben aussehen kann
Repositories auf Github:
→ AndroidThingsTextRecognition-Backend
→ AndroidThingsTextRecognition-Android
Nehmen und verwenden!