PDI+VC · Exercício de Programação

EP02_11 — 🏆 Correção de Perspectiva (Homografia) em Imagem Real

2.12.11 EP02_11 🏆 Correção de Perspectiva (Homografia) em Imagem Real

Nesta atividade, o objetivo é aplicar a transformação de perspectiva (homografia) para “retificar” um objeto inclinado em uma fotografia real. Você lidará com a imagem de um jornal, onde a grade de um jogo de Sudoku está distorcida devido ao ângulo em que a foto foi capturada.

Seu programa deve ler os parâmetros de entrada do terminal, carregar a imagem, calcular a matriz de homografia \(3 \times 3\), aplicar a transformação geométrica e exibir um indicador global de validação.

  • Leia dois inteiros L e C, representando as dimensões de linhas e colunas (altura e largura) que a imagem retificada de saída deve ter.
  • Leia quatro pares de coordenadas \((x, y)\) via terminal, representando os quatro cantos do quadrilátero de origem (o Sudoku distorcido na imagem original).
  • Calcule automaticamente os quatro pares de coordenadas de destino utilizando as dimensões \(L\) e \(C\) fornecidas, mapeando os cantos para as extremidades da nova imagem: \((0,0)\), \((C-1, 0)\), \((0, L-1)\) e \((C-1, L-1)\).
  • Carregue a imagem local sudoku.png e converta-a para tons de cinza (grayscale).
  • Calcule a matriz de homografia e aplique a transformação espacial na imagem.
  • Saída: Calcule e imprima a soma de todos os pixels da imagem resultante.

📌 Importante:

  • Arquivo de entrada: A imagem sudoku.png deve estar na mesma pasta do script. O programa deve lê-la diretamente do disco (ex: usando mm::read("sudoku.png") ou cv2.imread).
  • Ordem dos Pontos: Garanta que a leitura dos 4 pontos de origem e a geração dos 4 pontos de destino sigam rigorosamente a mesma ordem dos cantos: Superior-Esquerdo (TL), Superior-Direito (TR), Inferior-Esquerdo (BL) e Inferior-Direito (BR).
  • Dimensões no OpenCV: Lembre-se que funções como cv2.warpPerspective esperam o tamanho da imagem de saída no formato (largura, altura), o que equivale a (C, L).
  • Interpolação: Para garantir a consistência matemática da soma dos pixels com o corretor automático, utilize a interpolação bilinear padrão (flags=cv2.INTER_LINEAR).
  • Créditos: A imagem utilizada é “Sudoku en periódico” de Héctor Rodríguez, sob licença CC BY 2.0.

2.12.11.1 🧠 Contexto do Problema

A homografia possui 8 graus de liberdade, exigindo no mínimo 4 correspondências de pontos para ser calculada. Ao contrário de transformações afins, ela mapeia qualquer quadrilátero em outro quadrilátero, permitindo que linhas que convergem para pontos de fuga voltem a ser paralelas:

Operação Característica Aplicação Típica
Homografia Projeção entre planos Retificação de documentos, escaneamento de placas e QR Codes.
Mapeamento Inverso Varredura do destino para a origem Evita “buracos” ou pixels vazios na imagem final retificada.
Warping Reamostragem espacial Correção de distorção de lentes e montagem de panoramas (stitching).

2.12.11.2 📌 Exemplos

Entrada Saída Observação
500
500
100 120
420 95
80 440
450 460
32982820 As duas primeiras entradas são as dimensões de saída (\(L\) e \(C\)). As 4 linhas seguintes são as coordenadas \((x, y)\) dos cantos do Sudoku na imagem original + PAD. A saída é a soma total dos pixels da imagem retificada.
200 200
100 120
420 95
80 440
450 460
5277150 Mesmos pontos de origem do exemplo anterior, mas gerando uma imagem de saída menor (\(200 \times 200\)). A soma dos pixels reduz proporcionalmente devido à escala.

2.12.11.3 Aquisição da imagem do sudoku e conversão para níveis de cinza

A Figura 2.22 mostra a leitura da imagem original seguida da conversão para tons de cinza e redimensionamento para uma matriz de \(500 \times 500\) pixels, preparando os dados para a etapa seguinte.

A correção de perspectiva, aplicada na Figura 2.23 via matriz de homografia, elimina as deformações causadas pelo ângulo da câmera e produz uma visão frontal e regular da grade do Sudoku.

%%writefile tmp/fig_02_sudoku_original.cpp
#define MM_OUT "tmp/fig_02_sudoku_original.png"
//| label: fig-02-sudoku-original
//| fig-cap: "Aquisição da imagem de um Sudoku à esquerda. À direita, conversão para tons de cinza e redimensionamento. Crédito: Héctor Rodríguez de Guardamar, Espanha (CC BY 2.0)."
//| echo: false
//| output: true

#include "morph.hpp"
#include <string>
#include <filesystem>

int main() {
    std::string url = "https://upload.wikimedia.org/wikipedia/commons/e/e7/Sudoku_en_peri%C3%B3dico.jpg";
    mm::Image sudoku_rgb = mm::read(url);
    mm::Image sudoku_gray = mm::gray(sudoku_rgb);
    mm::Image sudoku_small = mm::resize(sudoku_gray, 500, 500);
    mm::write(sudoku_small, "sudoku.png");   // consumida pela célula fig-02-sudoku2

    mm::show(
        std::vector<mm::Image>{sudoku_rgb, sudoku_small},
        MM_OUT,
        std::vector<std::string>{"Original RGB", "Cinza 500x500"},
        2
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(sudoku_rgb, "tmp/fig_02_sudoku_original_0.png");
mm::write(sudoku_small, "tmp/fig_02_sudoku_original_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_sudoku_original.cpp
!g++ -I. -std=c++17 tmp/fig_02_sudoku_original.cpp -o tmp/fig_02_sudoku_original \
  && ./tmp/fig_02_sudoku_original \
  && test -f "tmp/fig_02_sudoku_original.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_sudoku_original.png"
[1] Original RGB
[2] Cinza 500x500
try:
    mm.show(
        [
            mm.read("tmp/fig_02_sudoku_original_0.png"),
            mm.read("tmp/fig_02_sudoku_original_1.png"),
        ],
        titles=[
            'Original RGB',
            'Cinza 500x500',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_sudoku_original_0.png (ver a versao Python)")
Figura 2.22: Aquisição da imagem de um Sudoku à esquerda. À direita, conversão para tons de cinza e redimensionamento. Crédito: Héctor Rodríguez de Guardamar, Espanha (CC BY 2.0).
%%writefile tmp/fig_02_sudoku2.cpp
#define MM_OUT "tmp/fig_02_sudoku2.png"
// | label: fig-02-sudoku2
// | fig-cap: "Correção de perspectiva por homografia 3×3: imagem com *padding* e a vista frontal retificada, via *mm::perspective_transform* (solve 8×8 dos 4 pontos + mapeamento inverso projetivo)."
// | echo: true
// | output: true

#include "morph.hpp"
#include <array>
#include <vector>
#include <string>
#include <filesystem>

int main() {
    // 1. Imagem gravada pela célula anterior (sudoku.png, 500×500, cinza)
    mm::Image img = mm::read("sudoku.png");

    // 2. Padding para não cortar os vértices da grade (mm::pad preenche com 0)
    int PAD = 60;
    mm::Image img_pad = mm::pad(img, PAD);

    // 3. Cantos da grade na imagem expandida — TL, TR, BL, BR
    std::vector<std::array<double, 2>> pts1 = {{100, 160}, {390, 45}, {200, 580}, {570, 420}};

    // 4. Cantos de destino: vista frontal SIZE×SIZE
    int SIZE = 500;
    std::vector<std::array<double, 2>> pts2 = {{0, 0}, {SIZE, 0}, {0, SIZE}, {SIZE, SIZE}};

    // 5. Homografia pts1 -> pts2 e retificação
    mm::Image img_rect = mm::perspective_transform(img_pad, pts1, pts2, SIZE, SIZE);

    // 6. Exibição
    mm::show(
        std::vector<mm::Image>{img_pad, img_rect},
        MM_OUT,
        std::vector<std::string>{"Original (com padding)", "Vista frontal retificada"},
        2
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_pad, "tmp/fig_02_sudoku2_0.png");
mm::write(img_rect, "tmp/fig_02_sudoku2_1.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_02_sudoku2.cpp
!g++ -I. -std=c++17 tmp/fig_02_sudoku2.cpp -o tmp/fig_02_sudoku2 \
  && ./tmp/fig_02_sudoku2 \
  && test -f "tmp/fig_02_sudoku2.png" \
  || echo "⚠ mm::show não gravou tmp/fig_02_sudoku2.png"
tmp/fig_02_sudoku2.cpp: In function ‘int main()’:

tmp/fig_02_sudoku2.cpp:26:57: warning: narrowing conversion of ‘SIZE’ from ‘int’ to ‘double’ [-Wnarrowing]

   26 |     std::vector<std::array<double, 2>> pts2 = {{0, 0}, {SIZE, 0}, {0, SIZE}, {SIZE, SIZE}};

      |                                                         ^~~~

tmp/fig_02_sudoku2.cpp:26:71: warning: narrowing conversion of ‘SIZE’ from ‘int’ to ‘double’ [-Wnarrowing]

   26 | ector<std::array<double, 2>> pts2 = {{0, 0}, {SIZE, 0}, {0, SIZE}, {SIZE, SIZE}};

      |                                                             ^~~~



tmp/fig_02_sudoku2.cpp:26:79: warning: narrowing conversion of ‘SIZE’ from ‘int’ to ‘double’ [-Wnarrowing]

   26 | d::array<double, 2>> pts2 = {{0, 0}, {SIZE, 0}, {0, SIZE}, {SIZE, SIZE}};

      |                                                             ^~~~



tmp/fig_02_sudoku2.cpp:26:85: warning: narrowing conversion of ‘SIZE’ from ‘int’ to ‘double’ [-Wnarrowing]

   26 | :array<double, 2>> pts2 = {{0, 0}, {SIZE, 0}, {0, SIZE}, {SIZE, SIZE}};

      |                                                                 ^~~~



[1] Original (com padding)

[2] Vista frontal retificada
try:
    mm.show(
        [
            mm.read("tmp/fig_02_sudoku2_0.png"),
            mm.read("tmp/fig_02_sudoku2_1.png"),
        ],
        titles=[
            'Original (com padding)',
            'Vista frontal retificada',
        ],
        cols=2,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_02_sudoku2_0.png (ver a versao Python)")
Figura 2.23: Correção de perspectiva por homografia 3×3: imagem com padding e a vista frontal retificada, via mm::perspective_transform (solve 8×8 dos 4 pontos + mapeamento inverso projetivo).
🎮 Simulador EP02_11: Perspectiva do Sudoku Homografia 3×3 · CC BY 2.0
📷 Original (Expandida) — Arraste os Cantos Foto: Héctor Rodríguez · CC BY 2.0
✅ Corrigida (400×400) — Visão Frontal
Carregando imagem do Sudoku...
Figura 2.24: Simulador EP02_11: Correção de Perspectiva do Sudoku (Homografia 3×3 com Reamostragem Bilinear)
%%writefile EP02_11.cpp
// sua solução
Overwriting EP02_11.cpp
TestSuite("EP02_11.cpp").run()
✔️ EP02_11.cases já existe em casos/
📋 4 caso(s) carregado(s) de casos/EP02_11.cases

🔍 Testando C++: EP02_11.cpp
⚠️ EP02_11.cpp: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.