Oi Meu nome é Azat Kalmykov, sou aluno do segundo ano da Faculdade de Ciências da Computação e Matemática Aplicada da Escola Superior de Economia e estagiário no Departamento de Desenvolvimento Móvel ABBYY. Neste artigo, falarei sobre meu pequeno projeto, realizado como parte de um estágio de verão.

Imagine um pequeno transportador. Bens ou alguns detalhes acompanham-no, nos quais é importante reconhecer o texto (talvez esse seja um identificador exclusivo ou talvez algo mais interessante). Encomendas são um bom exemplo. A operação do transportador é controlada remotamente por um operador que monitora mau funcionamento e, nesse caso, resolve problemas. O que pode ajudá-lo com isso? Um dispositivo baseado na plataforma Android Things pode ser uma boa solução: é móvel, fácil de configurar e pode funcionar via Wi-Fi. Decidimos tentar usar as tecnologias ABBYY e descobrir como elas são adequadas para essas situações - reconhecimento de texto em um fluxo em "dispositivos não padrão" da categoria Internet das Coisas. Vamos simplificar deliberadamente muitas coisas, pois estamos apenas construindo um conceito. Se você estiver interessado, bem-vindo ao gato.
Android Things
Uma coisa maravilhosa chamada Android Things Starter Kit chegou ao escritório da ABBYY na conferência de E / S do Google . A bondade não desapareceu, e queríamos brincar com ela em busca de vários cenários para usar nossas bibliotecas de reconhecimento . Primeiro você precisa montar nosso dispositivo e depois executar. Não é difícil fazer isso, siga rigorosamente as instruções do fabricante.
Leia mais sobre a plataforma aqui e aqui .
O que veio em minhas mãos

E no final do post, mostrarei como é o dispositivo montado
O que estamos fazendo?
Escreveremos um aplicativo para a plataforma Android Things que processará a imagem da câmera, enviando texto reconhecido e (periodicamente) quadros para o nosso servidor, para que o operador condicional possa entender o que está acontecendo no pipeline. O servidor será escrito em django.
Apresso-me a notar que, para a implementação deste projeto, você não precisará de investimentos, nem de registro e SMS (ok, na AWS, você ainda precisa se registrar e obter uma conta gratuita).
Lançamos foguete para o espaço servidor
Vamos supor que você já tenha uma conta gratuita da AWS. Amarraremos nosso cartão para que a Amazônia maligna, no caso de nossa imprudência, deduza alguns siclos de nós. Usaremos o AWS EC2 e criaremos uma máquina virtual no Ubuntu Server 18.04 LTS (HVM) com o tipo de volume SSD. Para este sistema operacional e ao usar uma conta gratuita, apenas uma configuração está disponível, selecione-a (não se preocupe, um gigabyte de RAM é suficiente para nós). Criaremos uma chave ssh (ou usaremos uma já pronta) e tentaremos conectar à nossa máquina. Também criaremos o Elastic IP (algo como um IP estático) e o vincularemos imediatamente à nossa máquina. Observe que o IP elástico, que não está vinculado a nenhuma máquina virtual, custará dinheiro durante o desenvolvimento.
Estamos conectados ao servidor. Instalamos o kit de ferramentas necessário na máquina.
A terceira versão do Python está pré-instalada. O assunto é deixado para o pequeno.
$ sudo apt-get update $ sudo apt-get install python3-pip $ sudo pip3 install virtualenv
Instale a janela de encaixe, precisamos dela mais tarde.
$ sudo apt-get install docker.io
Você também precisa abrir a porta 8000. Nós a usaremos ao usar o serviço da web. A porta 22 para ssh está aberta por padrão.
Viva! Agora temos um computador remoto para executar nossos aplicativos. Escreveremos o código diretamente no servidor.
Django (+ canais)
Decidi usar o django, pois isso permitirá criar rápida e facilmente um pequeno serviço da web. Uma biblioteca de canais django adicionais nos dará a oportunidade de trabalhar com soquetes da web (a saber, fazer muleta transmitido através da transferência de fotos sem atualizar a página).
Crie um diretório no qual colocar o projeto. Instale o django junto com os canais do django, sem se desviar das instruções na documentação .
$ mkdir Project $ cd Project $ virtualenv venv $ source venv/bin/activate $ pip install -U channels
Criamos um projeto. Teremos 3 subdiretórios. O principal terá o mesmo nome - meu site (criado automaticamente), os outros dois - streaming e upload. O primeiro será responsável por exibir informações em uma página da Web e o segundo - por fazer o download por meio da 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
Configure canais django. Comente a linha com WSGI_APPLICATION e inclua uma nova com ASGI_APPLICATION. Agora, nosso aplicativo funcionará de forma assíncrona.
# mysite/settings.py # ... # WSGI_APPLICATION = ... ASGI_APPLICATION = 'mysite.routing.application' # ...
Também atualizamos o valor da 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', ] # ...
Arquitetura
Escreveremos o código com base no tutorial oficial dos canais django. A estrutura do nosso pequeno serviço ficará assim:
MYIP: 8000 / frame - uma página da web que mostra o resultado, condicionalmente, a página que o operador está visualizando
MYIP: 8000 / upload / upload_text / - endereço para uma solicitação POST, enviando texto reconhecido
MYIP: 8000 / upload / upload_image / - endereço da solicitação PUT, enviando imagens individuais
É necessário registrar essa lógica nos arquivos urls.py dos diretórios correspondentes.
# 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
Passamos à descrição da lógica da nossa 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 da Web
Todas as informações caberão em uma página, portanto, a lógica será simples.
# 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', {})
Precisamos escrever um pequeno documento html para exibir os resultados. Ele terá um script interno para conectar-se a um soquete da Web e preenchê-lo com conteúdo.
<!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 roteamento, soquetes
Qual é a melhor tradução da palavra roteamento para o russo? Vamos tentar tirar essa questão da nossa cabeça e configurá-la (ela).
# mysite/mysite/settings.py # ... ALLOWED_HOSTS = ['*'] # [] ['*'], # ... CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, }
Agora você precisa registrar a lógica de "encaminhamento" (os arquivos routing.py são semelhantes aos arquivos urls.py, somente agora para os soquetes da 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), ]
E agora implementamos o próprio FrameConsumer em customers.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'] }))
E finalmente, com as palmas das mãos suadas, lançamos.
$ docker run -p 6379:6379 -d redis:2.8 $ python manage.py runserver 0.0.0.0:8000
E agora sobre o Android
Usaremos o RTR SDK da ABBYY para reconhecimento de texto. O pacote Ultimate RTR SDK para nossos propósitos pode ser baixado aqui . Implementamos uma interface bastante simples para processar quadros, nosso aplicativo será baseado em uma amostra do arquivo baixado do link anterior (/ sample-textcapture). Vamos jogar fora as partes em excesso do aplicativo, aperfeiçoá-lo um pouco para trabalhar especificamente com o Android Things e implementar a comunicação com o servidor.
O arquivo .aar da biblioteca está no diretório libs do arquivo baixado que importamos. Copiamos o conteúdo do diretório de ativos do arquivo morto (basicamente os arquivos necessários para o processo de reconhecimento) nos ativos do nosso projeto. Lá, copiamos o arquivo de licença do arquivo morto, sem ele o aplicativo não será iniciado.
Para implementar a funcionalidade do ABBYY RTR SDK de que precisamos, precisamos criar um objeto do tipo Engine e usá-lo já como um objeto do tipo ITextCaptureService, que lançaremos mais tarde.
try { mEngine = Engine.load(this, LICENSE_FILE_NAME); mTextCaptureService = mEngine.createTextCaptureService(textCaptureCallback); return true; }
Nesse caso, você precisa passar um objeto do tipo ITextCaptureService.Callback, criá-lo diretamente em nossa classe MainActivity, e deve implementar 3 métodos.
private ITextCaptureService.Callback textCaptureCallback = new ITextCaptureService.Callback() { @Override public void onRequestLatestFrame(byte[] buffer) {
Delegamos o recebimento do quadro ao objeto da câmera. Vou mostrar o que está acontecendo lá dentro.
private Camera.PreviewCallback cameraPreviewCallback = new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) {
Para enviar mensagens, escrevemos algumas classes, que por sua vez delegarão seu trabalho a um objeto da 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; } }
A comunicação com a própria rede na classe Uploader é implementada usando a conveniente biblioteca OkHttp3. Permite simplificar bastante a interação com o servidor.
Resultado
Temos um aplicativo cliente-servidor em funcionamento com reconhecimento da ABBYY, incorporado à Internet das Coisas - não é legal?
Um dispositivo coletado e um pequeno anúncio nativo do meu empregador

Texto reconhecido

Panorama selfie com uma visão geral de vários dispositivos

Vidos, como tudo pode parecer na vida real
Repositórios no github:
→ AndroidThingsTextRecognition-Backend
→ AndroidThingsTextRecognition-Android
Pegue e use!