← Todos os textos

OCR na edge

Nesta página

À medida que a tecnologia avançada se torna comum, a visão por computador passou a ser uma ferramenta que parece essencial, com muitas aplicações.

Serve para identificar objetos, padrões, pessoas, plantas e animais de estimação, dá informação útil a quem a usa, apoia decisões e facilita a triagem de conjuntos de dados enormes.

Junta a isto a procura crescente de análise em tempo real e de respostas com baixa latência, e a edge compute tornou-se o melhor sítio para pores o teu modelo treinado a funcionar.

O que é o OCR?

O reconhecimento ótico de caracteres (OCR, de Optical Character Recognition) é o processo de converter imagens de texto em texto codificado para máquinas. Costuma usar-se para transformar imagens de documentos em texto que se pode pesquisar e indexar.

Podes usá-lo para digitalizar documentos, recibos, cartões de visita e mais. Percebe-se logo porque é útil na Blinq.

O que é a edge compute?

A edge compute é a prática de correr o teu código o mais perto possível do utilizador. Normalmente faz-se correndo o código numa CDN, como os Cloudflare Workers ou o AWS Lambda@Edge.

É diferente da cloud tradicional, em que o código corre num centro de dados que costuma estar longe do utilizador.

Porquê a edge compute?

Embora seja possível correr o modelo no dispositivo, deixá-lo na edge tem algumas vantagens:

  • Baixa latência: O modelo está mais perto do utilizador, por isso a resposta é mais rápida. Parece que está no dispositivo.
  • Baixo custo: Não pagas os recursos de computação para manter o modelo a correr, só pagas os pedidos.
  • Fácil de atualizar: Podes atualizar o modelo sem atualizar a app. Ajuda sobretudo se tiveres muitos utilizadores em versões antigas da app.

Na Blinq, mantemos uma cauda longa de versões da app para telemóvel, por isso poder atualizar o modelo sem atualizar a app é uma grande vantagem para nós.

Como correr o teu modelo na edge

Os runtimes de edge, como os Cloudflare Workers e o Lambda@Edge, são muito diferentes dos runtimes tradicionais, como o Node.js e o Golang. Foram feitos para serem rápidos e leves, por isso não têm acesso às mesmas bibliotecas e APIs.

Parecem-se muito com correr o código num browser, por isso não adianta copiar e colar o código à espera de que funcione.

O modelo

Neste exemplo uso um modelo de OCR simples, o Keras OCR. É um modelo TensorFlow que recebe uma imagem e devolve o texto que lá encontra.

Também uso a funcionalidade Constellation da Cloudflare (em beta), que permite correr modelos ONNX na edge. É uma ótima novidade, porque deixa correr modelos que o TensorFlow.js não suporta e que não ficam limitados pela quota de 10MB dos Workers da Cloudflare.

O Lambda@Edge tem limites muito mais altos, mas continua a ser boa ideia manter o modelo o mais pequeno possível para reduzir o tempo de arranque a frio.

Neste exemplo usei o modelo crnn_kurapan, disponível aqui.

Converter Keras em ONNX

O primeiro passo é converter o modelo Keras em ONNX. É um processo bastante simples, mas exige alguns passos. Parto do princípio de que tens o Conda instalado e usas Python 3.10.

Se estiveres num Mac com Apple Silicon, precisas também de:

tensorflow-macos

Primeiro, cria um ambiente conda.

$ conda create -n edge-onnx python=3.10 pip
$ conda activate edge-onnx

A seguir, instala as dependências.

$ python -m tensorflow tf2onnx keras-ocr

Se estiveres num Mac com Apple Silicon, tens de instalar o tensorflow-macos em vez do tensorflow.

$ python -m pip install tensorflow-macos

Por fim, converte o modelo com este excerto.

import tf2onnx
import onnx
import keras_ocr

recognition = keras_ocr.recognition.Recognizer(
    alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', weights='kurapan')

output_path = 'crnn_kurapan.onnx'
model_proto, _ = tf2onnx.convert.from_keras(
    recognition.model, opset=None, output_path=output_path)

onnx.save(model_proto, output_path)

Isto cria um modelo ONNX na pasta atual, em crnn_kurapan.onnx.

Correr o modelo na edge

Para correr o modelo ONNX na edge compute, usei o Cloudflare Constellation, porque o modelo é demasiado grande para os Cloudflare Workers.

Podes fazê-lo pelo painel da Cloudflare ou pela CLI. Eu uso a CLI, porque é mais fácil de automatizar.

Primeiro, temos de criar um projeto.

$ npx wrangler constellation project create "ocr" ONNX
$ npx wrangler constellation project list

Toma nota do ID do projeto.

A seguir, carrega o modelo:

$ wrangler constellation model upload ocr crnn_kurapan crnn_kurapan.onnx
$ npx wrangler constellation model list ocr

Toma nota do ID do modelo.

Depois, prepara o projeto e inicializa o wrangler.

$ npm create cloudflare@2

Chamei ocr ao projeto e usei o template “Hello World”.

No ficheiro wrangler.toml, acrescenta a ligação ao constellation e ativa o node_compat.

name = "ocr"
main = "src/index.js"
node_compat = true
workers_dev = true
compatibility_date = "2023-07-01"

constellation = [
    {binding = 'OCR', project_id = '{{ project ID from earlier }}'},
]

Por fim, instala o cliente do constellation e as dependências para enviar imagens em stream para o modelo.

npm install --save @cloudflare/constellation string-to-stream pngjs

Agora podemos escrever o código que corre o modelo.

import str from 'string-to-stream';
import { PNG } from 'pngjs/browser';

import { Tensor, run } from '@cloudflare/constellation';

const MODEL_ID = '{{ model ID from earlier }}';

function normalizeImage(data) {
  return new Promise(async (resolve, reject) => {
    const stream = str(data);

    const png = new PNG({ filterType: 4 });

    stream
      .pipe(png)
      .on('parsed', function () {
        const [r, g, b] = new Array(3).fill([]);

        for (let i = 0; i < this.data.length; i += 4) {
          r.push(this.data[i] / 255.0);
          g.push(this.data[i + 1] / 255.0);
          b.push(this.data[i + 2] / 255.0);
        }

        resolve({
          input: [...r, ...g, ...b],
          shape: [3, this.height, this.width, 3],
        });
      })
      .on('error', function (error) {
        reject({ err: error.toString() });
      });
  });
}

export default {
  async fetch(request, env) {
    if (request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }

    const formData = await request.formData();
    const image = formData.get('image');

    if (!image) {
      return new Response('No image found', { status: 400 });
    }

    const buffer = await image.arrayBuffer();

    try {
      const normalized = await normalizeImage(buffer);

      if (!normalized) {
        return new Response('Unable to normalize image', { status: 500 });
      }

      const input = new Tensor(
        'float32',
        normalized.shape,
        normalized.input,
        'input_1'
      );

      const output = await run(env.OCR, MODEL_ID, input);

      return new Response(JSON.stringify(output), { status: 200 });
    } catch (err) {
      return new Response(err, { status: 500 });
    }
  },
};

Enquanto estiver em beta, talvez tenhas de desativar os avisos do constellation com export NO_CONSTELLATION_WARNING=true.

Quando estiveres pronto para fazer deploy, basta correr wrangler deploy.

Testar o modelo

Agora podes fazer um pedido HTTP ao Worker para testar o modelo.

$ curl -X POST -F "
image=@/path/to/image.png" https://ocr.example.workers.dev

Isto devolve uma resposta JSON com o resultado do modelo.

{
  "output_1": [...]
}

O resultado do modelo é um array de probabilidades para cada caractere. Para o converter em texto, podes usar algo parecido com este excerto.

const charset =
  'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

const text = output
  .map((probabilities) => {
    const max = Math.max(...probabilities);
    const index = probabilities.indexOf(max);

    return charset[index];
  })
  .join('');

Porquê fazer isto?

Queria ver se era possível correr um modelo grande-ish na edge. Queria também ver que desempenho teria.

Embora seja pouco provável que este modelo vá para produção, é um bom exemplo do que se consegue com os Cloudflare Workers e o Constellation. Os modelos de OCR maiores costumam ter melhor desempenho, mas dá para usar modelos mais pequenos se aceitares perder precisão. Ainda assim, podes sempre recorrer ao Tesseract.js para isso.

Desempenho

O modelo demora cerca de 1,5 segundos a correr na edge. É muito mais lento do que correr localmente, mas ainda é rápido que chegue para produção. Mesmo assim, num cenário real também podes recorrer às ofertas da Google e da Amazon Web Services, que são muito mais precisas. Em contrapartida, são também muito mais caras.

Precisão

O modelo é bastante preciso, mas não é perfeito. Tem dificuldades com algumas fontes e não é bom a reconhecer texto em imagens com muito ruído. Só lida com imagens pequenas, mas isso melhora com uma janela deslizante.

Experimenta tu

A Cloudflare está a desenvolver ativamente o Constellation, que de momento está em beta. Se o quiseres experimentar, podes inscrever-te no beta aqui.

Há também um canal de Discord onde podes fazer perguntas e pedir ajuda à equipa da Cloudflare.

Discussão no Hacker News