Ollama - Extraindo dados estruturados de imagens


     Neste artigo vamos mostrar como extrair dados estruturados de imagens usando o Ollama.

Nos últimos anos, os Large Language Models (LLMs) deixaram de ser apenas ferramentas de geração de texto e passaram a atuar como mecanismos avançados de interpretação de dados não estruturados — incluindo imagens.

Mas existe uma diferença enorme entre:  Pedir para um modelo descrever uma imagem, e usar um modelo para extrair dados estruturados confiáveis dessa imagem.

É aí que a engenharia começa de verdade.



O que é o Ollama?

O Ollama é uma ferramenta que permite executar modelos de linguagem localmente, na sua própria máquina. Em vez de depender de APIs externas e enviar dados para a nuvem, você pode baixar um modelo (como llama3.2-vision) e executá-lo diretamente no seu hardware.   

Obs: O llama3.2-vision éum modelo multimodal que entende texto e imagem ao mesmo tempo baseado na familia 3.x da Meta mas com capacidade de visão adicionada.  Um modelo “vision” consegue:  Interpretar imagens, descrever o que vê , ler texto dentro de imagens (OCR implícito), extrair informações estruturadas e  relacionar texto e imagem no mesmo contexto

Isso traz vantagens importantes:
✔ Privacidade de dados
✔ Sem dependência de API keys
✔ Controle total sobre o ambiente
✔ Custos previsíveis
✔ Possibilidade de uso offline

Para desenvolvedores .NET, o Ollama pode ser integrado facilmente usando bibliotecas como OllamaSharp e Microsoft.Extensions.AI, permitindo trabalhar com uma interface padronizada (IChatClient) independente do provedor do modelo.

O que são Dados Estruturados com IA?

As imagens, textos livres e documentos escaneados são exemplos clássicos de dados não estruturados. Eles não possuem um formato diretamente utilizável por sistemas computacionais tradicionais.

Em nosso exemplo vamos usar a imagem de um recibo que contém:
   Titulo, Endereco e Data
   Nome dos produtos
   Preços unitários
   Valores sub-totais e totais 

A seguir temos a figura do recibo do arquivo receipt3.png presente pasta Receipts do projeto. Este arquivo deve ter o valor Copy to Output das propriedades alterado para Copy if Newer.

Mas essas informações estão organizadas visualmente, não semanticamente.

Quando falamos em extrair dados estruturados com IA, estamos nos referindo a transformar esse conteúdo não estruturado em um formato padronizado, como:

{
  "items": [
    {
      "name": "Apples",
      "quantity": 1.500,
      "unitPrice": 5.99,
      "totalPrice": 8.99
    }
  ],
  "subtotal": 8.99
}

Agora aqui temos:
   Campos definidos
   Tipos de dados conhecidos
   Estrutura previsível
   Validação possível


Esse é o ponto central:  A IA deixa de ser apenas geração de texto e passa a ser um mecanismo de extração semântica estruturada.

O Desafio Real

Extrair dados estruturados com LLMs envolve três desafios principais:

Precisão numérica – o modelo não pode arredondar valores.
Consistência – a mesma imagem deve gerar resultados semelhantes.
Validação – precisamos confirmar matematicamente que os dados fazem sentido.

É exatamente isso que vamos resolver neste artigo: Transformar um modelo multimodal rodando localmente em uma ferramenta confiável de extração de dados estruturados.

Extraindo dados estruturados de recibos

Para ilustrar vamos criar um projeto Console no Visual Studio e usar o Ollama e o LLM llama3.2-vision:latest para :

- Extrair itens de um recibo a partir de imagem
- Gerar JSON estruturado validável
- Desserializar para objetos C# fortemente tipados
- Validar matematicamente os dados
- Garantir consistência

A arquitetura da solução usada será a seguinte:

Vamos a seguir exibir o código de cada um dos recursos usados no projeto.

1- Receipt

public class Receipt
{
        public string StoreName { get; set; } = string.Empty;
        public string Address { get; set; } = string.Empty;
        public DateTime Date { get; set; }
        public string Manager { get; set; } = string.Empty;

        public List Items { get; set; } = new();

        public decimal Subtotal { get; set; }
        public decimal Tax { get; set; }
        public decimal Total { get; set; }

Essa classe representa o modelo de domínio do recibo, ou seja, a estrutura que define como os dados extraídos pela IA devem ser organizados na aplicação.

Ela atua como contrato tipado para o GetResponseAsync<Receipt>, permitindo que o JSON retornado pelo modelo seja convertido automaticamente em objeto C#.

Cada propriedade mapeia um campo esperado no JSON estruturado (loja, itens, subtotal, imposto, total), garantindo tipagem forte e validação posterior.  Ela é usada para padronizar os dados, facilitar regras de negócio e permitir serialização consistente dentro do sistema.

2- LineItem

public class LineItem
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

Essa classe representa um item individual do recibo, definindo a estrutura mínima de cada produto extraído pela IA. Ela atua como modelo tipado para mapear cada objeto do array items retornado pelo JSON do modelo multimodal.

As propriedades garantem tipagem forte (nome como texto e preço como decimal), permitindo validações matemáticas confiáveis. Ela é utilizada dentro da classe Receipt para compor a lista de itens e viabilizar cálculos como subtotal e verificação de consistência.

3-PromptBuilder

public static class PromptBuilder
{
    public static ChatMessage BuildSystemPrompt()
    {
        return new ChatMessage(ChatRole.System,
        """
        Extract all structured information from this receipt.
        Respond ONLY in valid JSON with this structure:
        {
          "storeName": "",
          "address": "",
          "date": "",
          "manager": "",
          "items": [
            {
              "name": "item name",
              "price": 0.00
            }
          ],
          "subtotal": 0.00,
          "tax": 0.00,
          "total": 0.00
        }

        Rules:
        - Do NOT invent quantities.
        - Do NOT create unitPrice.
        - Extract exactly what appears on the receipt.
        - Read every digit exactly as printed.
        - Do NOT round values.
        - Use period as decimal separator.
        - Ensure subtotal equals sum of item prices.
        - Ensure subtotal + tax equals total.
        - Output only valid JSON.

        """);
    }

Essa classe é responsável por construir o System Prompt, que define o comportamento e as regras que o modelo deve seguir ao interpretar a imagem.

Ela atua configurando o contexto da conversa com ChatRole.System, garantindo prioridade às instruções e reduzindo ambiguidades na saída. O JSON descrito no prompt funciona como um contrato textual, orientando o modelo a retornar dados estruturados no formato esperado.

Ela é usada para controlar a precisão, evitar alucinações e aumentar a consistência da extração de dados.

Obs: O valor Temperature = 0, reduz a aleatoriedade da geração de texto, fazendo o modelo escolher sempre a resposta mais provável estatisticamente.  Usamos esse valor para tornar a saída mais determinística e consistente, especialmente em cenários de extração de dados estruturados.

4- ReceiptExtractionService

public class ReceiptExtractionService
{
    private readonly IChatClient _chatClient;

    public ReceiptExtractionService(IChatClient chatClient)
    {
        _chatClient = chatClient;
    }

    public async Task ExtractAsync(string imagePath)
    {
        if (!File.Exists(imagePath))
            return null;

        var systemPrompt = PromptBuilder.BuildSystemPrompt();

        var userMessage = new ChatMessage(
            ChatRole.User,
            "Extract structured receipt data.");

        userMessage.Contents.Add(
            new DataContent(
                await File.ReadAllBytesAsync(imagePath),
                "image/png"));

        var response = await _chatClient.GetResponseAsync(
            new[] { systemPrompt, userMessage },
            new ChatOptions
            {
                Temperature = 0
            });

        return response.Result;
    }

Essa classe é o serviço de aplicação responsável por orquestrar a comunicação com o modelo de IA para extrair dados do recibo. Ela atua enviando a imagem e o prompt ao IChatClient, que encapsula o acesso ao modelo multimodal configurado no Ollama.

O método ExtractAsync lê o arquivo, monta as mensagens (System + User), envia para o modelo e recebe o resultado já desserializado como objeto Receipt. Ela é usada para centralizar a lógica de extração estruturada, mantendo o restante da aplicação desacoplado do provedor de IA.

5- ReceiptValidator

public static class ReceiptValidator
{
    public static bool Validate(Receipt receipt, out List errors)
    {
        errors = new List();

        if (receipt == null)
        {
            errors.Add("Receipt is null.");
            return false;
        }

        // Campos obrigatórios
        if (string.IsNullOrWhiteSpace(receipt.StoreName))
            errors.Add("Store name is missing.");

        if (string.IsNullOrWhiteSpace(receipt.Address))
            errors.Add("Address is missing.");

        if (string.IsNullOrWhiteSpace(receipt.Manager))
            errors.Add("Manager name is missing.");

        if (receipt.Date == default)
            errors.Add("Invalid or missing date.");

        // Itens
        if (receipt.Items == null || receipt.Items.Count == 0)
        {
            errors.Add("No line items found.");
        }
        else
        {
            foreach (var item in receipt.Items)
            {
                if (string.IsNullOrWhiteSpace(item.Name))
                    errors.Add("Item with missing name detected.");

                if (item.Price < 0)
                    errors.Add($"Negative price detected for item: {item.Name}");
            }
        }

        // Soma dos itens = Subtotal
        var calculatedSubtotal = receipt.Items?.Sum(i => i.Price) ?? 0;

        if (Math.Abs(calculatedSubtotal - receipt.Subtotal) > 0.01m)
        {
            errors.Add(
                $"Subtotal mismatch. " +
                $"Calculated: {calculatedSubtotal:F2}, " +
                $"Reported: {receipt.Subtotal:F2}");
        }

        // Subtotal + Tax = Total
        var calculatedTotal = receipt.Subtotal + receipt.Tax;

        if (Math.Abs(calculatedTotal - receipt.Total) > 0.01m)
        {
            errors.Add(
                $"Total mismatch. " +
                $"Expected: {calculatedTotal:F2}, " +
                $"Reported: {receipt.Total:F2}");
        }

        // 5 Valores negativos gerais
        if (receipt.Subtotal < 0)
            errors.Add("Subtotal cannot be negative.");

        if (receipt.Tax < 0)
            errors.Add("Tax cannot be negative.");

        if (receipt.Total < 0)
            errors.Add("Total cannot be negative.");

        return errors.Count == 0;
    }
}

Aqui temos a camada de validação determinística do sistema, garantindo que os dados extraídos pela IA sejam consistentes e confiáveis. 

Ela atua verificando campos obrigatórios, integridade dos itens e impedindo valores inválidos ou negativos. Também realiza validações matemáticas, assegurando que a soma dos itens corresponda ao subtotal e que subtotal mais imposto resulte no total.

A tolerância decimal evita falsos negativos causados por pequenas diferenças de arredondamento.
Ela é usada para proteger a aplicação contra erros probabilísticos do modelo, tornando a IA apenas um auxiliar e não a fonte final de verdade.

6- Classe Program

using IAStructuredDataFromImage.Application;
using IAStructuredDataFromImage.Infrastructure;
using IAStructuredDataFromImage.Shared;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Text.Json;

CultureNormalization.ApplyInvariantCulture();

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddOllamaVision(
    "http://localhost:11434",
    "llama3.2-vision:latest");

builder.Services.AddSingleton();

var app = builder.Build();

Console.WriteLine("Recebendo e analisando imagem...");
Console.WriteLine("Aguarde, isso pode levar alguns segundos...\n");

var extractor = app.Services.GetRequiredService();

var receipt = await extractor.ExtractAsync("receipts/receipt3.png");

if (receipt == null)
{
    Console.WriteLine("Falha ao extrair dados.");
    return;
}

Console.WriteLine($"Itens extraídos: {receipt.Items.Count}");

Console.WriteLine("\n=== DADOS EXTRAÍDOS ===\n");

Console.WriteLine($"Store: {receipt.StoreName}");
Console.WriteLine($"Address: {receipt.Address}");
Console.WriteLine($"Date: {receipt.Date}");
Console.WriteLine($"Manager: {receipt.Manager}");
Console.WriteLine($"Subtotal: {receipt.Subtotal}");
Console.WriteLine($"Tax: {receipt.Tax}");
Console.WriteLine($"Total: {receipt.Total}");

if (ReceiptValidator.Validate(receipt, out var errors))
{
    Console.WriteLine("\nRecibo válido.");
}
else
{
    Console.WriteLine("Erros encontrados:");
    foreach (var error in errors)
        Console.WriteLine($"- {error}");
}

Console.WriteLine("\n=== JSON GERADO ===\n");

var json = JsonSerializer.Serialize(
    receipt,
    new JsonSerializerOptions
    {
        WriteIndented = true
    });

Console.WriteLine(json);

Console.ReadLine();

Esse código é o ponto de entrada da aplicação e atua como orquestrador do fluxo completo de extração com IA.  Ele configura a cultura invariável, registra o modelo Ollama e o serviço de extração no container de injeção de dependência.

Em seguida, resolve o ReceiptExtractionService, envia a imagem para o modelo multimodal e recebe os dados estruturados já tipados.  Após a extração, executa a validação determinística para garantir consistência matemática e integridade dos campos.

Por fim, exibe os dados no console e serializa o objeto final em JSON formatado para inspeção e depuração.

7- OllamaConfiguration

using Microsoft.Extensions.DependencyInjection;
using OllamaSharp;

namespace IAStructuredDataFromImage.Infrastructure;

public static class OllamaConfiguration
{
    public static IServiceCollection AddOllamaVision(
        this IServiceCollection services,
        string endpoint,
        string model)
    {
        var httpClient = new HttpClient
        {
            BaseAddress = new Uri(endpoint),
            Timeout = TimeSpan.FromMinutes(5)
        };

        services.AddChatClient(
            new OllamaApiClient(httpClient, model));

        return services;
    }
}

Essa classe configura a integração com o modelo Ollama dentro do container de injeção de dependência da aplicação.  Ela cria um HttpClient apontando para o endpoint local do Ollama e define um tempo de espera maior para evitar timeout em modelos pesados.

O método AddOllamaVision registra o IChatClient configurado com o modelo especificado, tornando-o disponível para injeção nos serviços.

Ela é usada para isolar a infraestrutura externa, permitindo trocar o provedor de IA sem alterar a lógica da aplicação.

8- CultureNormalization

using System.Globalization;

namespace IAStructuredDataFromImage.Shared;

public static class CultureNormalization
{
    public static void ApplyInvariantCulture()
    {
        CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
        CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
    }
}

Este código é responsável por padronizar a cultura da aplicação para InvariantCulture, evitando variações regionais no processamento de dados.  Ela atua garantindo que números decimais utilizem sempre ponto como separador, independentemente da configuração regional da máquina.
Isso é essencial para evitar erros na leitura e validação de valores monetários retornados pelo modelo.

Ela é usada para assegurar consistência na serialização, desserialização e cálculos numéricos dentro do sistema.

Agora é só alegria...

Executando o projeto iremos obter:

Pegue o projeto completo em :   https://github.com/macoratti/IAStructuredDataFromImage

E estamos conversados...  

"Assim que, se alguém está em Cristo, nova criatura é; as coisas velhas já passaram; eis que tudo se fez novo."
2 Coríntios 5:17

Referências:


José Carlos Macoratti