Beta · Estamos validando o produto. Pode ter instabilidades. Saiba mais

Voltar para o blog
·9 min·Israel Oriente

OCR de placa em PHP e Laravel: integrar leitura de placa em 2026

Tutorial completo para reconhecer placa de carro em PHP puro e Laravel. Comparativo entre Tesseract OCR, gerador próprio e API. Código pronto e custo real em produção.

phplaravelocralprtutorial

PHP ainda é a linguagem dominante em backend BR de pequenas e médias empresas — sistema de estacionamento, controle de acesso de condomínio, ERP de oficina, software de despachante. E quase todos esses casos, em algum momento, precisam ler placa de carro automaticamente.

O problema é que a maioria dos tutoriais de OCR em PHP que circula na internet tem 8 anos, ensina a chamar Tesseract via shell_exec, e ignora completamente o padrão Mercosul brasileiro. Esse post atualiza o caminho.

TL;DR

  • Tesseract via shell_exec: funciona, ~65% de precisão em placas brasileiras, infra de servidor complica deploy.
  • Pacote PHP de wrapper (thiagoalessio/tesseract_ocr): mesma precisão, código mais limpo, ainda precisa Tesseract instalado no servidor.
  • API HTTP via Guzzle ou cURL: 95%+ de precisão, deploy trivial (zero dependência nativa), suporta Mercosul. Esse é o caminho que eu recomendo em 2026.

O cenário típico em PHP

Você tem um Laravel ou um WordPress headless, recebe upload de foto via formulário (<input type="file">), e precisa retornar a placa pra preencher um cadastro, validar entrada de cliente, ou registrar uma passagem.

// O que você quer fazer:
public function processarFoto(Request $request)
{
    $foto = $request->file('foto');
    $placa = LerPlaca::a($foto); // Quem é esse LerPlaca?
    return response()->json(['placa' => $placa]);
}

A pergunta é: como implementar LerPlaca::a()?

Abordagem 1: Tesseract via shell_exec (PHP puro)

O Tesseract é o OCR open-source mais conhecido. Roda como binário. Em PHP puro:

function lerPlacaTesseract(string $imagemPath): ?string
{
    // Pré-processamento com ImageMagick (binário, via convert)
    $tmpProcessed = sys_get_temp_dir() . '/' . uniqid() . '.png';
    shell_exec("convert {$imagemPath} -colorspace Gray -normalize -threshold 50% {$tmpProcessed}");

    // OCR com whitelist de caracteres
    $output = shell_exec(
        "tesseract {$tmpProcessed} - -l eng -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 2>/dev/null"
    );
    @unlink($tmpProcessed);

    if (!$output) return null;

    // Extrai 7 caracteres plausíveis (padrão Mercosul ou antigo)
    $clean = preg_replace('/\s+/', '', strtoupper($output));
    if (preg_match('/[A-Z]{3}\d[A-Z0-9]\d{2}/', $clean, $m)) {
        return $m[0];
    }
    return null;
}

Problema 1: shell_exec é vetor clássico de injection. Você precisa escapar todo path de usuário ou usar escapeshellarg().

Problema 2: seu servidor precisa ter tesseract, imagemagick, e os language data packs instalados. No shared hosting, geralmente impossível. No VPS, é gerenciar.

Problema 3: ~65% de precisão em placas brasileiras reais. Tesseract foi treinado em texto impresso de livro, não em placas refletivas com ângulo.

Abordagem 2: pacote thiagoalessio/tesseract_ocr

Mesmo Tesseract, sintaxe mais limpa:

composer require thiagoalessio/tesseract_ocr
use thiagoalessio\TesseractOCR\TesseractOCR;

function lerPlacaWrapper(string $imagemPath): ?string
{
    $text = (new TesseractOCR($imagemPath))
        ->lang('eng')
        ->allowlist(range('A', 'Z'), range(0, 9))
        ->run();

    $clean = preg_replace('/\s+/', '', strtoupper($text));
    return preg_match('/[A-Z]{3}\d[A-Z0-9]\d{2}/', $clean, $m) ? $m[0] : null;
}

Mais elegante, mesma precisão, mesmas dependências de servidor.

Abordagem 3: API gerenciada via Guzzle

A solução que eu recomendo pra 95% dos casos: chama uma API HTTP, recebe JSON, pronto. Zero dependência nativa, zero tesseract no servidor.

Em PHP puro com cURL

function lerPlacaAPI(string $imagemPath): array
{
    $apiKey = getenv('LDP_API_KEY');
    $base64 = base64_encode(file_get_contents($imagemPath));

    $ch = curl_init('https://leituradeplaca.com.br/api/v1/read-plate');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer {$apiKey}",
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode(['image_base64' => $base64]),
        CURLOPT_TIMEOUT => 30,
    ]);

    $response = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code !== 200) {
        throw new \RuntimeException("API error {$code}: {$response}");
    }
    return json_decode($response, true);
}

// Uso
$result = lerPlacaAPI('/tmp/upload.jpg');
echo $result['plate']; // → "INO1102"

No Laravel, com Guzzle e config

config/services.php:

'leituradeplaca' => [
    'key' => env('LDP_API_KEY'),
    'url' => env('LDP_API_URL', 'https://leituradeplaca.com.br/api/v1'),
],

app/Services/LeitorPlacaService.php:

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Http\UploadedFile;

class LeitorPlacaService
{
    public function lerDeUpload(UploadedFile $file): array
    {
        return Http::withToken(config('services.leituradeplaca.key'))
            ->timeout(30)
            ->retry(3, 200, throw: false)
            ->attach('image', file_get_contents($file->path()), $file->getClientOriginalName())
            ->post(config('services.leituradeplaca.url') . '/read-plate')
            ->throw()
            ->json();
    }
}

Controller:

namespace App\Http\Controllers;

use App\Services\LeitorPlacaService;
use Illuminate\Http\Request;

class PlacaController extends Controller
{
    public function __construct(private LeitorPlacaService $leitor) {}

    public function processar(Request $request)
    {
        $request->validate([
            'foto' => ['required', 'image', 'max:8192'],
        ]);

        $resultado = $this->leitor->lerDeUpload($request->file('foto'));

        return response()->json([
            'placa' => $resultado['plate'],
            'confianca' => $resultado['confidence'],
        ]);
    }
}

Filas (recomendado em produção)

Como cada leitura demora 1-4 segundos, você não quer bloquear a request HTTP. Use uma Job Laravel:

namespace App\Jobs;

use App\Services\LeitorPlacaService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;

class LerPlacaJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function __construct(public int $passagemId, public string $caminho) {}

    public function handle(LeitorPlacaService $leitor): void
    {
        $resultado = $leitor->lerDoCaminho($this->caminho);
        \App\Models\Passagem::where('id', $this->passagemId)
            ->update([
                'placa' => $resultado['plate'],
                'confianca' => $resultado['confidence'],
                'lida_em' => now(),
            ]);
    }
}

E no controller você responde imediatamente com 202 Accepted enquanto a fila processa.

Comparativo

CritérioTesseract shell_execWrapper PHPAPI gerenciada
Precisão (placa BR)~65%~65%95%+
Mercosul out-of-the-boxNãoNãoSim
Dependências de servidortesseract + imagemagicktesseractnenhuma
Roda em shared hostingNãoNãoSim
Latência1-3s1-3s1-4s
Custo R$/1000 leituras0 (CPU)0 (CPU)R$ 6 a R$ 50
Tempo de integração1 dia1-2h30 min

Qual escolher

  • Hobby / hackathon: Tesseract via wrapper, suficiente pra rodar local.
  • Produção real (estacionamento, condomínio, oficina): API gerenciada. O custo unitário é desprezível pro volume típico desses negócios e o ganho de precisão paga sozinho.
  • Volume gigante (>500k/mês) com infra própria: considere modelo treinado próprio, mas você provavelmente vai sair do PHP pra Python ou Node nesse caso.

Próximos passos

Crie sua conta em leituradeplaca.com.br/signup e teste a API com 100 leituras por R$ 4,99. Suporte e exemplos completos de PHP/Laravel em /docs.

Pronto para integrar?

Crie uma conta e ganhe acesso à API em menos de 60 segundos.