2  Da Captura ao Pixel - Amostragem, Quantização e Conectividade

Este capítulo aprofunda a compreensão da imagem digital, transitando da natureza física da captura para a sua representação matemática discreta. Investigamos como a luz se torna dado e como a organização espacial dos pixels define as relações de vizinhança e conectividade essenciais para algoritmos avançados de Visão Computacional.

2.1 Objetivos

Ao final deste capítulo, você será capaz de:

  • Explicar o modelo físico de formação da imagem baseado em iluminação e refletância.
  • Diferenciar os mecanismos da visão humana e dos sensores digitais.
  • Compreender os processos de amostragem (discretização do espaço) e quantização (discretização da intensidade).
  • Descrever as relações topológicas entre pixels: vizinhança, adjacência, conectividade e distâncias.
  • Realizar transformações geométricas básicas (translação, rotação, escala) preservando a qualidade.
  • Visualizar na prática os efeitos da variação da resolução espacial e da profundidade de bits.

2.2 O Olho e a Câmera - Elementos da Percepção Visual

A formação de uma imagem digital começa com a captura da luz refletida pelos objetos. Como ilustrado na Figura 2.1, tanto o olho humano quanto as câmeras digitais seguem princípios ópticos semelhantes para focar a luz em uma superfície sensível, embora utilizem mecanismos biológicos e eletrônicos distintos para a transdução do sinal.

2.2.1 Visão humana

O olho funciona como um sistema óptico complexo: a luz atravessa a córnea, o humor aquoso, a pupila (controlada pela íris) e o cristalino — que ajusta o foco dinamicamente — até atingir a retina. Na retina, encontram-se os fotorreceptores: os cones (≈6 milhões), concentrados na fóvea, são responsáveis pela visão de cores e detalhes, enquanto os bastonetes (≈120 milhões) garantem a visão em baixa luminosidade (visão escotópica), detectando apenas intensidades de cinza. O ponto cego é a região de onde parte o nervo óptico, carecendo de receptores.

2.2.2 Sensores digitais

Nas câmeras, os sensores de imagem desempenham o papel da retina. Os dois tipos mais comuns são o CCD (Charge-Coupled Device - Dispositivo de Carga Acoplada) e o CMOS (Complementary Metal-Oxide-Semiconductor - Semicondutor de Óxido Metálico Complementar). O sensor é composto por uma matriz de fotossítios (pixels) que acumulam carga elétrica proporcional à luz incidente.

Para a reconstrução de cores, utiliza-se o Filtro de Bayer, uma matriz de filtros coloridos que permite que cada pixel capture apenas uma componente de cor: vermelho, verde ou azul (RGGB - Red, Green, Green, Blue). Posteriormente, um ADC (Analog-to-Digital Converter - Conversor Analógico-Digital) quantiza essa carga em valores numéricos, definidos por uma profundidade de bits (ex.: 8 bits, resultando em 256 níveis de intensidade).

Curiosidade: Embora o olho humano tenha milhões de receptores, a resolução de alta definição é restrita à fóvea (visão central), equivalente a aproximadamente 120 × 120 pixels. A percepção de uma cena completa em alta resolução é fruto de um intenso pós-processamento realizado pelo cérebro.

Figura 2.1: Comparativo didático entre o sistema visual biológico e o eletrônico: no topo, a anatomia do olho humano destacando a retina e os fotorreceptores (cones e bastonetes); abaixo, a estrutura de uma câmera digital detalhando o sensor CMOS, a matriz de filtros de Bayer (RGGB) e o processo de quantização digital realizado pelo ADC.

2.3 Ilusões de Ótica: Os Desafios da Percepção Visual

Enquanto os sensores digitais capturam a intensidade da luz de forma linear e objetiva, o sistema visual humano interpreta a cena com base em contexto, experiências prévias e mecanismos biológicos de sobrevivência. As ilusões de ótica não são “erros” do olho, mas evidências do intenso pós-processamento cerebral realizado no córtex visual.

2.3.1 Ambiguidade e Contexto

O cérebro busca constantemente dar sentido a padrões ambíguos. No exemplo do Vaso de Rubin (veja a Figura 2.2), a percepção alterna entre a figura (vaso) e o fundo (dois rostos), demonstrando que não conseguimos processar ambas as interpretações simultaneamente. Já a ilusão do Elefante de Shepard brinca com a nossa incapacidade de reconciliar linhas de contorno que sugerem volume em posições logicamente impossíveis.

2.3.2 Brilho e Contraste Local

Muitas ilusões decorrem da inibição lateral, mecanismo pelo qual neurônios vizinhos na retina competem entre si para realçar bordas. Na Grade de Scintillating, pontos escuros “fantasmas” parecem surgir nas interseções brancas devido a esse processamento local de contraste.

A ilusão da Sombra no Tabuleiro de Adelson é talvez a mais impactante para a Visão Computacional: o quadrado “A” e o quadrado “B” possuem exatamente o mesmo valor de cinza no sensor (ou arquivo digital), mas o cérebro “corrige” o brilho de “B” por entender que ele está sob uma sombra projetada, percebendo-o como mais claro.

2.3.3 Geometria e Perspectiva

A percepção de profundidade pode ser enganada por construções geométricas que desafiam a lógica tridimensional a partir de um ângulo de visão específico. A Escada de Schröder utiliza a ambiguidade da perspectiva para criar um objeto que parece subir ou descer dependendo de como é observado, evidenciando como a nossa interpretação de “cima” e “baixo” depende do ponto de fuga.

Figura 2.2: Coletânea de desafios perceptivos: (topo esquerdo) Vaso de Rubin — ambiguidade figura-fundo; (topo central) Elefante de Shepard — incongruência geométrica; (topo direita) Sombra de Adelson — constância de brilho baseada no contexto; (inferior esquerdo) Grade de pontos — inibição lateral; (inferior direito) Escada de Schröder.

2.4 O Modelo Matemático da Formação da Imagem

Uma imagem pode ser modelada como o produto de duas funções:

\[ f(x,y) = i(x,y) \cdot r(x,y) \tag{2.1}\]

onde:

  • \(i(x,y)\) é a iluminação incidente sobre a cena (energia luminosa por unidade de área). Depende da fonte de luz.
  • \(r(x,y)\) é a refletância do objeto (fração da luz refletida). Depende das propriedades do material e da superfície.

Na prática, as duas componentes variam em faixas distintas: \(i(x,y)\) varia lentamente no espaço, enquanto \(r(x,y)\) pode variar rapidamente (texturas, bordas). O processamento de imagens frequentemente tenta separar ou compensar essas componentes (ex.: correção de iluminação não uniforme).

2.5 Digitalização: Amostragem e Quantização

Para transformar uma cena contínua em uma imagem digital, dois processos são necessários.

2.5.1 Amostragem - Discretização do espaço

A amostragem consiste em medir o valor da função \(f(x,y)\) em pontos igualmente espaçados, formando uma matriz de \(M\) linhas (altura) e \(N\) colunas (largura). Cada elemento dessa matriz é um pixel. A resolução espacial é dada por \(M \times N\). Quanto maior a resolução, mais detalhes espaciais são preservados, mas maior também o custo computacional e de armazenamento.

2.5.2 Quantização - Discretização da intensidade

A quantização associa a cada pixel um valor numérico discreto, geralmente representado por um inteiro de \(b\) bits. A profundidade de bits define o número de níveis de intensidade: \(2^b\). Imagens em tons de cinza costumam usar 8 bits (256 níveis). Imagens coloridas usam três canais de 8 bits (24 bits no total).

Ilustração: Se usarmos apenas 1 bit por pixel (preto e branco), perdemos todos os tons intermediários. Com 2 bits (4 níveis) já se percebe degradês grosseiros. Com 8 bits, o olho humano dificilmente percebe a discretização (visão contínua).

AvisoErro de quantização

Erro de quantização é a diferença entre o valor analógico real e o valor discreto atribuído. Ele se manifesta como ruído de quantização, visível em regiões com gradiente suave quando se usa poucos bits (efeito de “posterização”).

2.5.3 Laboratório prático: Efeitos da amostragem e quantização

Os experimentos a seguir mostram como a redução da resolução espacial (subamostragem) e da profundidade de bits degradam a qualidade visual. Use o código para explorar diferentes fatores e níveis de cinza.

import os, importlib, urllib.request
import numpy as np
import matplotlib.pyplot as plt
import cv2

# Baixar morph.py se necessário
BASE_URL = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph"
for f in ["morph.py"]:
    if not os.path.exists(f):
        urllib.request.urlretrieve(f"{BASE_URL}/{f}", f)

import morph
importlib.reload(morph)
from morph import mm

print("✅ Ambiente pronto")
✅ Ambiente pronto
# Carregar imagem de exemplo
base = "https://upload.wikimedia.org/wikipedia/commons"

arquivo = (
    "Area_de_Prote%C3%A7%C3%A3o_Ambiental_"
    "Quilombos_do_M%C3%A9dio_Ribeira_-_Thomas-"
    "Fuhrmann_%282023-_02%29_Malacoptila_striata.jpg"
)

url = f"{base}/c/c5/{arquivo}"
caminho = "imagens/barbudo-rajado.jpg"

if not os.path.exists(caminho):
    os.makedirs("imagens", exist_ok=True)
    img_pil = mm.read(url, pil=True)   # retorna PIL Image com EXIF
    mm.write(img_pil, caminho)         # salva preservando EXIF
else:
    img_pil = mm.read(caminho, pil=True)

img_numpy = np.array(img_pil)

exif = img_pil._getexif()   # agora funciona — img_pil é PIL Image


img_color = mm.read(url)
img_gray0 = mm.gray(img_color)
print(f"Imagem original: {img_color.shape}")
print(f"Tipo da imagem: {type(img_color)}")
Imagem original: (3067, 2047, 3)
Tipo da imagem: <class 'numpy.ndarray'>

O experimento apresentado na Figura 2.3 ilustra o compromisso entre a resolução espacial e o custo de armazenamento em memória. O código utiliza a técnica de subamostragem por fatiamento (slicing) para reduzir a matriz original de pixels conforme um fator \(f\), resultando em uma economia drástica de memória — por exemplo, um fator \(f=8\) reduz o tamanho do dado em 64 vezes (\(8^2\)). Para fins de comparação visual, as imagens reduzidas são restauradas às dimensões originais (\(512 \times 512\)) através da interpolação por vizinho mais próximo (nearest). Este processo não recupera a informação perdida, mas torna evidente o efeito de aliasing e a estrutura de blocos (pixelização) gerada pela baixa densidade de dados da matriz amostrada.

def subsample_simple(image, f):
    # Subamostragem via fatiamento (slicing)
    reduced = image[::f, ::f]
    
    # Cálculo de memória em KB
    mem_kb = reduced.nbytes / 1024
    label = f"{reduced.shape[1]}x{reduced.shape[0]}, {int(mem_kb)} KB\n(Fator {f})"
    
    # Restaura o tamanho para visualização (H, W originais)
    res = mm.resize(reduced, (image.shape[1], image.shape[0]), method='nearest')
    return res, label

factors = [1, 4, 8, 12]
img_gray =img_gray0[820:1850, 890:1550] # Recorte para melhor visualização dos detalhes

# Gera os resultados e separa em listas para o mm.show
results = [subsample_simple(img_gray, f) for f in factors]
imgs_list = [r[0] for r in results]
titles_list = [r[1] for r in results]

mm.show(imgs_list, titles=titles_list, cols=4, figsize=(16, 12))
Figura 2.3: Efeito da subamostragem. Os títulos exibem as dimensões (W x H) e o tamanho da matriz em memória (KB).

O experimento na Figura 2.4 foca na quantização de intensidade, o processo de discretização da amplitude da função \(f(x,y)\). Enquanto a subamostragem afeta a grade espacial, a redução da profundidade de bits limita a quantidade de tons de cinza disponíveis para representar o brilho.

Ao reduzir a profundidade de 8 bits (256 níveis) para valores menores, surge o efeito de posterização, onde os gradientes suaves de uma cena são substituídos por transições abruptas. No limite de 1 bit, a imagem torna-se estritamente binária, preservando apenas a silhueta e perdendo detalhes de textura e volume.

2.5.4 Análise Técnica

  • Domínio vs. Codomínio: Note que a resolução espacial (dimensões da matriz) permanece constante em 512x512; o que se altera é apenas o codomínio da função da imagem.
  • Constância de Memória: Observe nos títulos que o tamanho em KB não diminui. Isso ocorre porque o NumPy armazena cada pixel quantizado em um contêiner de 8 bits (uint8), independentemente de o valor real ser apenas 0 ou 1.
  • Percepção: A degradação visual torna-se crítica abaixo de 4 bits, onde o olho humano começa a perceber as “fronteiras” artificiais criadas pela falta de tons intermediários.
def quantize_simple(image, bits):
    """Reduz a profundidade de bits e calcula metadados de memória."""
    levels = 2 ** bits
    # Normalização e quantização uniforme
    quantized = (np.floor(image / 256 * levels) / levels * 255).astype(np.uint8)
    
    # Cálculo de memória em KB
    mem_kb = quantized.nbytes / 1024
    label = f"{bits} bits ({levels} níveis)\n{int(mem_kb)} KB"
    
    return quantized, label

# Lista de bits para teste (8 é o padrão, 1 é o binário)
bits_test = [8, 4, 2, 1]

# Gera os resultados e separa em listas para o mm.show
results_q = [quantize_simple(img_gray, b) for b in bits_test]
imgs_q = [r[0] for r in results_q]
titles_q = [r[1] for r in results_q]

mm.show(imgs_q, titles=titles_q, cols=4, figsize=(16, 12))
Figura 2.4: Efeito da redução da profundidade de bits. Os títulos exibem a quantidade de bits, níveis e o tamanho em memória (KB).
# Exemplo de compactação real para economia de memória
import numpy as np

# Imagem binária em uint8 (512x512)
img_uint8 = np.zeros((512, 512), dtype=np.uint8) 
print(f"Tamanho uint8: {img_uint8.nbytes / 1024} KB") # 256 KB

# Imagem compactada (Bit-packing)
img_packed = np.packbits(img_uint8)
print(f"Tamanho Compactado: {img_packed.nbytes / 1024} KB") # 32 KB
Tamanho uint8: 256.0 KB
Tamanho Compactado: 32.0 KB

A limitação de tipos de dados menores que um byte no ecossistema Python/NumPy deve-se à arquitetura de hardware, que endereça a memória em blocos de 8 bits (Bytes). Para que se mantenha a compatibilidade com o OpenCV e se garanta a eficiência, mapeiam-se até mesmo elementos binários para contêineres de 1 byte (uint8 ou bool8).

Embora linguagens como ANSI C permitam a compactação de 8 pixels por byte (bit-packing), tal abordagem exige a descompactação constante para cálculos e impõe alta complexidade na manipulação de ponteiros. Conforme a Tabela 2.1, privilegia-se o uso de uint8 pela facilidade no acesso a vizinhos e pela versatilidade em transformações geométricas. Além disso, métodos nativos do NumPy e OpenCV executam o processamento internamente em baixo nível (C/C++), o que torna operações vetorizadas mais rápidas do que implementações manuais com laços aninhados em Python.

Tabela 2.1: Comparativo entre estratégias de compactação e eficiência de processamento.
Característica Python (NumPy/OpenCV) ANSI C (Bit-packing)
Menor Unidade 1 Byte (8 bits) 1 Bit
Memória (Binária) 256 KB (para 512x512) 32 KB (para 512x512)
Velocidade Alta (Vetorização em C) Variável (Lenta se houver bit-shift)
Complexidade Baixa: Métodos prontos Alta: Ponteiros e Máscaras

2.6 Relações entre Pixels - Topologia da Imagem

Os pixels não são elementos isolados; suas posições relativas definem importantes conceitos para processamento.

2.6.1 Vizinhança

Dado um pixel de coordenadas \((x,y)\), definem-se dois tipos principais de vizinhança (para imagens em \(grid\) retangular):

  • Vizinhança-4 (von Neumann): inclui os pixels nas posições \((x-1,y)\), \((x+1,y)\), \((x,y-1)\), \((x,y+1)\).
  • Vizinhança-8 (Moore): inclui todos os oito pixels adjacentes (acrescenta os quatro diagonais).

A escolha da vizinhança influencia operações como detecção de bordas, cálculo de gradientes e conectividade.

# Criação de uma matriz 3x3 para exemplo topológico
viz = np.zeros((3, 3), dtype='uint8')

# Pixel central (p)
viz[1, 1] = 150 

# Definindo Vizinhança-4 (N4) com valor diferente para destaque
viz[0, 1] = viz[2, 1] = viz[1, 0] = viz[1, 2] = 255

# Exibição da matriz para análise de coordenadas
mm.drawImagePlt(viz, scale=40)
Figura 2.5: Ilustração de vizinhanças 4 em uma matriz 3x3. No centro (1,1), o pixel de interesse.

2.6.2 Adjacência, conectividade e caminhos

Dois pixels são adjacentes se estão em contato segundo uma vizinhança definida e satisfazem um critério de valor (ex.: mesmo nível de intensidade). Uma conectividade define uma relação de equivalência entre pixels que formam uma região conexa. Um caminho é uma sequência de pixels adjacentes.

A conectividade-4 e conectividade-8 podem produzir resultados diferentes na segmentação e no cálculo de componentes conexas (etiquetagem). Por exemplo, um padrão de tabuleiro de xadrez pode ser completamente desconexo em 4-vizinhança, mas totalmente conexo em 8-vizinhança.

2.6.3 Distâncias entre pixels

As métricas de distância são fundamentais para quantificar a proximidade física e a conectividade entre os elementos que compõem a grade digital. Conforme demonstrado na Tabela 2.2, a escolha da métrica define o custo de deslocamento entre pixels e altera o comportamento de algoritmos de segmentação e análise morfológica.

Para medir a distância entre dois pixels \(p(x_1, y_1)\) e \(q(x_2, y_2)\), utilizam-se diferentes funções métricas que impõem restrições de movimento distintas sobre o grid:

Tabela 2.2: Comparativo de métricas de distância aplicadas à malha de pixels.
Métrica Definição Interpretação
Euclidiana \(\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}\) Distância em linha reta (contínua)
Manhattan (City block) \(|x_1-x_2| + |y_1-y_2|\) Movimentos horizontais + verticais
Chebyshev (Tabuleiro) \(\max(|x_1-x_2|, |y_1-y_2|)\) Movimentos incluindo diagonais

Essas distâncias são aplicadas em diversos contextos de PDI, incluindo algoritmos de interpolação geométrica, transformadas de distância, crescimento de regiões e análise de formas.

NotaExemplo prático

Considerando dois pixels com deslocamentos relativos \(\Delta x = 3\) e \(\Delta y = 4\):

  • Euclidiana: \(\sqrt{3^2 + 4^2} = 5\) (hipotenusa do triângulo retângulo).
  • Manhattan: \(3 + 4 = 7\) (soma dos catetos).
  • Chebyshev: \(\max(3, 4) = 4\) (predomínio do maior deslocamento).

2.7 Armazenamento de Imagens

A escolha do formato de arquivo é um passo decisivo no fluxo de processamento, pois determina como os dados de amostragem e quantização serão preservados ou descartados. Conforme se apresenta na Tabela 2.3, cada extensão equilibra de forma distinta a fidelidade dos dados e a eficiência de armazenamento.

As imagens digitais podem ser armazenadas em diversos formatos, cada um com características que impactam o processamento posterior:

Tabela 2.3: Principais formatos de armazenamento e suas aplicações em PDI.
Formato Características Uso típico
PGM Formato simples de mapa de cinzas (texto ou binário). Pesquisa acadêmica e ferramentas Unix.
BMP Não comprimido (ou compressão simples). Windows, aplicações legado.
PNG Compressão sem perdas (lossless). Web, imagens com transparência.
JPEG Compressão com perdas (lossy), ideal para fotografias. Fotos, imagens médicas (uso moderado).
TIFF Suporta múltiplas camadas e compressão variada. Editoração, arquivamento.
RAW Dados brutos (sensor ou matriz sem cabeçalho). Fotografia profissional e exames médicos.

Os metadados de uma imagem incluem parâmetros como largura, altura, profundidade de bits e codificação de cor. Em formatos científicos, preservam-se também informações de calibração e detalhes da captura. Ao utilizar-se a função mm.read(), a biblioteca morph preserva automaticamente esses dados para que se respeitem as propriedades originais da imagem.

Em contextos científicos e hospitalares, privilegia-se o padrão DICOM para garantir que não haja perda de precisão diagnóstica. Repositórios públicos como o The Cancer Imaging Archive (TCIA), o Alzheimer’s Disease Neuroimaging Initiative (ADNI) e o PhysioNet disponibilizam vastos conjuntos de dados neste formato, incluindo metadados clínicos anonimizados que são essenciais para a investigação científica.

2.7.1 Exemplo: Extração de Metadados e Localização GPS

Diferente da matriz de pixels pura obtida pela leitura convencional — como na imagem do pássaro apresentada no início deste capítulo —, o uso do argumento info=True no método mm.read() altera a natureza do objeto retornado (ver Figura 2.6). Enquanto o imread do OpenCV retorna um numpy.ndarray (padrão utilizado quando info=False), a leitura com informações preservadas retorna um objeto especializado da biblioteca Pillow, capaz de interpretar o cabeçalho EXIF.

O cabeçalho EXIF (Exchangeable Image File Format) funciona como um repositório técnico da captura, permitindo que o objeto Pillow interprete uma vasta gama de informações que vão muito além das coordenadas de GPS. Ao utilizar info=True, o sistema ganha acesso ao “DNA” da imagem, incluindo metadados de hardware (marca e modelo da câmera), configurações ópticas (abertura, distância focal e tempo de exposição) e parâmetros de iluminação (uso de flash e balanço de branco).

Essa distinção é importante no PDI, pois transforma a matriz de amostragem em um conjunto de dados contextualizado, onde as características físicas do sensor e da lente podem ser utilizadas para normalizar brilhos ou corrigir distorções geométricas.

from PIL.ExifTags import TAGS
import os, numpy as np

base    = "https://upload.wikimedia.org/wikipedia/commons"
arquivo = (
    "Area_de_Prote%C3%A7%C3%A3o_Ambiental_"
    "Quilombos_do_M%C3%A9dio_Ribeira_-_Thomas-"
    "Fuhrmann_%282023-_02%29_Malacoptila_striata.jpg"
)
url     = f"{base}/c/c5/{arquivo}"
caminho = "imagens/barbudo-rajado.jpg"

# 1. Leitura — preserva objeto PIL com EXIF
if not os.path.exists(caminho):
    os.makedirs("imagens", exist_ok=True)
    img_obj = mm.read(url, pil=True)
    mm.write(img_obj, caminho)      # salva preservando EXIF
else:
    img_obj = mm.read(caminho, pil=True)

img_numpy = np.array(img_obj)      # conversão para NumPy

# 2. Extração e conversão de GPS (Tag 34853)
exif = img_obj._getexif()
if exif and (gps := exif.get(34853)):
    to_dec = lambda dms, ref: float(-(dms[0]+dms[1]/60+dms[2]/3600) if ref in 'SW'
                                    else (dms[0]+dms[1]/60+dms[2]/3600))
    lat, lon = to_dec(gps[2], gps[1]), to_dec(gps[4], gps[3])
    print(f"GPS Decimal: {lat:.6f}, {lon:.6f}")
    print(f"Maps: https://www.google.com/maps/search/?api=1&query={lat},{lon}")

# 3. Diagnóstico de tipos, dimensões e acesso a pixels
print(f"\nTipo PIL  : {type(img_obj)}   | Dimensões (x,y): {img_obj.size}")
print(f"Pillow (0,0): {img_obj.getpixel((0, 0))}")
print(f"Tipo NumPy: {type(img_numpy)} | Dimensões [y,x,c]: {img_numpy.shape}")
print(f"NumPy [0,0]: {img_numpy[0, 0]}")

# 4. Exibição
mm.show(img_numpy, scale=30)
GPS Decimal: -24.587955, -48.629758
Maps: https://www.google.com/maps/search/?api=1&query=-24.587955,-48.629758333333335

Tipo PIL  : <class 'PIL.JpegImagePlugin.JpegImageFile'>   | Dimensões (x,y): (2047, 3067)
Pillow (0,0): (58, 96, 0)
Tipo NumPy: <class 'numpy.ndarray'> | Dimensões [y,x,c]: (3067, 2047, 3)
NumPy [0,0]: [58 96  0]
Figura 2.6: APA Quilombos do Médio Ribeira - Barbudo-rajado (Malacoptila striata). Crédito: Thomas Fuhrmann (CC BY-SA 4.0).
NotaNota Pedagógica: A sutil diferença das dimensões

Repare que a representação das dimensões muda conforme a estrutura de dados utilizada:

  • No Pillow (.size): Retorna (Largura, Altura). É uma visão orientada ao arquivo de imagem.
  • No NumPy (.shape): Segue a convenção matemática de matrizes: (Linhas/Altura, Colunas/Largura, Canais).

Esta distinção é fundamental para evitar erros de indexação ao implementar filtros manuais. Enquanto o objeto Pillow carrega o “onde” e o “quando” (contexto), o array NumPy carrega o “quanto” de luz (intensidade) existe em cada ponto da imagem.

2.7.2 Por que esta separação é importante?

Ao carregar uma imagem via cv2.imread(), o resultado é um <class 'numpy.ndarray'>, que contém estritamente os valores numéricos resultantes da quantização e amostragem. No entanto, ao utilizar info=True, o mm.read() retorna um objeto da classe PIL.JpegImagePlugin.JpegImageFile.

Esta classe mantém o arquivo “aberto” para permitir o acesso ao contexto da captura antes que os dados sejam convertidos em uma matriz bruta. Essa separação é vital: pixels servem para algoritmos; metadados servem para georreferenciamento, catalogação científica e correções baseadas no hardware de aquisição.

2.8 Transformações Geométricas Básicas

As transformações geométricas alteram a posição dos pixels, mantendo os valores de intensidade. São fundamentais para alinhamento, correção de distorções e aumento de dados (data augmentation) em aprendizado de máquina.

Uma transformação afim é qualquer mapeamento que preserve colinearidade (pontos sobre uma reta permanecem sobre uma reta) e razões de distâncias entre pontos colineares. Em 2D, toda transformação afim pode ser expressa em coordenadas homogêneas por uma matriz \(3 \times 3\):

\[ \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \underbrace{\begin{bmatrix} a_{11} & a_{12} & t_x \\ a_{21} & a_{22} & t_y \\ 0 & 0 & 1 \end{bmatrix}}_{\mathbf{T}} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} \tag{2.2}\]

A sub-matriz \(2 \times 2\) superior esquerda \(\mathbf{A} = \begin{bmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{bmatrix}\) controla rotação, escala e cisalhamento; o vetor \((t_x, t_y)^\top\) controla a translação. As transformações geométricas mais usadas em PDI — translação, rotação e escala — são casos particulares de \(\mathbf{T}\), e podem ser compostas por multiplicação de matrizes, na ordem \(\mathbf{T} = \mathbf{T}_n \cdots \mathbf{T}_2 \mathbf{T}_1\).

NotaTransformação inversa e interpolação

Na implementação prática (cv2.warpAffine), aplica-se a transformação inversa: para cada pixel \((x', y')\) da imagem destino, calcula-se a posição de origem \((x, y) = \mathbf{T}^{-1}(x', y')\) e interpola-se o valor. Isso evita buracos na imagem resultante causados pelo mapeamento direto de pixels inteiros para posições não inteiras.

2.8.1 Translação

A translação é a transformação afim mais simples: desloca todos os pixels por um vetor \((t_x, t_y)\). Em coordenadas homogêneas, é expressa pela matriz:

\[ \begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} \quad \Rightarrow \quad \begin{cases} x' = x + t_x \\ y' = y + t_y \end{cases} \tag{2.3}\]

A terceira linha da matriz garante que a operação permaneça no espaço afim, permitindo que translação, rotação e escala sejam combinadas por simples multiplicação de matrizes. Na prática, cv2.warpAffine usa apenas as duas primeiras linhas (matriz \(2 \times 3\)), pois a terceira é sempre \([0, 0, 1]\).

Pixels deslocados para além da área original são descartados; áreas descobertas são preenchidas com 0 (preto). Ver um exemplo na Figura 2.7.

img_tx1 = mm.translate(img_gray, 50, 50)
img_tx2 = mm.translate(img_gray, 100, 50)
mm.show([img_gray, img_tx1, img_tx2],
        titles=["Original", "Translação (50,50)", "Translação (100,50)"],
        cols=3, figsize=(16, 12))
Figura 2.7: Exemplo de translação da imagem do pássaro com deslocamentos (50,50) e (100,50).

2.8.2 Rotação

A rotação por ângulo \(\theta\) em torno de um ponto central \((c_x, c_y)\) é composta por três transformações afins: translação para a origem, rotação pura e translação de volta. A matriz resultante é:

\[ \mathbf{T}_{\text{rot}} = \begin{bmatrix} \cos\theta & -\sin\theta & c_x(1-\cos\theta) + c_y\sin\theta \\ \sin\theta & \cos\theta & c_y(1-\cos\theta) - c_x\sin\theta \\ 0 & 0 & 1 \end{bmatrix} \tag{2.4}\]

Na implementação, cv2.getRotationMatrix2D gera diretamente as duas primeiras linhas de \(\mathbf{T}_{\text{rot}}\) (matriz \(2 \times 3\) para warpAffine), aceitando também um fator de escala \(s\) que multiplica \(\cos\theta\) e \(\sin\theta\). Ver exemplo na Figura 2.8.

img_rot30 = mm.rotate(img_gray, 30, interp='bilinear')
img_rot45 = mm.rotate(img_gray, 45, interp='bilinear')

mm.show([img_gray, img_rot30, img_rot45],
        titles=["Original", "Rotação 30°", "Rotação 45°"],
        cols=3, figsize=(16, 12))
Figura 2.8: Exemplo de rotação da imagem do pássaro em 30° e 45° usando interpolação bilinear.

2.8.3 Escala (Redimensionamento)

O redimensionamento por fatores \((s_x, s_y)\) é uma transformação afim com matriz:

\[ \mathbf{T}_{\text{escala}} = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \tag{2.5}\]

Quando \(s > 1\) (ampliação), pixels da imagem destino mapeiam para posições não inteiras na origem — exigindo interpolação para estimar o valor. Quando \(s < 1\) (redução), múltiplos pixels de origem contribuem para um único pixel destino — exigindo decimação. Os três métodos disponíveis em mm.resize diferem na qualidade e no custo computacional, conforme a Tabela 2.4:

Tabela 2.4: Métodos de interpolação disponíveis em mm.resize e seus respectivos números de vizinhos utilizados no cálculo.
Método Vizinhos usados Característica
'nearest' 1 Rápido; produz efeito de blocos (pixelation)
'bilinear' 4 Bom compromisso qualidade/custo; bordas suaves
'bicubic' 16 Maior nitidez; preferido em softwares profissionais
# 1. Recorte da região do bico
y, x, offset = 210, 40, 40
crop = img_gray[y-offset:y+offset, x-offset:x+offset]
print(f"Imagem: {img_gray.shape} | Crop: {crop.shape}")

# 2. Ampliar 4× com mm.resize
crop_nearest  = mm.resize(crop, 4, method='nearest')
crop_bilinear = mm.resize(crop, 4, method='bilinear')

# 3. Exibição comparativa
mm.show(
    [crop, crop_nearest, crop_bilinear],
    titles=["Original (recorte)", "Vizinho mais próximo (4×)", "Bilinear (4×)"],
    cols=3, figsize=(16, 12), dpi=200
)
Imagem: (1030, 660) | Crop: (80, 80)
Figura 2.9: Comparação de interpolação com zoom no detalhe do olho (recorte 60×60, ampliado 4×). Note o efeito de blocos no vizinho mais próximo vs. a suavização na bilinear.

2.8.4 Cisalhamento (Shear)

O cisalhamento é uma transformação afim que distorce a imagem ao deslocar cada pixel proporcionalmente à sua posição em um eixo. A matriz geral combina cisalhamento horizontal (\(sh_x\)) e vertical (\(sh_y\)):

\[ \mathbf{T}_{\text{cisalh}} = \begin{bmatrix} 1 & sh_x & 0 \\ sh_y & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \tag{2.6}\]

Para \(sh_x \neq 0\) e \(sh_y = 0\), cada linha é deslocada horizontalmente proporcionalmente à sua posição vertical — produzindo o efeito de “inclinação” característico. Ver exemplo na Figura 2.10.

img_shx = mm.shear(img_gray, shx=0.3)
img_shy = mm.shear(img_gray, shy=0.3)
img_shc = mm.shear(img_gray, shx=0.2, shy=0.2)

mm.show(
    [img_gray, img_shx, img_shy, img_shc],
    titles=["Original", "Horiz. (shx=0.3)", "Vert. (shy=0.3)", "Combinado (0.2, 0.2)"],
    cols=4, figsize=(16, 12)
)
Figura 2.10: Exemplo de cisalhamento da imagem do pássaro: horizontal (shx=0.3), vertical (shy=0.3) e combinado (shx=0.2, shy=0.2).

2.9 Resumo

Neste capítulo foram apresentados os fundamentos de digitalização e topologia de imagens:

  • Formação da imagem: \(f(x,y) = i(x,y) \cdot r(x,y)\).
  • Amostragem: discretização do espaço → resolução espacial \(M \times N\).
  • Quantização: discretização da intensidade → profundidade de bits \(b\).
  • Relações topológicas: vizinhança-4, vizinhança-8, conectividade, distâncias (Euclidiana, Manhattan, Chebyshev).
  • Transformações geométricas: translação, rotação, escala (com interpolação bilinear ou vizinho mais próximo).
  • Formatos de arquivo: BMP, PNG, JPEG, TIFF, RAW; cada um com diferentes compromissos entre qualidade e tamanho.

O Capítulo 3 abordará operações espaciais como convolução, filtragem e morfologia matemática (erosão, dilatação).

2.10 🤖 Uso do NotebookLM como Tutor Complementar

Nesta edição, incentivamos o uso do NotebookLM como ferramenta complementar de aprendizagem. Essa ferramenta de IA utiliza exclusivamente os documentos fornecidos pelo autor como base de conhecimento, garantindo respostas coerentes com o conteúdo do livro.

Para cada capítulo, preparamos um projeto específico na plataforma. Para uma experiência de estudo ampliada, utilize o acesso abaixo:

Importante🎓 Estude com o Tutor Inteligente

Para interagir com o conteúdo deste capítulo, acesse o link a seguir. O ambiente contém materiais didáticos em diferentes formatos, gerados a partir do PDF do capítulo. Na plataforma, explore especialmente as opções Guia de Estudo e Conversa para aprofundar sua compreensão.

🚀 ACESSAR NOTEBOOKLM: CAPÍTULO 02

2.11 Lista de Exercícios

  1. (15%) Explique, com suas próprias palavras, a diferença entre amostragem e quantização. Dê um exemplo concreto de cada uma no contexto de uma imagem digital.

  2. (15%) Considere uma imagem com resolução espacial de 1024 × 768 pixels e profundidade de 24 bits (8 bits por canal RGB). Calcule o tamanho total não comprimido da imagem em bytes e em megabytes.

  3. (20%) Utilizando o código do laboratório, modifique o fator de subamostragem para 3 e para 6. Descreva visualmente o que ocorre com as bordas dos objetos. O que é o efeito de aliasing?

  4. (20%) Para a imagem em tons de cinza, aplique quantização com 3 bits (8 níveis) e 5 bits (32 níveis). Compare os resultados e explique por que 5 bits já pode ser considerado suficiente para muitas aplicações.

  5. (15%) Dados dois pixels \(A=(10,20)\) e \(B=(15,25)\), calcule as distâncias Euclidiana, Manhattan e Chebyshev entre eles.

  6. (15%) Usando a função mm.rotate, gire a imagem do pássaro em ângulos de 90°, 180° e 270° com interpolação bilinear. Compare com a rotação usando method='nearest'. Em quais situações a interpolação vizinho mais próximo ainda é útil?

Referências do Capítulo

A fundamentação teórica deste capítulo baseia-se nas seguintes obras:

  • Gonzalez; Woods (2018) para os conceitos de amostragem, quantização e relações entre pixels.
  • Szeliski (2022) para transformações geométricas e conectividade.
  • Bradski; Kaehler (2008) para a implementação prática com OpenCV e morph.py.

2.12 💻 Parte Prática com Exercícios de Programação


🎯 Objetivo deste Caderno

O caderno permite desenvolver, validar, organizar e testar soluções de Exercícios de Programação (EPs) em ambientes interativos, como o Colab, com os mesmos casos de teste do Moodle, copiando para lá apenas na hora de registrar a nota oficial.

Download

Baixe morph.py e testsuite.py executando a célula abaixo:

import os, sys, importlib, inspect, urllib.request

# URLs do repositório
BASE_URL = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph"
for f in ["morph.py", "testsuite.py"]:
    if not os.path.exists(f):
        urllib.request.urlretrieve(f"{BASE_URL}/{f}", f)

import morph, testsuite
importlib.reload(morph); importlib.reload(testsuite)
from morph import mm
from testsuite import TestSuite

print(f"✅ Ambiente pronto. Morph: {morph.__version__} | TestSuite: {testsuite.__version__}")
✅ Ambiente pronto. Morph: 1.1.0 | TestSuite: 1.1.0

2.12.1 EP02_01 ☀️ Ajuste de Brilho e Contraste

Nesta atividade, você deve implementar uma transformação linear de intensidade para ajustar o brilho e o contraste de uma imagem.

  • Leia dois inteiros L e C, representando as dimensões da matriz.
  • Leia um valor real \(\alpha\) (contraste) e um inteiro \(\beta\) (brilho).
  • Leia os valores inteiros da matriz.
  • Para cada pixel \(p\), calcule o novo valor \(p'\) usando a fórmula:

\[p' = \text{clip}(\text{round}(\alpha \times p + \beta))\]

  • Imprima a matriz resultante com os valores finais.

📌 Importante:

  • Saturação (Clip): O resultado final deve ser obrigatoriamente um inteiro no intervalo \([0, 255]\). Valores menores que 0 tornam-se 0, e valores maiores que 255 tornam-se 255.
  • Arredondamento: Realize o arredondamento matemático antes de converter para inteiro e aplicar o clip.
  • Ver um simulador interativo para esta questão na Figura 2.11 (manipule os parâmetros \(\alpha\) e \(\beta\) para visualizar o efeito de realce e correção).

2.12.1.1 🧠 Transformação Linear de Intensidade

Ajustar o brilho e o contraste são operações fundamentais em PDI. Elas permitem corrigir imagens subexpostas (escuras demais) ou melhorar a visibilidade de detalhes sutis:

Parâmetro Função Efeito
\(\alpha\) (Alpha) Multiplicador Ajusta o Contraste. \(>1\) aumenta a diferença entre tons; \(<1\) diminui.
\(\beta\) (Beta) Aditivo Ajusta o Brilho. Valores positivos clareiam; negativos escurecem.
Clip Limitador Garante que o pixel permaneça dentro do padrão de 8 bits (\(0-255\)).

2.12.1.2 📋 Tarefa (especificação para VPL)

Entrada:

A primeira linha contém L.

A segunda linha contém C.

A terceira linha contém alpha (\(\alpha\)) e beta (\(\beta\)).

As linhas seguintes contêm os elementos da matriz.

Saída:

A matriz transformada com L linhas e C colunas.


2.12.1.3 📌 Exemplos

Entrada Saída Observação
1
4
1.5 -30
0 100 180 255
0 120 240 255 Note o efeito de saturação no último pixel

Executando os Testes

Para rodar os testes, execute TestSuite("EP01_01.extensão").run() numa nova célula, trocando a extensão pela da linguagem usada (.py, .java, .c, .cpp, .js ou .r). O sistema baixa os casos de teste do GitHub, executa o programa e calcula a nota automaticamente.

Exemplo de teste de sqrt em Python, com timeit isolando cada operação:

🎮 Simulador: Brilho e Contraste ☀️ p' = αp + β
1.0
0

Entrada Original

Resultado (p')

Fórmula: clip(1.0 * p + 0)
Figura 2.11: Simulador: Ajuste de Brilho e Contraste
%%writefile EP02_01.py
# Código Python
Writing EP02_01.py
TestSuite("EP02_01.py").run()
✔️ EP02_01.cases já existe em casos/
📋 8 caso(s) carregado(s) de casos/EP02_01.cases

🔍 Testando Python: EP02_01.py
⚠️ EP02_01.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.2 EP02_02 🔬 Subamostragem Espacial

Nesta atividade, você deve implementar a redução da resolução espacial de uma imagem através do processo de subamostragem.

  • Leia dois inteiros L e C, representando as dimensões da matriz original.
  • Leia um valor inteiro \(f\) (\(f \ge 1\)), que representa o fator de amostragem.
  • Leia os valores inteiros da matriz original.
  • A nova imagem deve ser construída selecionando o pixel da posição \((f \cdot i, f \cdot j)\) da imagem original.
  • Imprima a matriz resultante com as novas dimensões.
  • Ver na Figura 2.12 uma simulação deste EP.

📌 Importante:

  • Dimensões Finais: A imagem amostrada terá dimensões \(\lceil L/f \rceil \times \lceil C/f \rceil\). No contexto de programação, isso equivale ao tamanho resultante de um fatiamento (slicing) com passo \(f\).
  • Implementação: Não utilize funções prontas de bibliotecas de processamento de imagens (como OpenCV ou PIL) para o redimensionamento. Implemente a lógica de seleção de pixels manualmente ou via fatiamento de matrizes.
  • Aliasing: Note que este processo pode causar o efeito de aliasing (serrilhamento), onde detalhes finos são perdidos ou padrões indesejados aparecem.

2.12.2.1 🧠 Discretização do Espaço

A subamostragem reduz a resolução espacial de uma imagem, selecionando apenas um pixel a cada \(f\) pixels em cada direção. É o processo inverso da interpolação:

Parâmetro Função Efeito
Fator \(f\) Salto de amostragem Define o intervalo de seleção. Um fator \(2\) reduz a largura e altura pela metade.
Resolução Densidade de pixels Diminui a quantidade total de informação espacial da imagem.
Aliasing Efeito colateral Surgimento de padrões em escada ou blocos devido à perda de detalhes finos.

2.12.2.2 📋 Tarefa (especificação para VPL)

Entrada:

A primeira linha contém L.

A segunda linha contém C.

A terceira linha contém o fator f.

As linhas seguintes contêm os elementos da matriz \(L \times C\).

Saída:

A matriz reduzida com as dimensões correspondentes ao fatiamento por f.


2.12.2.3 📌 Exemplos

Entrada Saída Observação
2
4
2
10 20 30 40
50 60 70 80
10 30 O fator 2 seleciona os pixels (0,0) e (0,2) da primeira linha. A segunda linha é ignorada.
🎮 Simulador: Subamostragem 🔽 p' = p(⌊i/f⌋, ⌊j/f⌋)
1
f = 1 → resolução original (4×4)   |   f = 2 → metade (2×2)   |   f = 3 ou 4 → 1×1

Original (4×4)

Amostrada (tamanho variável)

Fator f = 1 → mantém todos os pixels (4×4)
Figura 2.12: Simulador: Subamostragem
%%writefile EP02_02.py
# Código Python
Overwriting EP02_02.py
TestSuite("EP02_02.py").run()
✔️ EP02_02.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP02_02.cases

🔍 Testando Python: EP02_02.py
⚠️ EP02_02.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.3 EP02_03 🎨 Quantização de Níveis de Cinza

Nesta atividade, você deve implementar a quantização uniforme de uma imagem, reduzindo a quantidade de níveis de intensidade de cinza originais para uma nova escala baseada em um número menor de bits.

  • Leia dois inteiros L e C, representando as dimensões da matriz.
  • Leia um inteiro \(k\) (\(1 \le k \le 8\)), representando o novo número de bits da imagem.
  • Calcule o número de níveis (\(N = 2^k\)) e o tamanho do intervalo (passo).
  • Para cada pixel \(p\), calcule o novo valor \(p'\) mapeando-o para o nível discretizado correspondente.
  • Imprima a matriz resultante com os mesmos valores de dimensões originais.
  • Ver na Figura 2.13 uma simulação deste EP.

📌 Importante:

  • Posterização: Ao reduzir drasticamente os níveis (ex: \(k=2\)), você notará que degradês suaves se transformam em faixas abruptas de cor.
  • Cálculo do Passo: O intervalo entre cada nível é definido por \(passo = 256 / 2^k\).
  • Mapeamento: Um método comum de quantização uniforme reconstrutiva é:

\[p' = \text{round}\left(\frac{p}{passo}\right) \times passo\]

  • Certifique-se de que o valor final esteja clipado no intervalo \([0, 255]\).

2.12.3.1 🧠 Discretização da Amplitude

Enquanto a subamostragem lida com a resolução espacial, a quantização foca na precisão da cor (amplitude). Reduzir os bits significa simplificar a informação cromática:

Parâmetro Função Efeito
Bits (\(k\)) Profundidade de cor Define quantos tons diferentes a imagem pode ter (\(2^k\)).
Passo Intervalo de tom Espaçamento entre os níveis de cinza permitidos.
Posterização Fenômeno visual Transformação de variações contínuas em blocos de cor sólida.

2.12.3.2 📋 Tarefa (especificação para VPL)

Entrada:

A primeira linha contém L.

A segunda linha contém C.

A terceira linha contém o número de bits k.

As linhas seguintes contêm os elementos da matriz \(L \times C\).

Saída:

A matriz transformada com os valores quantizados, mantendo o tamanho original \(L \times C\).


2.12.3.3 📌 Exemplos

Entrada Saída Observação
1
4
2
0 80 170 255
0 1 2 3 Com \(k=2\), temos \(2^2=4\) níveis: \(0,1,2,3\). O passo é \(256/4=64\). Cada pixel é dividido pelo passo: \(0→0\), \(80→1\), \(170→2\), \(255→3\).
1
5
1
10 50 120 200 250
0 0 0 1 1 Com \(k=1\), temos \(2^1=2\) níveis: \(0\) e \(1\). Passo \(=256/2=128\). Pixels \(< 128→0\), pixels \(\geq 128→1\): \(10→0\), \(50→0\), \(120→0\), \(200→1\), \(250→1\).
🎮 Simulador: Quantização (Profundidade de bits) 🎚️ q = round( p × (L‑1) / 255 )
8
Níveis de saída: 256  |  Valores exibidos: 0 a 255

Original (8 bits → 0 … 255)

Quantizada ( valores 0…255 )

Bits = 8 → 256 níveis (valores originais preservados)
Figura 2.13: Simulador: Quantização
%%writefile EP02_03.py
# Código Python
Writing EP02_03.py
TestSuite("EP02_03.py").run()
✔️ EP02_03.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP02_03.cases

🔍 Testando Python: EP02_03.py
⚠️ EP02_03.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.4 EP02_04 📐 Transformada de Distância em Imagem Binária

Dada uma imagem binária onde pixels de valor 1 representam o objeto e pixels 0 representam o fundo, a distância de um pixel de fundo recebe a menor distância ao pixel de objeto mais próximo. Pixels de objeto recebem distância 0. Para simplificar, considere que a imagem tem apenas um único objeto com um pixel de valor 1.

Problema: Leia uma imagem binária \(L \times C\) e uma métrica, e calcule essa distância simplificada aplicando uma das três fórmulas:

\[d_{\text{Euclidiana}} = \sqrt{(\Delta r)^2 + (\Delta c)^2}\]

\[d_{\text{City-block}} = |\Delta r| + |\Delta c|\]

\[d_{\text{Chessboard}} = \max(|\Delta r|,\; |\Delta c|)\]

onde \(\Delta r\) é a diferença de linhas e \(\Delta c\) a diferença de colunas entre dois pixels.


2.12.4.1 🖼️ Por que isso importa? - Aplicações da DT

A Transformada de Distância (DT) aparece em dezenas de pipelines de visão computacional:

Métrica Complexidade Aplicação típica
Euclidiana 🔴 \(O(n^2)\) ingênuo Esqueletização, matching de formas
City-block 🟡 \(O(n)\) com 2 passes Robótica, planejamento de caminho
Chessboard 🟢 \(O(n)\) com 2 passes Morfologia, dilatação/erosão

2.12.4.2 📌 Requisitos Técnicos

  • Entrada: * Primeira linha: \(L\) e \(C\) (inteiros).
    • Segunda linha: nome da métrica (euclidean, cityblock ou chessboard).
    • Em seguida, a matriz binária \(L \times C\) (valores 0 ou 1).
  • Pixels de objeto (1): distância \(= 0\) (ou \(0.00\) para euclidiana).
  • Pixels de fundo (0): distância ao único pixel de objeto na imagem.
  • Arredondamento (euclidiana): imprimir com 2 casas decimais (formato :.2f). City-block e Chessboard produzem inteiros — imprimir sem decimais.
  • Saída: valores separados por espaço, uma linha por linha da matriz.
  • Ver na Figura 2.14 uma simulação deste EP.

2.12.4.3 📌 Exemplos

Entrada Saída Observação
4 4
chessboard
0 0 0 0
0 0 0 0
0 0 1 0
0 0 0 0
2 2 2 2
2 1 1 1
2 1 0 1
2 1 1 1
A distância Chessboard é \(\max(\|dx\|, \|dy\|)\). O único pixel objeto é \((2,2)=0\); os demais armazenam sua distância mínima até ele.

2.12.4.4 📌 Observações finais

  • Como a imagem tem apenas um objeto de um pixel, a distância de cada pixel de fundo é simplesmente a distância desse pixel ao único ponto objeto.
  • A implementação pode usar força bruta (percorrer todos os pixels da imagem e calcular a distância diretamente), pois \(L\) e \(C\) são pequenos nos casos de teste.
  • Este problema é um aquecimento para a Transformada de Distância geral, que será trabalhada em capítulos posteriores com múltiplos objetos e algoritmos otimizados.
🎮 Simulador Interativo — Transformada de Distância Clique na grade para alternar pixels
Métrica:

Imagem Binária (clique para editar)

Transformada de Distância

Grade 5×5 · Clique em qualquer célula da imagem para adicionar/remover objeto
Figura 2.14: Simulador: Ajuste de Brilho e Contraste
%%writefile EP02_04.py
# Código Python
Writing EP02_04.py
TestSuite("EP02_04.py").run()
✔️ EP02_04.cases já existe em casos/
📋 5 caso(s) carregado(s) de casos/EP02_04.cases

🔍 Testando Python: EP02_04.py
⚠️ EP02_04.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.5 EP02_05 ➡️ Translação de Imagem

Nesta atividade, você deve implementar o deslocamento espacial de uma imagem. A translação move cada pixel da imagem original para uma nova posição com base em um vetor de deslocamento.

  • Leia dois inteiros L e C, representando as dimensões da matriz.
  • Leia dois inteiros \(t_x\) (deslocamento horizontal) e \(t_y\) (deslocamento vertical).
  • Leia os valores inteiros da matriz original.
  • Calcule a nova posição \((x', y')\) para cada pixel \((x, y)\) original.
  • Imprima a matriz resultante com as mesmas dimensões da original.
  • Ver na Figura 2.15 uma simulação deste EP.

📌 Importante:

  • Preenchimento: Pixels que “entram” na imagem devido ao deslocamento e não possuem correspondente na original devem ser preenchidos com 0 (preto).
  • Descarte: Pixels que, após a translação, ficarem fora dos limites da matriz (\(0 \dots L-1\) ou \(0 \dots C-1\)) devem ser ignorados.
  • Coordenadas: Considere \(x\) como o índice da linha e \(y\) como o índice da coluna.

2.12.5.1 🧠 Deslocamento Espacial

Transladar uma imagem significa mover todos os seus pontos por uma distância fixa em direções especificadas. Matematicamente, usando coordenadas homogêneas, a operação é descrita como:

\[\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\]

Que resulta nas equações simples:

  • \(x' = x + t_x\)
  • \(y' = y + t_y\)

2.12.5.2 📋 Tarefa (especificação para VPL)

Entrada:

A primeira linha contém L.

A segunda linha contém C.

A terceira linha contém os inteiros tx e ty.

As linhas seguintes contêm os elementos da matriz \(L \times C\).

Saída:

A matriz resultante com as mesmas dimensões \(L \times C\) após o deslocamento.


2.12.5.3 📌 Exemplos

Entrada Saída Observação
2
2
1 1
10 20
30 40
0 0
0 10
Deslocamento \((t_x=1, t_y=1)\): cada pixel move uma posição para baixo e para a direita. Só o pixel \((0,0)=10\) cabe no destino \((1,1)\); os demais saem da imagem. As posições vazias são preenchidas com \(0\).
3
3
-1 0
1 2 3
4 5 6
7 8 9
0 1 2
0 4 5
0 7 8
Deslocamento \((t_x=-1, t_y=0)\): cada pixel move uma posição para a esquerda. A primeira coluna fica vazia (\(0\)) e a última coluna original é descartada.
🎮 Simulador: Translação (tx, ty) 📍 p'(i,j) = p(i−ty, j−tx)
0
0

Original (4×4)

Transladação (tx, ty)

tx = 0, ty = 0 → nenhum deslocamento (imagem original)
Figura 2.15: Simulador: Translação (tx, ty)
%%writefile EP02_05.py
# Código Python
Writing EP02_05.py
TestSuite("EP02_05.py").run()
✔️ EP02_05.cases já existe em casos/
📋 7 caso(s) carregado(s) de casos/EP02_05.cases

🔍 Testando Python: EP02_05.py
⚠️ EP02_05.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.6 EP02_06 🔄 Rotação de Imagem

Nesta atividade, você deve implementar a rotação de uma imagem em torno do seu centro geométrico. Esta operação requer o mapeamento de coordenadas e o uso de técnicas de interpolação para determinar os novos valores dos pixels.

  • Leia dois inteiros L e C, representando as dimensões da matriz.
  • Leia um valor real \(\theta\) (ângulo em graus) e uma string representando o método de interpolação (nearest ou bilinear).
  • Leia os valores inteiros da matriz original.
  • Realize a rotação em torno do centro da imagem \((L/2, C/2)\).
  • Imprima a matriz resultante com as mesmas dimensões da original.
  • Ver na Figura 2.16 uma simulação deste EP.

📌 Importante:

  • Mapeamento Inverso: Para evitar “buracos” na imagem final, percorra cada pixel \((x', y')\) da imagem de destino e calcule sua posição correspondente \((x, y)\) na imagem original usando a matriz de rotação inversa.

  • Interpolação:

  • nearest: Atribui o valor do pixel mais próximo da coordenada calculada.

  • bilinear: Calcula uma média ponderada baseada nos 4 vizinhos mais próximos.

  • Bordas: Pixels cuja origem \((x, y)\) caia fora dos limites da imagem original devem ser preenchidos com 0.


2.12.6.1 🧠 Transformação por Ângulo

A rotação de um ponto \((x, y)\) em relação à origem por um ângulo \(\theta\) é dada pela matriz de transformação. Para rotacionar em torno de um centro \((x_c, y_c)\), primeiro transladamos o centro para a origem, rotacionamos e transladamos de volta:

\[\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta & x_c \\ \sin\theta & \cos\theta & y_c \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x-x_c \\ y-y_c \\ 1 \end{bmatrix}\]

Dica: Use o mapeamento inverso para garantir que todos os pixels da imagem de saída sejam preenchidos corretamente.


2.12.6.2 📋 Tarefa (especificação para VPL)

Entrada:

A primeira linha contém L.

A segunda linha contém C.

A terceira linha contém o ângulo theta (em graus) e o método interp (nearest ou bilinear).

As linhas seguintes contêm os elementos da matriz \(L \times C\).

Saída:

A matriz rotacionada com L linhas e C colunas.

2.12.6.3 📌 Exemplos

Entrada Saída Observação
2
2
90 nearest
1 2
3 4
3 1
4 2
Rotação de 90° horário: a coluna 0 vira a linha 0 (de baixo para cima). \((0,0)=1→(1,0)\), \((1,0)=3→(0,0)\), \((0,1)=2→(1,1)\), \((1,1)=4→(0,1)\).
3
3
45 bilinear
0 0 0
0 255 0
0 0 0
0 180 0
180 255 180
0 180 0
Rotação de 45°: o pixel central permanece \(255\); os vizinhos diretos recebem valor interpolado \(\approx 180\) por bilinear; os cantos permanecem \(0\).
🎮 Simulador: Rotação de Imagem (θ) 🔄 (x',y') = (x·cθ - y·sθ, x·sθ + y·cθ)

● Quadrado azul com marcador laranja (canto superior direito) – a rotação é em torno do centro

θ = 0° → cos = 1.000, sin = 0.000 → matriz identidade
Figura 2.16: Simulador: Rotação (ângulo θ)
%%writefile EP02_06.py
# Código Python
Writing EP02_06.py
TestSuite("EP02_06.py").run()
✔️ EP02_06.cases já existe em casos/
📋 7 caso(s) carregado(s) de casos/EP02_06.cases

🔍 Testando Python: EP02_06.py
⚠️ EP02_06.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.7 EP02_07 🔍 Redimensionamento (Escala)

Nesta atividade, você deve implementar o redimensionamento de uma imagem utilizando fatores de escala. Diferente da subamostragem simples, aqui utilizaremos técnicas de interpolação para permitir tanto a ampliação quanto a redução da imagem.

  • Leia dois inteiros L e C, representando as dimensões da matriz original.
  • Leia dois valores reais \(s_x\) (escala nas linhas) e \(s_y\) (escala nas colunas).
  • Leia uma string representando o método de interpolação (nearest ou bilinear).
  • Leia os valores inteiros da matriz original.
  • Calcule as novas dimensões: \(L' = \text{round}(L \times s_x)\) e \(C' = \text{round}(C \times s_y)\).
  • Imprima a matriz resultante com as novas dimensões.
  • Ver na Figura 2.17 uma simulação deste EP.

📌 Importante:

  • Mapeamento Inverso: Para cada pixel \((x', y')\) da imagem de destino, encontre a posição correspondente na origem usando \((x, y) = (x'/s_x, y'/s_y)\).

  • Interpolação:

  • nearest: Seleciona o valor do pixel mais próximo (arredondamento das coordenadas).

  • bilinear: Realiza uma interpolação linear dupla entre os quatro pixels vizinhos mais próximos na imagem original.

  • Bordas: Certifique-se de que o mapeamento não tente acessar índices fora do intervalo \([0, L-1]\) e \([0, C-1]\).


2.12.7.1 🧠 Interpolação para Ampliação/Redução

Redimensionar uma imagem por fatores \((s_x, s_y)\) exige o preenchimento de vazios (na ampliação) ou a fusão de informações (na redução). O método de interpolação define a qualidade visual do resultado:

Método Funcionamento Efeito Visual
Nearest Pega o valor do vizinho mais próximo. Rápido, mas gera efeito “pixelado” ou blocos.
Bilinear Média ponderada dos 4 vizinhos (\(2 \times 2\)). Suaviza a imagem, reduzindo o serrilhamento.

2.12.7.2 📋 Tarefa (especificação para VPL)

Entrada:

A primeira linha contém L.

A segunda linha contém C.

A terceira linha contém os fatores sx e sy.

A quarta linha contém o método interp (nearest ou bilinear).

As linhas seguintes contêm os elementos da matriz \(L \times C\).

Saída:

A matriz redimensionada com dimensões \(L' \times C'\).


2.12.7.3 📌 Exemplos

Entrada Saída Observação
2
2
2.0 2.0
nearest
1 2
3 4
1 1 2 2
1 1 2 2
3 3 4 4
3 3 4 4
Ampliação 2×: cada pixel original é replicado em um bloco 2×2. A imagem \(2\times2\) vira \(4\times4\).
2
2
0.5 0.5
nearest
10 20
30 40
10 Redução 0.5×: a imagem \(2\times2\) vira \(1\times1\). Com nearest, o único pixel de saída amostra a posição \((0,0)=10\).
🎮 Simulador: Redimensionamento (fator sx = sy) 🔍 Nearest: réplica | Bilinear: média ponderada
1.0
Fator = 1 → tamanho original (3×3)  |  Fator = 2 → 6×6  |  Fator = 4 → 12×12

Original (3×3)

🔲 Nearest Neighbor (réplica)

🌀 Bilinear (interpolação)

Fator = 1.0 → tamanho 3×3 (sem alteração)
Figura 2.17: Simulador: Redimensionamento (Nearest vs Bilinear)
%%writefile EP02_07.py
# Código Python
Writing EP02_07.py
TestSuite("EP02_07.py").run()
✔️ EP02_07.cases já existe em casos/
📋 7 caso(s) carregado(s) de casos/EP02_07.cases

🔍 Testando Python: EP02_07.py
⚠️ EP02_07.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.8 EP02_08 🔀 Cisalhamento (Shear)

Nesta atividade, você deve implementar a transformação de cisalhamento em uma imagem. O cisalhamento é uma transformação afim que desloca cada ponto em uma direção fixada, por um valor proporcional à sua distância de uma reta paralela a essa direção, resultando em um efeito de inclinação.

  • Leia dois inteiros L e C, representando as dimensões da matriz.
  • Leia dois valores reais \(sh_x\) (cisalhamento horizontal) e \(sh_y\) (cisalhamento vertical).
  • Leia uma string representando o método de interpolação (nearest ou bilinear).
  • Leia os valores inteiros da matriz original.
  • Aplique a transformação mantendo o tamanho original da imagem (cortando o que ultrapassar os limites).
  • Imprima a matriz resultante com as dimensões \(L \times C\).
  • Ver na Figura 2.18 uma simulação deste EP.

📌 Importante:

  • Mapeamento Inverso: Para cada pixel \((x', y')\) da imagem de destino, calcule a posição correspondente na origem \((x, y)\) utilizando a matriz de cisalhamento inversa.
  • Preenchimento: Coordenadas que resultarem em posições fora da matriz original devem ser preenchidas com 0.
  • Coordenadas: Para fins desta implementação, considere \(x\) como o índice da linha e \(y\) como o índice da coluna.

2.12.8.1 🧠 Distorção Afim

O cisalhamento altera a geometria da imagem inclinando seus eixos. A relação entre as coordenadas originais \((x, y)\) e as transformadas \((x', y')\) é dada por:

\[\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & sh_x & 0 \\ sh_y & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\]

Isso resulta nas equações:

  • \(x' = x + sh_x \cdot y\)
  • \(y' = y + sh_y \cdot x\)

2.12.8.2 📋 Tarefa (especificação para VPL)

Entrada:

A primeira linha contém L.

A segunda linha contém C.

A terceira linha contém os fatores shx e shy.

A quarta linha contém o método interp (nearest ou bilinear).

As linhas seguintes contêm os elementos da matriz \(L \times C\).

Saída:

A matriz transformada com as mesmas dimensões \(L \times C\).


2.12.8.3 📌 Exemplos

Entrada Saída Observação
3
3
0.5 0.0
nearest
10 20 30
40 50 60
70 80 90
10 20 30
0 40 50
0 0 70
Cisalhamento horizontal: linha \(i\) desloca \(\lfloor i \cdot 0.5 \rfloor\) pixels. Linha \(0→0\)px, linha \(1→0\)px, linha \(2→1\)px. Os pixels deslocados para fora são descartados e as posições vazias preenchidas com \(0\).
2
2
0.0 1.0
nearest
10 20
30 40
10 0
30 20
Cisalhamento vertical: coluna \(j\) desloca \(\lfloor j \cdot 1.0 \rfloor\) pixels para baixo. Coluna \(0→0\)px (inalterada), coluna \(1→1\)px: \(20\) desce para \((1,1)\) e \((0,1)\) fica \(0\).
🎮 Simulador: Cisalhamento (Shear) ✂️ (x', y') = (x + shx·y, y + shy·x)
0.0
0.0

Original (4×4)

Cisalhada (Nearest)

shx = 0.0, shy = 0.0 → sem deformação.
Figura 2.18: Simulador: Cisalhamento (shx, shy)
%%writefile EP02_08.py
# Código Python
Writing EP02_08.py
TestSuite("EP02_08.py").run()
✔️ EP02_08.cases já existe em casos/
📋 8 caso(s) carregado(s) de casos/EP02_08.cases

🔍 Testando Python: EP02_08.py
⚠️ EP02_08.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.9 EP02_09 🧩 Transformação Afim Genérica

Nesta atividade, você deve implementar uma transformação afim arbitrária em uma imagem. Esta operação é a generalização de todas as transformações lineares (escala, rotação, cisalhamento) combinadas com a translação, permitindo manipulações geométricas complexas através de uma única matriz.

  • Leia dois inteiros L e C, representando as dimensões da matriz.
  • Leia seis valores reais (\(a, b, t_x, c, d, t_y\)) que compõem a matriz de transformação afim \(2 \times 3\).
  • Leia uma string representando o método de interpolação (nearest ou bilinear).
  • Leia os valores inteiros da matriz original.
  • Aplique a transformação mantendo o tamanho original \(L \times C\).
  • Imprima a matriz resultante.
  • Ver na Figura 2.19 uma simulação deste EP.

📌 Importante:

  • Mapeamento Inverso: Para calcular o valor de cada pixel na imagem de destino, você deve utilizar a inversa da matriz de transformação afim fornecida para encontrar a coordenada correspondente na imagem original.
  • Preenchimento: Coordenadas calculadas que caiam fora dos limites \([0, L-1]\) e \([0, C-1]\) da imagem original devem resultar em um pixel de valor 0.
  • Flexibilidade: Esta implementação deve ser capaz de realizar qualquer uma das tarefas anteriores (translação, rotação, etc.) bastando alterar os parâmetros da matriz.

2.12.9.1 🧠 Combinação de Operações

A transformação afim preserva pontos, retas e planos. No processamento de imagens, ela mapeia a posição \((x, y)\) para \((x', y')\) seguindo o sistema:

\[\begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} a & b \\ c & d \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} + \begin{bmatrix} t_x \\ t_y \end{bmatrix}\]

Ou, de forma compacta em coordenadas homogêneas:

\[\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} a & b & t_x \\ c & d & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\]


2.12.9.2 📋 Tarefa (especificação para VPL)

Entrada:

A primeira linha contém L.

A segunda linha contém C.

A terceira linha contém seis floats: a b tx c d ty.

A quarta linha contém o método interp (nearest ou bilinear).

As linhas seguintes contêm os elementos da matriz \(L \times C\).

Saída:

A matriz transformada com as dimensões originais \(L \times C\).


2.12.9.3 📌 Exemplos

Entrada Saída Observação
2
2
1.0 0.0 0.5 0.0 1.0 0.5
bilinear
10 20
30 40
15 20
25 30
Translação fracionária \((t_x=0.5, t_y=0.5)\): cada pixel de saída \((i,j)\) amostra a posição \((i+0.5,\, j+0.5)\) da entrada via bilinear. Ex: \((0,0)\) interpola os quatro vizinhos \(→15\).
3
3
2.0 0.0 0.0 0.0 2.0 0.0
nearest
1 2 3
4 5 6
7 8 9
1 1 2
1 1 2
4 4 5
Escala \(2\times\) via matriz afim \((a=2, d=2)\): cada pixel de saída \((i,j)\) amostra a posição \((2i, 2j)\) da entrada com nearest. Ex: \((0,2)→(0,4)\) fora da imagem \(→\) nearest clipa para \((0,2)=3\)… aguarda confirmação da lógica de borda.
🎮 Simulador: Transformação Afim 2D 📐 [x'] = [a b tx]·[x y 1]ᵀ
Matriz afim 2×3
a b tx
c d ty

● Seta laranja (ponta triangular) + corpo retangular preto. A transformação afim é aplicada à figura inteira.

Matriz = [[1.0, 0.0, 0], [0.0, 1.0, 0]] → transformação identidade.
Figura 2.19: Simulador: Transformação Afim (matriz 2×3)
%%writefile EP02_09.py
# Código Python
Writing EP02_09.py
TestSuite("EP02_09.py").run()
✔️ EP02_09.cases já existe em casos/
📋 7 caso(s) carregado(s) de casos/EP02_09.cases

🔍 Testando Python: EP02_09.py
⚠️ EP02_09.py: Arquivo sem conteúdo (menos de 3 linhas). Testes ignorados.

2.12.10 EP02_10 🎯 Correção de Perspectiva (Homografia)

Nesta atividade, você deve implementar a transformação de perspectiva, também conhecida como homografia. Diferente das transformações afins, a perspectiva não preserva o paralelismo, permitindo “retificar” objetos inclinados, como documentos ou placas capturados em ângulos oblíquos.

  • Leia dois inteiros L e C, representando as dimensões da matriz original.
  • Leia quatro pares de coordenadas \((x, y)\) representando os cantos do quadrilátero de origem (objeto distorcido).
  • Leia quatro pares de coordenadas \((x, y)\) representando os cantos do quadrilátero de destino (onde o objeto deve ser mapeado).
  • Leia os valores da matriz original.
  • Calcule a matriz de homografia \(3 \times 3\) e aplique a transformação.
  • Imprima a matriz resultante com as dimensões de saída especificadas.
  • Ver na Figura 2.20 uma simulação deste EP.

📌 Importante:

  • Graus de Liberdade: A homografia possui 8 graus de liberdade (o nono elemento da matriz \(3 \times 3\) é uma constante de normalização, geralmente 1), exigindo no mínimo 4 pontos correspondentes para ser calculada.
  • Projeção: Após multiplicar as coordenadas pela matriz, é necessário dividir os resultados \(x'\) e \(y'\) pela componente homogênea \(w\) para retornar ao plano 2D.
  • Uso de Bibliotecas: Para esta tarefa, você pode utilizar as funções cv2.getPerspectiveTransform para obter a matriz e cv2.warpPerspective para aplicar a transformação, ou implementar o sistema linear e o mapeamento inverso manualmente para um desafio extra.

2.12.10.1 🧠 Deformação não-afim

Enquanto transformações afins mapeiam paralelogramos em paralelogramos, a homografia mapeia qualquer quadrilátero em outro quadrilátero. Isso é essencial para visão computacional:

Operação Característica Aplicação Típica
Homografia Projeção em plano Correção de documentos, escaneamento de placas.
Ponto de Fuga Convergência de linhas Reconstrução 3D a partir de imagens 2D.
Warping Deformação de malha Estabilização de vídeo e panoramas (stitching).

2.12.10.2 📌 Exemplos

Entrada Saída Observação
60 65
390 52
28 408
415 413
0 0
300 0
0 300
300 300
Matriz \(300 \times 300\) As 4 primeiras linhas são os pontos de origem; as 4 seguintes são os destinos. Transforma o trapézio irregular em um quadrado perfeito, corrigindo a perspectiva da câmera.
🎮 Simulador: Correção de Perspectiva 📐 p' = H·p (homografia 3×3)

Como usar: Arraste os 4 pontos vermelhos (cantos do quadrilátero distorcido). O interior mostra uma textura quadriculada. Clique em Corrigir perspectiva para transformar o quadrilátero em um retângulo 300×300 (simulação visual).

Arraste os cantos → o quadrilátero se deforma. A correção mapeia os 4 pontos para um retângulo 300×300 usando homografia.
Figura 2.20: Simulador: Correção de Perspectiva (Homografia)
%%writefile EP02_10.py
# Código Python
Writing EP02_10.py

TestSuite(“EP02_10.py”).run()


2.12.11 EP02_11 🎯 Correção de Perspectiva em Imagem Real (Sudoku)

Nesta atividade, o objetivo é aplicar a transformação de perspectiva (homografia) em uma fotografia real. Diferente de exemplos sintéticos, você lidará com uma imagem de jornal inclinada, onde a grade do Sudoku não está perfeitamente retangular devido ao ângulo da captura.

  • Baixe a imagem através da URL fornecida (utilizando o cabeçalho adequado para evitar erros de acesso).
  • Aplique uma expansão (padding) na imagem original com cv2.copyMakeBorder para garantir que os vértices do objeto não sejam cortados durante o processamento.
  • Identifique os quatro pontos de canto \((x, y)\) da grade do Sudoku na imagem expandida (TL, TR, BL, BR).
  • Defina os quatro pontos de destino correspondentes para mapear o tabuleiro em uma vista frontal quadrada (ex: \(500 \times 500\)).
  • Utilize cv2.getPerspectiveTransform para calcular a matriz de homografia e cv2.warpPerspective para retificar a imagem.
  • Exiba o resultado da imagem corrigida lado a lado com a original.
  • Ver na Figura 2.22 uma simulação deste EP.

📌 Importante:

  • Ponto de Transdução: Lembre-se que a formação desta imagem começou com a transdução da luz refletida pelo papel em sinais eletrônicos no sensor da câmera.
  • Mapeamento Inverso: Funções como warpPerspective utilizam o mapeamento inverso com interpolação (geralmente bilinear) para evitar buracos na imagem de saída, calculando qual pixel da origem corresponde a cada posição do destino.
  • Créditos: A imagem utilizada é “Sudoku en periódico” de Héctor Rodríguez, sob licença CC BY 2.0. A fonte original e dados de GPS estão disponíveis no material de apoio.

2.12.11.1 🧠 Contexto do Problema

A homografia é uma ferramenta poderosa na visão computacional para remover distorções de perspectiva. Ao contrário de transformações afins, ela permite que linhas que convergem para pontos de fuga sejam tornadas paralelas novamente:

Operação Característica Aplicação Típica
Homografia Projeção entre planos Retificação de documentos, leitura de QR Codes.
Padding Expansão de bordas Proteção de gradientes em vértices periféricos.
Warping Reamostragem espacial Correção de lentes e montagem de panoramas.

2.12.11.2 📌 Exemplos

Entrada (pts1 — origem) Saída Observação
120 95
480 75
100 510
500 495
Imagem \(500 \times 500\) px Os pontos de origem marcam os cantos externos da grade na imagem expandida. A homografia produz uma visão frontal e sem distorção do tabuleiro, facilitando a extração de dígitos por algoritmos de OCR.

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

O processamento inicia-se com a leitura da imagem original e a extração de seus metadados EXIF, que revelam a geolocalização precisa da captura, conforme detalhado na Figura 2.21. Após a conversão para tons de cinza e o redimensionamento para uma matriz de \(500 \times 500\) pixels, os dados são preparados para a retificação geométrica. Este processo de correção de perspectiva, essencial para eliminar deformações causadas pelo ângulo da câmera, pode ser explorado de forma interativa através da Figura 2.22, onde a aplicação da matriz de homografia permite obter uma visão frontal e regular da grade do Sudoku.

Dados de Aquisição:
{'Make': 'Panasonic', 'Model': 'DMC-FX12', 'DateTime': '2009:04:14 23:10:05'}

⚠️ GPS não veio no EXIF local.
🌐 Consultando Wikimedia Commons API...
✅ GPS recuperado via API (extmetadata).

GPS Decimal : 38.090876, -0.654695
Google Maps : https://www.google.com/maps?q=38.090876,-0.654695

Tipo PIL: <class 'PIL.JpegImagePlugin.JpegImageFile'>
Dimensões PIL (x,y): (3072, 2304)

Tipo NumPy: <class 'numpy.ndarray'>
Dimensões NumPy [y,x,c]: (2304, 3072, 3)

Pixel Pillow (0,0): (28, 25, 8)
Pixel NumPy  [0,0]: [28 25  8]

✅ Arquivo salvo: sudoku.png
Figura 2.21: 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).
🎮 Simulador: Perspectiva do Sudoku 📐 Homografia 3×3 · CC BY 2.0

📷 Original (expandida) — arraste os cantos laranjas

Foto: Héctor Rodríguez · CC BY 2.0

✅ Corrigida (400×400) — frontal

Carregando imagem sudoku.png...
Figura 2.22: Simulador: correção de perspectiva do Sudoku.
%%writefile EP02_11.py
# Código Python
Writing EP02_11.py
TestSuite("EP02_11.py").run()
✔️ EP02_11.cases já existe em casos/
📋 7 caso(s) carregado(s) de casos/EP02_11.cases

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