8Compreendendo Cenas: Correspondência de Características, Detecção e Segmentação
No Capítulo 7, foi estudado como representar imagens por meio de descritores e utilizar essas representações para tarefas de classificação. Neste capítulo, o problema é ampliado: além de reconhecer padrões, torna-se necessário estabelecer correspondências entre diferentes imagens, localizar automaticamente objetos de interesse e interpretar a organização espacial de uma cena.
Esses problemas constituem algumas das principais tarefas da Visão Computacional e representam uma etapa natural após a classificação de imagens. Para resolvê-los, serão apresentados métodos clássicos de correspondência de características, detecção de objetos e segmentação de imagens, bem como uma visão geral das abordagens modernas baseadas em Deep Learning, preparando a transição para o Capítulo 9.
8.1 Objetivos do Capítulo
Ao concluir este capítulo, o estudante deverá ser capaz de:
Estabelecer correspondências entre imagens utilizando detectores e descritores locais, estimando transformações geométricas por meio de homografias para o registro e a correção de perspectiva;
Localizar objetos de interesse em imagens utilizando métodos clássicos de detecção e avaliar os resultados por meio de métricas como a Intersection over Union (IoU) e da técnica de Non-Maximum Suppression (NMS);
Diferenciar os paradigmas de segmentação — semântica, de instâncias e panóptica — compreendendo que a segmentação panóptica unifica a segmentação semântica e a segmentação de instâncias, proporcionando uma descrição mais completa da cena, e aplicar métodos clássicos de segmentação;
Extrair descritores geométricos e topológicos de objetos segmentados e exportá-los como anotações estruturadas (CSV ou formato YOLO), validando a qualidade das anotações por meio da métrica IoU;
Relacionar os métodos clássicos de correspondência, detecção e segmentação com as abordagens modernas baseadas em Deep Learning, estudadas no Capítulo 9.
A Figura 8.1 apresenta uma visão geral dos principais conceitos e das relações entre os tópicos abordados neste capítulo, servindo como um mapa conceitual para orientar a leitura.
Figura 8.1: Visão geral dos principais conceitos abordados neste capítulo, incluindo correspondência de características, detecção de objetos, segmentação de imagens e suas relações com abordagens modernas baseadas em Deep Learning.Fonte: elaborado com auxílio do Gemini Notebook (Google, 2025).
8.3 Detectores e Descritores Locais de Características
No capítulo anterior, cada imagem foi representada por um único vetor de características utilizado para classificação. Em diversas aplicações, entretanto, é necessário comparar apenas partes da imagem, estabelecendo correspondências entre regiões observadas em diferentes instantes, posições ou pontos de vista. Essa tarefa requer uma representação local da imagem, capaz de identificar estruturas suficientemente distintas para serem reencontradas em outras imagens.
Esse processo é realizado em duas etapas complementares. Inicialmente, um detector identifica pontos de interesse (keypoints), normalmente associados a cantos ou regiões com variações significativas de intensidade. Em seguida, um descritor representa numericamente a vizinhança de cada ponto detectado, permitindo comparar regiões correspondentes entre diferentes imagens.
Uma vez obtidos os pares (ponto, descritor), a correspondência (matching) consiste em encontrar, para cada descritor de uma imagem, o descritor mais semelhante na outra. Essas correspondências constituem a base de diversas aplicações, como registro de imagens, reconstrução tridimensional, navegação visual e realidade aumentada.
8.3.1 O Algoritmo ORB (Oriented FAST and Rotated BRIEF)
Neste capítulo será utilizado o ORB, um detector e descritor local que combina eficiência computacional e robustez a rotações. O algoritmo reúne três componentes principais:
FAST (Features from Accelerated Segment Test), responsável pela detecção dos pontos-chave;
BRIEF (Binary Robust Independent Elementary Features), responsável pela construção do descritor binário;
um mecanismo de estimativa da orientação da vizinhança, que torna o descritor aproximadamente invariante à rotação.
O detector FAST percorre todos os pixels da imagem. Para cada pixel candidato, analisa um círculo de 16 pixels ao seu redor. Se um conjunto de pixels consecutivos apresentar intensidade significativamente maior ou menor que a intensidade do pixel central, esse pixel é considerado um ponto-chave (keypoint). Em seguida, candidatos muito próximos são filtrados, preservando apenas os mais representativos.
Após a detecção dos pontos-chave, o descritor BRIEF é calculado em uma vizinhança ao redor de cada ponto-chave, e não apenas sobre os 16 pixels utilizados pelo FAST. Nessa região, um padrão de amostragem, formado por um conjunto fixo de pares de pontos \((x,y)\) distribuídos em uma janela ao redor do ponto-chave, é utilizado para realizar comparações de intensidade segundo a Equação 8.1. No ORB, a orientação predominante da vizinhança é estimada a partir da distribuição das intensidades nessa região, e o padrão de amostragem é rotacionado de acordo com essa orientação. Além disso, o ORB utiliza uma versão otimizada do BRIEF, denominada rBRIEF (Rotated BRIEF), na qual os pares de pontos são selecionados para produzir descritores mais discriminativos e com baixa correlação entre seus bits.
\(x\) e \(y\) são dois pontos da vizinhança de \(p\), escolhidos pelo padrão de amostragem do BRIEF;
\(I(\cdot)\) representa a intensidade de um pixel;
\(\tau(p;x,y)\) é o resultado da comparação binária entre os pontos \(x\) e \(y\).
Cada comparação gera um bit do descritor. A concatenação de todas essas comparações forma o descritor binário associado ao ponto-chave.
Como o descritor é binário, a similaridade entre dois pontos é medida pela distância de Hamming, correspondente ao número de bits diferentes entre dois descritores. Essa métrica pode ser calculada de forma muito eficiente por operações lógicas sobre os bits, tornando o ORB adequado para aplicações em tempo real.
8.3.2 Explorando o Simulador do ORB
A Figura 8.3 ilustra, de forma interativa, como o descritor ORB é construído. Cada segmento representa um dos 32 pares de pontos\((x,y)\) utilizados na Equação 8.1. A cor do segmento indica o resultado da comparação de intensidades: verde quando \(I(x)<I(y)\) (bit igual a 1) e vermelho caso contrário (bit igual a 0). A seta amarela representa a orientação predominante da vizinhança, estimada a partir do centróide de intensidade. O padrão de amostragem do BRIEF é rotacionado de acordo com essa orientação, tornando o descritor aproximadamente invariante à rotação.
Inicialmente, deixe o ruído em zero e compare a sequência de bits com o slider em 0° e depois em 15° (anote o valor do campo “Assinatura do Descritor Binário” em cada caso):
Nesse exemplo, ambos os descritores são idênticos, e o painel “Dist. Hamming” confirma distância zero — evidência de que a estimativa de orientação está compensando corretamente a rotação da imagem.
Agora clique em “Adicionar Ruído” e repita o experimento. Como o ruído é gerado aleatoriamente a cada execução, os valores abaixo são apenas um exemplo — os seus serão diferentes, mas devem apresentar uma distância de Hamming de magnitude semelhante (tipicamente entre 6 e 14 bits, de um total de 32):
O ruído altera parte das comparações de intensidade, modificando alguns bits do descritor. A diferença entre dois descritores é medida pela distância de Hamming, correspondente ao número de posições em que os bits diferem. Para os descritores acima, essa distância é igual a 10.
Uma forma eficiente de calcular essa distância em Python consiste em aplicar a operação XOR (^), que identifica os bits diferentes, seguida do método bit_count(), que contabiliza quantos bits iguais a 1 existem no resultado.
def hamming(a: int, b: int) ->int:return (a ^ b).bit_count()a =0b01000000001000110110011111101010b =0b10110000001011100100111111100010hamming(a, b)
10
A Figura 8.2 compara essa implementação com uma versão baseada na comparação de caracteres e com a implementação otimizada do OpenCV (cv2.NORM_HAMMING).
import random, timeit, cv2, numpy as np, pandas as pdimport matplotlib.pyplot as pltBITS, N =256, 100_000A = [''.join(random.choice('01') for _ inrange(BITS)) for _ inrange(N)]B = [''.join(random.choice('01') for _ inrange(BITS)) for _ inrange(N)]Ai, Bi =map(lambda L: [int(x,2) for x in L], (A,B))Acv = np.array([[int(s[i:i+8],2) for i inrange(0,BITS,8)] for s in A], np.uint8)Bcv = np.array([[int(s[i:i+8],2) for i inrange(0,BITS,8)] for s in B], np.uint8)H = [ ("Strings", lambda: sum(sum(x!=y for x,y inzip(a,b)) for a,b inzip(A,B))), ("XOR+bit_count()", lambda: sum((a^b).bit_count() for a,b inzip(Ai,Bi))), ("OpenCV", lambda: sum(cv2.norm(a,b,cv2.NORM_HAMMING) for a,b inzip(Acv,Bcv)))]df = pd.DataFrame( [(n, timeit.timeit(f, number=1)) for n,f in H], columns=["Método","Tempo (s)"])df["Speedup"] = (df.iloc[0,1]/df["Tempo (s)"]).round(1)print(df)plt.figure(figsize=(6,3))plt.bar(df["Método"], df["Tempo (s)"])plt.ylabel("Tempo (s)")plt.show()
Método Tempo (s) Speedup
0 Strings 1.353989 1.0
1 XOR+bit_count() 0.011943 113.4
2 OpenCV 0.184508 7.3
Figura 8.2: Comparação do desempenho de diferentes implementações da distância de Hamming.
🎯 Simulador Interativo: Alinhamento de Orientação e BRIEF (ORB)Invariância de Orientação em Tempo Real
Ângulo (θ)
0°
Dist. Hamming
0
Pares Ativos
32
Assinatura do Descritor Binário Gerada (BRIEF 32-bits):
00000000000000000000000000000000
Teste = 1 (I(A) < I(B))
Teste = 0 (I(A) ≥ I(B))
A linha amarela indica o vetor centroide da orientação estimada.
Figura 8.3: Simulador interativo do descritor ORB: explore a lógica de rotação e construção do descritor binário. Altere a rotação para observar como o padrão de amostragem de testes binários do BRIEF (linhas verdes e vermelhas) se orienta dinamicamente para garantir a invariância angular.
8.4 Correspondência de Características e Homografia com ORB
Para ilustrar o processo de correspondência de características, utiliza-se uma cena sintética obtida pela rotação de 20° da imagem original. Essa transformação simula uma segunda captura da mesma cena sob outro ponto de vista. A Figura 8.4 apresenta a imagem de referência e sua versão rotacionada, que serão utilizadas nas etapas seguintes.
Figura 8.4: Imagem original gerada pelo Gemini e versão rotacionada (20°), simulando uma mudança de ponto de vista.
8.4.1 Detectando e Correspondendo Características com ORB
Com as duas imagens disponíveis, o ORB detecta os pontos-chave e calcula seus descritores binários. Em seguida, o BFMatcher da biblioteca cv2 estabelece as correspondências entre os descritores utilizando a distância de Hamming e a verificação mútua (cross-check). Por fim, as correspondências são ordenadas da menor para a maior distância de Hamming, priorizando os pares potencialmente mais confiáveis. No código a seguir, que gera a Figura 8.5, destacam-se os seguintes passos:
cv2.ORB_create(nfeatures=500) instancia o detector ORB, limitando a busca aos 500 pontos-chave mais representativos de cada imagem. Essa restrição reduz o custo computacional e evita a seleção de pontos pouco distintivos.
orb.detectAndCompute(...) executa, em uma única chamada, a detecção dos pontos-chave pelo FAST e o cálculo dos descritores pelo BRIEF orientado, retornando a lista de pontos-chave (kp) e seus descritores binários de 256 bits (des).
cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) cria um comparador por força bruta (Brute-Force Matcher), que utiliza a distância de Hamming — a mesma métrica explorada na Figura 8.3 — para comparar cada descritor da imagem original com todos os descritores da imagem rotacionada. O parâmetro crossCheck=True preserva apenas os pares em que a melhor correspondência é recíproca, isto é, quando o melhor correspondente de A é B e, simultaneamente, o melhor correspondente de B é A. Esse critério elimina grande parte das correspondências ambíguas.
matches = sorted(...) ordena as correspondências da menor para a maior distância de Hamming. Quanto menor essa distância, maior a similaridade entre os descritores e, consequentemente, maior a probabilidade de a correspondência estar correta.
A Figura 8.5 apresenta apenas as cinco correspondências com menor distância de Hamming. Embora esses pares sejam os mais promissores, ainda não existe qualquer restrição geométrica entre os pontos correspondentes. Como consequência, algumas ligações podem representar falsas correspondências (false matches), justificando o uso do RANSAC na etapa seguinte para identificar apenas as correspondências geometricamente consistentes.
orb = cv2.ORB_create(nfeatures=500)kp1, des1 = orb.detectAndCompute(img_original, None)kp2, des2 = orb.detectAndCompute(img_cena, None)print(f"Pontos de interesse detectados: {len(kp1)} (original), {len(kp2)} (cena)")bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)matches =sorted( bf.match(des1, des2), key=lambda m: m.distance)print(f"Correspondências encontradas: {len(matches)}")def draw_matches_destacado(img1, kp1, img2, kp2, matches, espessura=2, raio_ponto=4, seed=42, cor_fixa=None):"""Desenha as imagens lado a lado com linhas conectando os pontos correspondentes. Se cor_fixa=None, cada correspondência recebe uma cor aleatória (facilita distinguir ligações individuais). Se cor_fixa for definida (ex: verde), todas as linhas usam a mesma cor — útil para destacar um subconjunto específico, como os inliers do RANSAC. """ h1, w1 = img1.shape[:2] h2, w2 = img2.shape[:2] h =max(h1, h2) canvas = np.zeros((h, w1 + w2, 3), dtype=np.uint8) canvas[:h1, :w1] = cv2.cvtColor(img1, cv2.COLOR_GRAY2BGR) if img1.ndim ==2else img1 canvas[:h2, w1:w1+w2] = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR) if img2.ndim ==2else img2 rng = np.random.RandomState(seed) # seed fixa = cores reproduzíveis a cada execuçãofor m in matches: pt1 =tuple(np.round(kp1[m.queryIdx].pt).astype(int)) pt2 =tuple(np.round(kp2[m.trainIdx].pt).astype(int) + np.array([w1, 0])) cor = cor_fixa if cor_fixa isnotNoneelse\tuple(int(c) for c in rng.randint(60, 256, size=3)) cv2.line(canvas, pt1, pt2, cor, espessura, lineType=cv2.LINE_AA) cv2.circle(canvas, pt1, raio_ponto, cor, -1, lineType=cv2.LINE_AA) cv2.circle(canvas, pt2, raio_ponto, cor, -1, lineType=cv2.LINE_AA)return canvas# experimente as 5 piores: matches[-5:]img_matches = draw_matches_destacado( img_original, kp1, img_cena, kp2, matches[:5], espessura=10, raio_ponto=15)mm.show([img_matches], titles=["Top 5 Correspondências ORB"], cols=1, figsize=(10, 5))
Pontos de interesse detectados: 500 (original), 500 (cena)
Correspondências encontradas: 300
Figura 8.5: As 5 melhores correspondências de características ORB entre a imagem original e a cena sintética, antes da filtragem por RANSAC.
A função draw_matches_destacado() possui apenas finalidade de visualização: ela posiciona as imagens lado a lado e desenha linhas entre os pares correspondentes, não interferindo na estimação das correspondências.
8.5 Modelagem Matemática: Homografia e RANSAC
8.5.1 Homografia
Nos Exercícios 10 e 11 do Capítulo 2, as funções cv2.getPerspectiveTransform e cv2.warpPerspective foram utilizadas para corrigir a perspectiva de imagens a partir de quatro pares de pontos correspondentes informados manualmente. Neste capítulo, essas correspondências passam a ser obtidas automaticamente pelo ORB, tornando possível estimar a transformação entre duas imagens sem intervenção do usuário.
Matematicamente, essa transformação é descrita por uma homografia, representada por uma matriz \(3\times3\) que relaciona as coordenadas de um mesmo plano observado sob diferentes pontos de vista:
Após a normalização das coordenadas homogêneas, obtém-se o ponto correspondente
\[
\left(\frac{x'}{w'},\frac{y'}{w'}\right).
\]
Como a homografia é definida a menos de um fator de escala, ela possui oito graus de liberdade. Consequentemente, são necessários, no mínimo, quatro pares de pontos correspondentes para estimar seus parâmetros.
Na prática, porém, as correspondências produzidas automaticamente pelo ORB podem conter associações incorretas (outliers). Para estimar a homografia de forma confiável mesmo na presença desses erros, utiliza-se o algoritmo RANSAC, apresentado na próxima seção.
8.5.2 RANSAC
O RANSAC (Random Sample Consensus) é um algoritmo de estimação robusta capaz de ajustar um modelo geométrico mesmo na presença de observações incorretas (outliers). Neste capítulo, o modelo de interesse é uma homografia, estimada a partir das correspondências produzidas pelo ORB.
Em cada iteração, o algoritmo:
seleciona aleatoriamente um pequeno subconjunto de correspondências (quatro pares de pontos, no caso da homografia);
estima uma homografia candidata a partir desse subconjunto;
verifica quais correspondências são compatíveis com essa transformação, classificando-as como inliers ou outliers;
registra a homografia que produz o maior número de inliers;
reestima a homografia utilizando apenas os inliers encontrados.
Embora o exemplo deste capítulo utilize uma homografia, o RANSAC é um algoritmo de propósito geral e pode ser empregado para estimar diversos modelos geométricos, como retas, circunferências, planos e outras transformações. Em todos os casos, o princípio é o mesmo: gerar modelos candidatos a partir de pequenas amostras aleatórias e selecionar aquele que apresenta o maior consenso entre os dados.
Para compreender esse processo de forma gradual, são apresentados dois simuladores.
O primeiro, mostrado na Figura 8.6, utiliza o exemplo mais simples possível: o ajuste de uma reta a um conjunto de pontos contendo aproximadamente 25% de outliers. O objetivo é compreender as etapas fundamentais do algoritmo — amostrar, estimar um modelo, identificar os inliers e repetir o processo — sem a complexidade do registro entre imagens.
🎯 Simulador: RANSAC — Ajuste de Reta Robusto a OutliersDados com ~25% de correspondências espúrias
Limiar (px)
15
Inliers
–
Outliers
–
Iterações
–
Pontos (ainda não classificados)
Inliers
Outliers
Figura 8.6: Simulador interativo do algoritmo RANSAC: ajuste o limiar de distância e execute o algoritmo para observar a separação entre inliers e outliers.
O segundo simulado, apresentado na Figura 8.7, aproxima-se do problema estudado neste capítulo. Em vez de um único conjunto de pontos, são consideradas duas imagens contendo correspondências entre pontos-chave. Algumas correspondências estão corretas (inliers), enquanto outras são incorretas (outliers), resultantes de erros no processo de correspondência dos descritores. Nesse simulador, o modelo estimado é uma transformação de similaridade (rotação, escala e translação), mais simples que uma homografia completa, mas suficiente para ilustrar o problema de registro entre imagens.
Em ambos os simuladores, o algoritmo executado segue exatamente o mesmo princípio utilizado posteriormente para estimar a homografia. A única diferença está no modelo geométrico ajustado.
Ajuste o limiar de distância e execute o algoritmo em cada simulador para observar como o RANSAC identifica os inliers, descarta os outliers e estima um modelo consistente utilizando apenas as correspondências válidas.
🧩 Simulador: RANSAC — Registro por Correspondência de Pontos~30% de correspondências espúrias (falsos matches)
Limiar (px)
12
Inliers
–
Outliers
–
Iterações
–
Pontos-chave
Correspondência (não classificada)
Inlier
Outlier
Figura 8.7: Simulador interativo do RANSAC aplicado ao registro de imagens: pontos-chave de duas imagens são casados por um descritor, algumas correspondências são espúrias (outliers), e o RANSAC estima a transformação de similaridade que alinha a maioria delas.
8.5.3 Estimando a Homografia com RANSAC
Após obter as correspondências entre os pontos-chave pelo ORB, o próximo passo consiste em estimar a homografia entre as duas imagens. Para isso, utiliza-se a função cv2.findHomography(), que emprega o algoritmo RANSAC para calcular essa transformação e retornar uma máscara indicando quais correspondências foram classificadas como inliers.
O código a seguir realiza quatro operações principais:
extrai as coordenadas dos pontos correspondentes em cada imagem;
estima a homografia por meio de cv2.findHomography(..., cv2.RANSAC);
recebe a máscara produzida pelo RANSAC, na qual cada correspondência é classificada como inlier ou outlier;
utiliza essa máscara para selecionar apenas as correspondências classificadas como inliers.
A Figura 8.8 apresenta apenas as correspondências classificadas como inliers. Observa-se que esses pares de pontos são compatíveis com uma mesma transformação geométrica, enquanto as correspondências inconsistentes (outliers) são descartadas. Como consequência, a homografia estimada representa de forma mais fiel a relação geométrica entre as duas imagens.
pts1 = np.float32([kp1[m.queryIdx].pt for m in matches])pts2 = np.float32([kp2[m.trainIdx].pt for m in matches])H, mascara_inliers = cv2.findHomography(pts1, pts2, cv2.RANSAC, ransacReprojThreshold=5.0)n_inliers =int(mascara_inliers.sum())print(f"Matriz de homografia estimada:\n{H}\n")print(f"Inliers: {n_inliers} de {len(matches)} correspondências \ ({100*n_inliers/len(matches):.1f}%)")matches_inliers = [m for m, ok inzip(matches, mascara_inliers.ravel()) if ok]img_inliers = draw_matches_destacado( img_original, kp1, img_cena, kp2, matches_inliers, espessura=2, raio_ponto=4, cor_fixa=(0, 200, 0) # verde (BGR))mm.show([img_inliers], titles=[f"Correspondências Inliers (RANSAC) — \{n_inliers}/{len(matches)}"], cols=1, figsize=(10, 5))
Matriz de homografia estimada:
[[ 9.39407240e-01 3.41706397e-01 -1.77390658e+02]
[-3.41929137e-01 9.39717641e-01 5.27534018e+02]
[-1.12175998e-07 -4.86089683e-08 1.00000000e+00]]
Inliers: 285 de 300 correspondências (95.0%)
Figura 8.8: Correspondências classificadas como inliers (verde) pelo RANSAC ao estimar a homografia entre as duas imagens.
8.5.4 Registrando a Imagem
Após estimar a homografia, o passo seguinte consiste em utilizá-la para registrar a imagem da cena no sistema de coordenadas da imagem original. Esse processo permite alinhar as duas imagens, facilitando a comparação entre elas.
O código realiza três operações principais:
aplica a transformação projetiva por meio de cv2.warpPerspective(), utilizando a opção cv2.WARP_INVERSE_MAP, que aplica internamente a transformação inversa sem a necessidade de calcular explicitamente \(H^{-1}\);
calcula a diferença absoluta pixel a pixel entre a imagem registrada e a imagem original por meio de cv2.absdiff();
exibe a imagem original, a cena rotacionada, a imagem registrada e um mapa das diferenças entre as duas imagens.
A Figura 8.9 apresenta o resultado do registro. Observa-se que a imagem registrada torna-se visualmente muito próxima da imagem original, indicando que a homografia estimada conseguiu alinhar corretamente as duas vistas da mesma cena. O mapa de diferenças evidencia apenas as regiões em que ainda existem pequenas discrepâncias decorrentes de erros de interpolação, quantização e da própria estimação da homografia.
h, w = img_original.shape[:2]# WARP_INVERSE_MAP: aplica H "de trás para frente", dispensando o cálculo manual de H^-1img_registrada = cv2.warpPerspective(img_cena, H, (w, h), flags=cv2.WARP_INVERSE_MAP)erro = cv2.absdiff(img_original, img_registrada)mm.show( # mostra mm.gray(erro)>10 em níveis de cinza [img_original, img_cena, img_registrada, mm.gray(erro)>10], titles=["Original", "Cena (rotacionada)", "Registrada", "Diferença"], cols=2, figsize=(14, 8),)
Figura 8.9: Registro da imagem da cena utilizando a homografia inversa estimada pelo RANSAC.
Nota🧠 Por que funciona? — Robustez por consenso
Muitos métodos de estimação ajustam um modelo utilizando todas as observações disponíveis, procurando minimizar o erro total entre os dados observados e o modelo ajustado (abordagem conhecida como mínimos quadrados). Quando existem outliers, essas observações incorretas podem deslocar significativamente o resultado obtido.
O RANSAC segue uma estratégia diferente. Em vez de utilizar todos os dados simultaneamente, ele estima sucessivos modelos a partir de pequenas amostras aleatórias. Cada modelo é então avaliado pelo número de correspondências compatíveis com ele. Ao final das iterações, é selecionado o modelo que apresenta o maior consenso entre os dados, isto é, o maior número de inliers.
Dois parâmetros exercem papel fundamental no algoritmo:
o limiar de reprojeção, que define a distância máxima para que uma correspondência seja classificada como inlier;
o número de iterações, que deve ser suficientemente grande para aumentar a probabilidade de selecionar pelo menos uma amostra livre de outliers.
Embora bastante robusto, o RANSAC pressupõe que exista um modelo geométrico predominante nos dados. Seu desempenho tende a diminuir quando a proporção de inliers é muito pequena ou quando diferentes estruturas geométricas coexistem na mesma cena, dificultando a identificação de um único modelo dominante.
8.6 Detecção de Objetos: Haar Cascade (Viola-Jones)
A correspondência de características responde à pergunta: “onde está o mesmo objeto ou padrão observado anteriormente?”. A detecção de objetos resolve um problema mais geral: localizar automaticamente instâncias de uma categoria (por exemplo, faces humanas), mesmo que os objetos específicos nunca tenham sido observados durante o treinamento. Enquanto o ORB necessita de duas imagens para estabelecer correspondências entre pontos, o Haar Cascade opera sobre uma única imagem, identificando diretamente as regiões candidatas a conter o objeto procurado.
O algoritmo Haar Cascade, proposto por Viola e Jones (Viola; Jones, 2001; Viola; Jones, 2004), combina características Haar, imagens integrais e uma cascata de classificadores para realizar a detecção de objetos de forma eficiente. Embora atualmente existam métodos mais recentes baseados em redes neurais convolucionais, o Haar Cascade permanece disponível na biblioteca OpenCV e constitui um exemplo clássico para o estudo de técnicas de detecção de objetos.
Seu funcionamento baseia-se em três componentes principais:
Características Haar: filtros retangulares simples que medem diferenças de intensidade entre regiões vizinhas da imagem, explorando padrões de contraste característicos do objeto, como a região dos olhos geralmente mais escura que a testa;
Imagem integral: estrutura de dados que permite calcular rapidamente a soma dos pixels de qualquer região retangular da imagem: \[
I_{\text{integral}}(x,y)=\sum_{x'\le x,\;y'\le y}I(x',y'),
\] reduzindo significativamente o custo computacional da avaliação das características Haar. Com essa estrutura, a soma dos pixels de qualquer retângulo pode ser obtida com apenas quatro acessos à imagem integral;
Cascata de classificadores: durante o treinamento, o algoritmo AdaBoost seleciona e combina classificadores simples em uma sequência de estágios. Na detecção, regiões que claramente não correspondem ao objeto são descartadas logo nos primeiros estágios, enquanto apenas as candidatas mais promissoras passam pelas etapas seguintes, mais precisas e computacionalmente mais caras. Essa estratégia permite realizar uma busca eficiente em diferentes posições e escalas da imagem.
O código a seguir, que gera a Figura 8.10, utiliza classificadores treinados previamente e disponibilizados pelo OpenCV para detectar faces e, em seguida, restringe a busca por olhos apenas ao interior de cada face detectada. Essa estratégia reduz falsos positivos e diminui o custo computacional, pois evita realizar a busca por olhos em toda a imagem.
Durante a detecção, uma janela percorre a imagem em diferentes posições e escalas. A função detectMultiScale() realiza essa busca automaticamente. O parâmetro scaleFactor controla o fator de redução entre escalas consecutivas da janela, enquanto minNeighbors define o número mínimo de detecções vizinhas necessárias para confirmar um objeto, reduzindo detecções espúrias. O parâmetro minSize estabelece o menor tamanho de objeto considerado durante a busca.
A função retorna uma lista de retângulos, cada um descrito pelas coordenadas do canto superior esquerdo e pelas dimensões (x, y, largura, altura). Esses retângulos delimitam as regiões classificadas como objetos pelo detector e são utilizados para desenhar as caixas mostradas na Figura 8.10.
def get_cascade(nome):"""Baixa (se necessário) e carrega um classificador Haar Cascade do OpenCV.""" caminho =f"haarcascades/{nome}" os.makedirs("haarcascades", exist_ok=True)ifnot os.path.exists(caminho): url =f"https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/{nome}" urllib.request.urlretrieve(url, caminho)return cv2.CascadeClassifier(caminho)# Mais utilizado#face_cascade = get_cascade("haarcascade_frontalface_default.xml")# Mais preciso, porém mais lentoface_cascade = get_cascade("haarcascade_frontalface_alt2.xml")# Compromisso entre velocidade e precisão#face_cascade = get_cascade("haarcascade_frontalface_alt.xml")# Muito rápido, porém menos preciso#face_cascade = get_cascade("haarcascade_frontalface_alt_tree.xml")# para os olhos, mais utilizado#eye_cascade = get_cascade("haarcascade_eye.xml")eye_cascade = get_cascade("haarcascade_eye_tree_eyeglasses.xml")img_original = mm.read(caminho_local)# Escala de cinza + equalização de histograma (Capítulo 3), como o Haar Cascade esperaimg_rgb = mm.rotate(img_original, angle=0)img_gray = cv2.equalizeHist(cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY))# Detecta faces e, dentro de cada uma, tenta detectar os olhosfaces = face_cascade.detectMultiScale( img_gray, scaleFactor=1.1, # Passos maiores entre escalas: mais rápido, porém menos sensível minNeighbors=3, # Nº mínimo de detecções sobrepostas para confirmar uma face minSize=(90, 90) # Ignora regiões candidatas menores que 30×30 px)img_anotada = img_rgb.copy()for (x, y, w, h) in faces: cv2.rectangle(img_anotada, (x, y), (x + w, y + h), (0, 255, 0), 3) olhos = eye_cascade.detectMultiScale( img_gray[y:y+h, x:x+w], scaleFactor=1.02, minNeighbors=4, minSize=(15, 15) )for (ex, ey, ew, eh) in olhos: cv2.rectangle(img_anotada, (x+ex, y+ey), (x+ex+ew, y+ey+eh), (255, 0, 0), 4)print(f"Regiões detectadas como face: {len(faces)}")mm.show([img_anotada], titles= ["Detecção Haar Cascade: faces (verde) e olhos (vermelho)"], cols=1, figsize=(8, 8))
Regiões detectadas como face: 5
Figura 8.10: Detecção de faces e olhos com Haar Cascade.
Nota🧠 Por que funciona? — E por que também falha
Na imagem utilizada neste exemplo, o classificador detecta a face e os olhos, mas também pode marcar uma segunda região sobre parte do fundo da imagem como se fosse uma face. Esse é um exemplo de falso positivo: a distribuição local de intensidades nessa região é suficientemente semelhante aos padrões aprendidos durante o treinamento para que a cascata a classifique incorretamente como um rosto.
Esse comportamento evidencia uma das principais limitações do Haar Cascade. Como o método baseia sua decisão apenas em características Haar, isto é, diferenças de intensidade entre regiões retangulares, ele não representa explicitamente a forma ou o significado dos objetos presentes na imagem. Assim, texturas e padrões de contraste semelhantes aos encontrados em faces podem produzir detecções incorretas. Além disso, seu desempenho tende a diminuir diante de grandes variações de pose, oclusões, expressões faciais e condições de iluminação diferentes daquelas presentes nos dados utilizados para o treinamento.
Apesar dessas limitações, o Haar Cascade permanece útil em aplicações que priorizam baixo custo computacional. Em situações que exigem maior capacidade de generalização diante de variações na aparência dos objetos, métodos modernos baseados em redes neurais profundas costumam apresentar melhor desempenho.
8.7 Detecção de Objetos: Caixas Delimitadoras, IoU e NMS
Embora os métodos de detecção de objetos utilizem estratégias bastante diferentes — do Haar Cascade aos detectores modernos baseados em redes neurais profundas —, seus resultados são normalmente representados por caixas delimitadoras (bounding boxes), definidas pelas coordenadas \((x_{min}, y_{min}, x_{max}, y_{max})\).
8.7.1Intersection over Union (IoU)
A métrica Intersection over Union (IoU) quantifica a sobreposição entre duas caixas delimitadoras, por exemplo, a detecção produzida por um algoritmo e a anotação de referência (ground truth):
Quanto mais próximo de 1, maior a concordância entre as caixas; o valor 0 indica ausência de sobreposição. Em avaliações de detectores de objetos, é comum considerar uma detecção correta quando \(\mathrm{IoU}\ge0{,}5\), embora aplicações específicas possam adotar limiares diferentes.
A métrica IoU não é utilizada apenas para avaliar detectores. Ela também constitui o critério empregado pelo algoritmo de Supressão de Não-Máximos para decidir quando duas caixas delimitadoras representam o mesmo objeto e, portanto, uma delas deve ser eliminada.
8.7.2 Supressão de Não-Máximos (NMS)
Durante a detecção, é comum que várias caixas delimitadoras sejam associadas ao mesmo objeto. A Supressão de Não-Máximos (Non-Maximum Suppression, NMS) elimina essa redundância em três etapas:
ordena as detecções pela pontuação de confiança, da maior para a menor;
mantém a caixa de maior confiança e elimina aquelas cujo IoU com ela excede um determinado limiar;
repete o processo com as caixas restantes até que não existam sobreposições relevantes.
O exemplo da Figura 8.11 ilustra esse procedimento com dois objetos, cada um inicialmente representado por várias caixas sobrepostas. Neste exemplo, cada caixa delimitadora recebe uma pontuação de confiança, que representa o grau de confiança do detector de que aquela região contém o objeto procurado.
Em vez de implementar o algoritmo manualmente, utiliza-se a função cv2.dnn.NMSBoxes, que implementa a Supressão de Não-Máximos empregada em diversos detectores modernos. A função desenhar_caixas exibe, lado a lado, as caixas antes e depois do NMS, evidenciando a redução de cinco detecções para apenas duas, preservando apenas a caixa de maior confiança para cada objeto.
def desenhar_caixas(caixas, pontuacoes, indices, cor, tamanho=(450, 450)):"""Desenha as caixas (e suas pontuações) indicadas em `indices` sobre um fundo branco.""" tela = np.full((tamanho[1], tamanho[0], 3), 255, dtype=np.uint8)for i in indices: x0, y0, x1, y1 = caixas[i] cv2.rectangle(tela, (x0, y0), (x1, y1), cor, 2) cv2.putText(tela, f"{pontuacoes[i]:.2f}", (x0, y0 -8), cv2.FONT_HERSHEY_SIMPLEX, 0.5, cor, 1, cv2.LINE_AA)return telacaixas = np.array([ [50, 50, 150, 150], [60, 55, 155, 145], [58, 60, 160, 150], [300, 300, 400, 420], [310, 305, 395, 415],])pontuacoes = np.array([0.90, 0.75, 0.60, 0.95, 0.70])# cv2.dnn.NMSBoxes espera caixas no formato (x, y, largura, altura)caixas_xywh = np.column_stack([ caixas[:, 0], caixas[:, 1], caixas[:, 2] - caixas[:, 0], caixas[:, 3] - caixas[:, 1]])mantidas = cv2.dnn.NMSBoxes( bboxes=caixas_xywh.tolist(), scores=pontuacoes.tolist(), score_threshold=0.0, # nenhum corte por confiança mínima aqui nms_threshold=0.5# limiar de IoU para considerar duas caixas redundantes).flatten()img_antes = desenhar_caixas(caixas, pontuacoes, range(len(caixas)), cor=(220, 38, 38))img_depois = desenhar_caixas(caixas, pontuacoes, mantidas, cor=(22, 163, 74))mm.show( [img_antes, img_depois], titles=[f"Antes do NMS ({len(caixas)} caixas)", \f"Depois do NMS ({len(mantidas)} caixa(s))"], cols=2, figsize=(9, 4.5))
Figura 8.11: Efeito da Supressão de Não-Máximos (NMS): múltiplas detecções redundantes (esquerda) são reduzidas a uma detecção por objeto (direita).
Nota🧠 Por que funciona? — Eliminação de Redundâncias
O NMS (Non-Maximum Suppression) não altera a qualidade das detecções produzidas pelo detector. Sua função é eliminar caixas delimitadoras redundantes que representam o mesmo objeto, preservando apenas aquela com maior pontuação de confiança.
Sua eficácia depende do limiar de IoU adotado. Um valor muito baixo pode eliminar caixas correspondentes a objetos diferentes cuja sobreposição seja elevada, enquanto um valor muito alto pode manter várias caixas sobrepostas para um mesmo objeto.
O NMS também não elimina falsos positivos isolados. Se uma região incorreta for detectada por apenas uma caixa, ela será preservada, pois não existe outra detecção redundante para compará-la. Por esse motivo, o NMS é aplicado como uma etapa de pós-processamento, utilizando apenas as caixas delimitadoras e suas pontuações de confiança produzidas pelo detector.
8.8 Segmentação Visual: Semântica, de Instâncias e Panóptica
Uma caixa delimitadora indica aproximadamente a posição de um objeto, mas não identifica quais pixels lhe pertencem. A segmentação visual resolve esse problema atribuindo um rótulo a cada pixel da imagem. Conforme a informação produzida, distinguem-se três paradigmas principais, resumidos na Tabela 8.1.
Tabela 8.1: Comparação entre os principais paradigmas de segmentação visual.
Paradigma
Pergunta que responde
Distingue objetos da mesma classe?
Semântica
“A que classe pertence cada pixel?”
Não. Todos os pixels de uma mesma classe recebem o mesmo rótulo.
De instâncias
“Quais pixels pertencem a cada objeto individual?”
Sim. Cada objeto recebe um identificador próprio.
Panóptica
Combina as duas anteriores.
Sim. Cada pixel recebe uma classe e, quando aplicável, um identificador de instância.
Na segmentação semântica, cada pixel recebe apenas o rótulo de sua classe. Na segmentação de instâncias, além da classe, objetos distintos pertencentes à mesma categoria são diferenciados entre si. Já a segmentação panóptica combina essas duas informações, atribuindo a cada pixel uma classe e, quando aplicável, um identificador de instância.
8.8.1 Segmentação Semântica por Limiarização
Antes da popularização dos métodos baseados em redes neurais profundas — tema do Capítulo 9 —, muitas aplicações de segmentação eram resolvidas por técnicas clássicas de processamento de imagens. Um dos exemplos mais simples consiste em separar objeto e fundo por limiarização, produzindo uma máscara binária. Essa máscara caracteriza uma segmentação semântica, pois cada pixel passa a pertencer a uma das duas classes: moeda ou fundo.
A Figura 8.12 utiliza a imagem de moedas da biblioteca scikit-image. Esse exemplo retoma os conceitos de limiarização e morfologia matemática estudados no Capítulo 4, em substituição ao exemplo anterior, que utilizava outra imagem de moedas.
Inicialmente, aplica-se um fechamento morfológico para suavizar pequenas imperfeições na superfície das moedas. Em seguida, a limiarização de Otsu produz a máscara binária que separa moedas e fundo. Após a remoção dos objetos conectados à borda da imagem, o preenchimento de regiões internas e a eliminação de pequenos ruídos por operações morfológicas, obtém-se uma máscara semântica adequada para as etapas seguintes.
8.8.2 Segmentação de Instâncias por Rotulação
Uma máscara semântica informa apenas a classe de cada pixel, mas não distingue objetos diferentes pertencentes à mesma categoria. Para obter uma segmentação de instâncias, aplica-se a rotulação de componentes conectados, que atribui um identificador diferente a cada região conexa da máscara binária.
Neste exemplo, cada componente conectado corresponde a uma moeda individual. Assim, a rotulação produz uma segmentação de instâncias, permitindo identificar, contar e medir separadamente cada moeda presente na imagem.
img_moedas = skdata.coins()# 1. Fechamento morfológico: suaviza bordas e preenche pequenas falhas na# superfície das moedas antes da limiarizaçãofechamento = mm.close(img_moedas, mm.sedisk(4))# 2. Limiarização de Otsu (Capítulo 4): separa moedas (claras) do fundo (escuro)mascara_bruta = mm.threshold(fechamento)# 3. Remove objetos conectados à borda da imagem — moedas cortadas nas# extremidades não formam instâncias completas e atrapalhariam a contagemmascara_sem_borda = mm.edgeoff(mascara_bruta)# 4. Fechamento de buracos: preenche eventuais regiões internas não detectadas# pelo threshold, garantindo que cada moeda seja um disco sólidomascara_semantica = mm.clohole(mascara_sem_borda)# 5. Abertura morfológica: remove ruído residual e desconecta moedas que# porventura se tocam, preparando a máscara para a Rotulaçãomascara_limpa = mm.open(mascara_semantica, mm.sedisk(6))# 6. Rotulação de componentes conectados: cada moeda isolada recebe um# identificador de instância distintorotulos_instancias = mm.label(mascara_limpa)print(f"Instâncias (moedas individuais) identificadas: {np.max(rotulos_instancias)}")mm.show( [img_moedas, mascara_bruta, mascara_sem_borda, mascara_semantica, mascara_limpa, rotulos_instancias], titles=["Original","1. Limiarização (Otsu)","2. Remoção de bordas","3. Preenchimento de buracos\n(máscara semântica: moeda x fundo)","4. Abertura (limpeza)","5. Rotulação\n(segmentação de instâncias)" ], cols=3, figsize=(10, 6))
Instâncias (moedas individuais) identificadas: 24
Figura 8.12: Segmentação clássica (não baseada em aprendizado profundo): máscara semântica (moeda x fundo) via limiarização de Otsu, e segmentação de instâncias via rotulação de componentes conectados.
Nota🧠 Por que funciona? — E onde a abordagem clássica encontra limitações
Neste exemplo, a segmentação é facilitada pelo contraste entre as moedas e o fundo e pela relativa homogeneidade de intensidade no interior de cada moeda. A limiarização de Otsu separa eficientemente essas duas regiões, enquanto as operações morfológicas removem pequenas imperfeições da máscara binária. Por fim, a rotulação de componentes conectados atribui um identificador distinto a cada região conexa, produzindo uma segmentação de instâncias.
Essa estratégia, entretanto, depende diretamente da qualidade da máscara binária. Se dois objetos estiverem unidos, sobrepostos ou apresentarem contraste insuficiente em relação ao fundo, eles poderão ser representados por uma única região ou deixar de ser segmentados corretamente. Além disso, métodos baseados predominantemente na intensidade dos pixels possuem capacidade limitada para distinguir objetos de classes diferentes com aparência semelhante.
Métodos modernos de segmentação baseados em redes neurais profundas aprendem representações visuais diretamente a partir dos dados de treinamento, o que geralmente lhes confere maior capacidade de lidar com variações de iluminação, textura, forma e oclusão. A segmentação panóptica amplia essa abordagem ao combinar, em uma única representação, a classificação semântica de todos os pixels e a identificação individual dos objetos presentes na cena.
8.8.3 Alternativa: Transformada de Distância + Watershed
A abertura morfológica pode separar moedas que se tocam por meio da erosão da máscara binária. Entretanto, essa operação modifica o contorno de todos os objetos, inclusive daqueles que já estavam isolados. Uma alternativa, introduzida no Capítulo 4, consiste em combinar a transformada de distância com o algoritmo watershed, utilizando marcadores obtidos a partir da própria máscara binária.
O procedimento é realizado em três etapas:
calcula-se a transformada de distância da máscara binária, atribuindo a cada pixel do objeto sua distância até o fundo mais próximo;
aplica-se um limiar à transformada de distância para obter marcadores localizados nas regiões centrais das moedas;
utiliza-se o algoritmo watershed para expandir esses marcadores até as fronteiras entre os objetos, separando moedas que se encontram em contato.
A Figura 8.13 apresenta essas etapas. Os marcadores são obtidos nas regiões centrais da transformada de distância e utilizados como sementes para o watershed, que propaga cada rótulo até encontrar as fronteiras entre objetos vizinhos.
Comparando com a abertura morfológica, ambas as abordagens conseguem separar moedas em contato. Entretanto, como o watershed utiliza marcadores para dividir regiões conectadas, sem aplicar erosão diretamente à máscara, os contornos originais tendem a ser mais bem preservados, favorecendo a obtenção de medidas geométricas, como área, perímetro e circularidade.
# Reaproveita a máscara semântica ANTES da abertura agressiva (sem erosão do contorno)mascara_base = mascara_semantica# 1. Transformada de distância: cada pixel da máscara recebe a distância até o fundo próximodistancia = mm.dist(mascara_base)# Marcadores obtidos por limiarização da transformada de distância.# As regiões centrais das moedas permanecem conectadas e são utilizadas# como sementes para o algoritmo watershed.marcadores = mm.label( np.uint8(distancia >0.5* distancia.max()))# 3. Watershed: propaga cada marcador dentro da máscara até os pontos de contato entre moedasrotulos_watershed = mm.watershed(marcadores, mascara_base)print(f"Abertura morfológica (mascara_limpa): {np.max(rotulos_instancias)} instâncias")print(f"Distância + watershed: {np.max(rotulos_watershed)} instâncias")mm.show( [mascara_base, distancia, mm.dil(marcadores,mm.sedisk(5)), rotulos_watershed], titles=["Máscara semântica\n(sem abertura, sem erosão)","Transformada de distância","Marcadores\n(máximos regionais)","Watershed\n(instâncias separadas)" ], cols=4, figsize=(11, 3.2))
Figura 8.13: Separação de moedas que se tocam via transformada de distância + watershed, sem erosão do contorno das moedas.
Nota🧠 Por que funciona? — Marcadores e Watershed
A transformada de distância atribui valores maiores aos pixels mais afastados do fundo, que normalmente se encontram nas regiões mais centrais dos objetos. Ao aplicar um limiar a essa transformada, obtêm-se marcadores localizados no interior de cada moeda, favorecendo a obtenção de um marcador para cada objeto.
O algoritmo watershed utiliza esses marcadores como sementes e propaga seus rótulos até que duas regiões em crescimento se encontrem. Os pontos de encontro entre essas regiões definem as fronteiras entre objetos adjacentes.
O desempenho desse método depende da qualidade dos marcadores. Objetos muito alongados, com formas irregulares ou contendo múltiplos máximos na transformada de distância podem gerar marcadores adicionais, resultando em uma segmentação excessiva (oversegmentation). Em aplicações mais complexas, métodos modernos de segmentação de instâncias baseados em redes neurais profundas aprendem diretamente, a partir dos dados de treinamento, representações adequadas para separar os objetos, dispensando a construção explícita de marcadores e outras heurísticas geométricas.
8.8.4 Medição e Extração de Atributos com mm.measure
Após a segmentação e a rotulação dos objetos, o próximo passo consiste em medir suas propriedades geométricas. Para isso, a biblioteca morph disponibiliza a função mm.measure, que recebe uma imagem binária e retorna, para cada objeto, um conjunto de descritores geométricos organizado em uma lista de dicionários.
Entre os descritores calculados destacam-se a área, o perímetro, o centroide, a caixa delimitadora (bounding box), a circularidade, a solidez e o número de vértices do contorno aproximado. Esses vértices são obtidos aproximando-se o contorno por um polígono simplificado, calculado pela função approxPolyDP, que preserva a forma geral do objeto utilizando um número reduzido de segmentos.
assumindo valor igual a 1 para um círculo perfeito e valores menores para formas progressivamente menos circulares.
A solidez é dada por
\[
\text{Solidez}=
\frac{\text{Área}}
{\text{Área do Fecho Convexo}},
\]
indicando o quanto o objeto preenche seu fecho convexo. Valores próximos de 1 caracterizam objetos convexos, enquanto valores menores indicam a presença de concavidades.
O número de vértices fornece uma indicação da complexidade da forma do objeto. Por exemplo, um triângulo tende a produzir três vértices e um retângulo quatro, enquanto objetos de contorno curvo, como moedas, costumam resultar em polígonos com maior número de vértices, dependendo da precisão adotada na aproximação.
Como as moedas deste exemplo apresentam contornos aproximadamente circulares e convexos, espera-se que a circularidade assuma valores elevados (tipicamente entre 0,8 e 0,9) e que a solidez permaneça muito próxima de 1. O número de vértices depende do parâmetro utilizado na aproximação poligonal (precision), aumentando à medida que a aproximação preserva mais detalhes do contorno. Em conjunto, esses descritores podem ser empregados na classificação de objetos, na identificação de componentes espúrios remanescentes da segmentação e na realização de medições quantitativas sobre a imagem.
# mm.measure recebe uma imagem binária; cada componente conexo corresponde# a uma moeda individual.medidas = mm.measure(mascara_limpa, precision=0.01)if medidas:print(f"Total de objetos catalogados: {len(medidas)}")print(f"{'ID':<4}{'Área':<10}{'Perímetro':<12}{'Circularidade':<14}",end="")print(f" {'Solidez':<10}{'Vértices':<8}")for m in medidas:print(f"{m['id']:<4}{m['area']:<10.1f}{m['perimeter']:<12.2f}"f"{m['circularity']:<15.3f}{m['solidity']:<10.3f}{m['vertices']:<8}")else:print("Nenhuma medida retornada.")
Observa-se que foram identificadas 24 moedas. As medidas de solidez permanecem próximas de 1, indicando contornos essencialmente convexos, enquanto a circularidade varia entre aproximadamente 0,84 e 0,90 devido às irregularidades do contorno discretizado. O número de vértices varia entre 9 e 14, refletindo a aproximação poligonal utilizada para representar cada moeda.
8.8.5 Exportação de Anotações: de mm.measure ao Formato YOLO
Além dos descritores geométricos, mm.measure calcula, para cada objeto, sua caixa delimitadora (bounding box). Essas informações podem ser exportadas automaticamente pela função mm.saveMeasures, permitindo gerar arquivos de anotação para diferentes aplicações sem a necessidade de rotulação manual.
A função suporta três formatos de saída:
fmt="csv": exporta todos os descritores geométricos, sendo útil para análise exploratória, medição e classificação baseada em atributos;
fmt="txt": grava os descritores em formato tabular simples;
fmt="yolo": gera anotações compatíveis com o formato utilizado pelos detectores da família YOLO (You Only Look Once).
No formato YOLO, cada objeto é representado por uma linha contendo
\[
\text{classe}\;\;x_c\;\;y_c\;\;w_n\;\;h_n,
\]
em que \((x_c,y_c)\) representa o centro da caixa delimitadora e \((w_n,h_n)\) suas dimensões, todos normalizados pelas dimensões da imagem:
em que \((x,y)\) correspondem às coordenadas do canto superior esquerdo da caixa delimitadora, \((w,h)\) às suas dimensões em pixels e \((W,H)\) à largura e à altura da imagem. A normalização torna as anotações independentes da resolução da imagem, permitindo utilizar o mesmo formato em imagens de diferentes tamanhos.
O código a seguir exporta as medidas extraídas das moedas para os formatos CSV e YOLO. A saída em CSV preserva todos os descritores geométricos calculados por mm.measure, enquanto o arquivo no formato YOLO contém apenas a classe e a caixa delimitadora normalizada de cada objeto, conforme exigido pelos detectores dessa família.
Essa representação será utilizada novamente no Capítulo 9, dedicado aos métodos de detecção de objetos baseados em Deep Learning.
# Exporta as medidas extraídas das moedas em dois formatos de anotaçãoaltura_img, largura_img = img_moedas.shape[:2]mm.saveMeasures("moedas.csv", medidas, fmt="csv")mm.saveMeasures("moedas.txt", medidas, fmt="yolo", img_width=largura_img, img_height=altura_img, class_id=0)print("--- Trecho de moedas.csv ---")withopen("moedas.csv") as f:for linha in f.readlines()[:4]:print(linha.rstrip())print("\n--- Trecho de moedas.txt (formato YOLO) ---")withopen("moedas.txt") as f:for linha in f.readlines()[:4]:print(linha.rstrip())
8.8.6 Validação de Anotações por IoU: mm.verifyBoundBox
Antes de utilizar anotações no treinamento ou na avaliação de modelos, é importante verificar se elas concordam com um conjunto de referência (ground truth).
A função mm.verifyBoundBox realiza essa comparação utilizando a métrica IoU (Intersection over Union), apresentada anteriormente neste capítulo. Para cada caixa delimitadora candidata, a função calcula sua sobreposição com as caixas do gabarito e contabiliza aquelas cujo IoU é superior a um limiar especificado.
No exemplo a seguir, constrói-se um gabarito sintético a partir das cinco primeiras caixas delimitadoras obtidas por mm.measure. Como as anotações comparadas são as mesmas utilizadas para construir o gabarito, espera-se que cada objeto encontre exatamente uma correspondência com \(\mathrm{IoU}\ge0{,}5\). O objetivo é apenas ilustrar o funcionamento da função mm.verifyBoundBox; em aplicações reais, o gabarito deve ser obtido independentemente, por exemplo, por anotação manual.
# Matriz de gabaritos sintética no formato (classe, x1, y1, x2, y2), normalizadogabaritos = np.array([ [0, *[(m["bbox"][0] + d) / largura_img for d in (0,)], (m["bbox"][1]) / altura_img, (m["bbox"][0] + m["bbox"][2]) / largura_img, (m["bbox"][1] + m["bbox"][3]) / altura_img]for m in medidas[:5]])acertos =0for m in medidas[:5]: correspondencias = mm.verifyBoundBox( object_id=0, bbox=m["bbox"], matrix=gabaritos, width=largura_img, height=altura_img, threshold=0.5 ) acertos +=int(correspondencias >0)print(f"Objeto {m['id']}: {correspondencias} gabarito(s) casam com IoU >= 0.5")print(f"\nTotal de objetos validados: {acertos}/{len(medidas[:5])}")
Objeto 1: 1 gabarito(s) casam com IoU >= 0.5
Objeto 2: 1 gabarito(s) casam com IoU >= 0.5
Objeto 3: 1 gabarito(s) casam com IoU >= 0.5
Objeto 4: 1 gabarito(s) casam com IoU >= 0.5
Objeto 5: 1 gabarito(s) casam com IoU >= 0.5
Total de objetos validados: 5/5
Observa-se que os cinco objetos foram corretamente associados aos respectivos gabaritos, resultando em cinco correspondências válidas. Em aplicações reais, essa mesma estratégia pode ser empregada para comparar automaticamente as caixas delimitadoras produzidas por um detector com anotações de referência, permitindo avaliar quantitativamente seu desempenho por meio da métrica IoU.
8.8.7 Visualização de Anotações Salvas: mm.showBoundBox
Após exportar as anotações, é conveniente verificar se as caixas delimitadoras foram gravadas corretamente. Para isso, a biblioteca morph disponibiliza a função mm.showBoundBox, ilustrada na Figura 8.14.
A função lê um arquivo de anotações nos formatos yolo, csv ou txt, reconstrói as caixas delimitadoras (bounding boxes) e as sobrepõe à imagem original, facilitando a inspeção visual do resultado. Dessa forma, torna-se possível confirmar rapidamente se as anotações estão alinhadas aos objetos, sem a necessidade de examiná-las manualmente.
Essa função complementa mm.verifyBoundBox. Enquanto mm.verifyBoundBox realiza uma validação quantitativa, comparando as anotações com um conjunto de referência por meio da métrica IoU, mm.showBoundBox fornece uma validação qualitativa, permitindo inspecionar visualmente as caixas delimitadoras reconstruídas.
Figura 8.14: Bounding boxes reconstruídas a partir do arquivo de anotações YOLO exportado por mm.saveMeasures, sobrepostas à imagem original das moedas.
Nota🧠 Por que funciona? — Da Segmentação às Anotações
Após segmentar uma imagem, é possível medir automaticamente cada objeto (mm.measure) e converter essas medidas em anotações (mm.saveMeasures) para diferentes formatos, incluindo o utilizado pelos detectores da família YOLO. Esse procedimento permite gerar automaticamente conjuntos iniciais de anotações em cenários controlados, reduzindo significativamente o trabalho de rotulação manual.
Antes de utilizar essas anotações no treinamento ou na avaliação de modelos, recomenda-se compará-las com um conjunto de referência (ground truth). A função mm.verifyBoundBox automatiza essa etapa por meio da métrica IoU, permitindo quantificar a concordância entre as caixas delimitadoras geradas e as anotações de referência. Complementarmente, mm.showBoundBox possibilita uma inspeção visual das anotações reconstruídas sobre a imagem original, facilitando a identificação de erros de normalização, posicionamento ou ordenação das coordenadas que podem não ser evidentes em uma validação apenas numérica.
Em conjunto, mm.measure, mm.saveMeasures, mm.verifyBoundBox e mm.showBoundBox implementam um fluxo completo para medir objetos segmentados, gerar anotações, avaliá-las e inspecioná-las visualmente. Quando a segmentação produz resultados confiáveis, esse fluxo pode reduzir significativamente a necessidade de rotulação manual na construção de conjuntos de dados.
8.9 Visão Geral dos Modelos Modernos
As técnicas apresentadas neste capítulo — como detectores em cascata, segmentação baseada em limiarização e pós-processamento por IoU e NMS — permanecem importantes para compreender os fundamentos da Visão Computacional. Atualmente, entretanto, muitas aplicações utilizam modelos de Aprendizado Profundo, capazes de aprender automaticamente representações discriminativas a partir de grandes conjuntos de dados, dispensando a definição manual de características.
A Tabela 8.2 apresenta algumas arquiteturas representativas para detecção e segmentação de imagens. O objetivo é apenas situá-las no contexto das tarefas estudadas neste capítulo. Seus princípios de funcionamento, treinamento e aplicação serão discutidos em detalhes no Capítulo 9.
Tabela 8.2: Arquiteturas representativas de Aprendizado Profundo para detecção e segmentação de imagens.
Modelo
Tarefa
Ideia central
YOLO (You Only Look Once)
Detecção de objetos
Detecta objetos em uma única passada pela rede, estimando classes e caixas delimitadoras. Versões recentes, como o YOLO26 (2026), eliminam a etapa de Non-Maximum Suppression (NMS), tornando a inferência totalmente end-to-end.
Faster R-CNN
Detecção de objetos
Gera regiões candidatas e as refina antes da classificação, priorizando precisão.
SSD (Single Shot Detector)
Detecção de objetos
Detecta objetos em diferentes escalas em uma única passada, buscando equilíbrio entre velocidade e precisão.
U-Net
Segmentação semântica
Produz uma máscara que classifica cada pixel da imagem segundo sua classe.
Mask R-CNN
Segmentação de instâncias
Estende o Faster R-CNN acrescentando uma máscara individual para cada objeto detectado.
Segment Anything (SAM)
Segmentação orientada por prompts
Segmenta objetos a partir de indicações como pontos, caixas delimitadoras ou máscaras. Evoluiu para o SAM 2, com suporte a vídeo e rastreamento temporal de objetos, e o SAM 3, que permite segmentação guiada diretamente por descrições em texto.
Observa-se uma evolução das tarefas abordadas neste capítulo. Modelos como YOLO, Faster R-CNN e SSD localizam objetos por meio de caixas delimitadoras. O Mask R-CNN amplia essa capacidade ao produzir também uma máscara para cada instância detectada. Já modelos mais recentes, como o Segment Anything (SAM), permitem segmentar objetos a partir de diferentes tipos de prompts, tornando o processo mais flexível.
Essa evolução acompanha a sequência de conceitos desenvolvida ao longo do capítulo: da correspondência de características e da localização aproximada por caixas delimitadoras até a segmentação precisa dos pixels pertencentes a cada objeto. No Capítulo 9, essas tarefas serão revisitadas sob a perspectiva do Aprendizado Profundo, explorando como redes neurais modernas aprendem automaticamente representações capazes de superar muitas das limitações dos métodos clássicos apresentados aqui.
8.10 Limitações das Abordagens Clássicas e Motivação para o Deep Learning
Os exemplos desenvolvidos neste capítulo ilustram três problemas fundamentais da Visão Computacional: correspondência de características, detecção de objetos e segmentação de imagens. Embora as técnicas clássicas apresentadas sejam eficientes em diversos cenários, todas compartilham uma limitação importante: baseiam-se em características definidas manualmente para descrever ou identificar os objetos de interesse.
O ORB utiliza descritores binários capazes de estabelecer correspondências entre pontos sob rotações e mudanças moderadas de escala, mas não foi concebido para reconhecer categorias de objetos.
O Haar Cascade emprega um conjunto fixo de filtros retangulares, funcionando bem para objetos com aparência relativamente padronizada, como faces frontais, mas tornando-se mais suscetível a falsos positivos em cenas complexas.
A segmentação por limiarização explora diferenças de intensidade entre objeto e fundo, sendo adequada para imagens com bom contraste, porém limitada diante de variações de iluminação, texturas ou cenas contendo múltiplas classes de objetos.
Em todos esses casos, o desempenho depende da capacidade das características escolhidas representarem adequadamente a variabilidade presente nas imagens. Em aplicações reais, fatores como mudanças de pose, iluminação, escala, oclusões e diversidade de categorias tornam essa tarefa cada vez mais difícil, reduzindo a generalização dos métodos clássicos.
O Capítulo 9 apresenta uma abordagem diferente para esses problemas por meio das Redes Neurais Convolucionais (CNNs). Em vez de utilizar características definidas manualmente, esses modelos aprendem automaticamente, a partir de grandes conjuntos de dados, representações adequadas para cada tarefa. As arquiteturas modernas apresentadas na seção anterior — como YOLO, Faster R-CNN, U-Net, Mask R-CNN e Segment Anything (SAM) — seguem esse princípio e representam uma evolução das técnicas clássicas estudadas neste capítulo, ampliando sua capacidade de lidar com a diversidade e a complexidade das imagens do mundo real.
8.11 Resumo
Neste capítulo foram estudados métodos clássicos de Visão Computacional para correspondência de características, detecção de objetos e segmentação de imagens. Ao final, foram apresentadas algumas arquiteturas modernas baseadas em Aprendizado Profundo, preparando a transição para o Capítulo 9. Os principais conceitos abordados foram:
ORB: detecção de pontos-chave, construção de descritores binários e correspondência de características por meio da distância de Hamming.
Homografia e RANSAC: estimação robusta de transformações projetivas para registro de imagens e eliminação de correspondências inconsistentes.
Haar Cascade: detecção de objetos utilizando características Haar, imagem integral e classificadores em cascata.
Segmentação clássica: limiarização, operações morfológicas, rotulação de componentes conectados, transformada de distância e algoritmo watershed para segmentação de instâncias.
Medição e anotação de objetos: extração de atributos geométricos com mm.measure, exportação automática de anotações (mm.saveMeasures) e validação por IoU (mm.verifyBoundBox) e inspeção visual (mm.showBoundBox).
Avaliação de detecções: utilização da métrica Intersection over Union (IoU) e da Supressão de Não-Máximos (NMS).
Modelos modernos: visão geral das arquiteturas YOLO, Faster R-CNN, SSD, U-Net, Mask R-CNN e Segment Anything (SAM), que servirão de base para o estudo dos métodos de Aprendizado Profundo no Capítulo 9.
Próximos Passos
Neste capítulo foram apresentados métodos clássicos para resolver problemas de correspondência de características, detecção de objetos e segmentação de imagens, baseados em descritores, filtros e modelos geométricos definidos manualmente.
No Capítulo 9, esses mesmos problemas serão revisitados sob a perspectiva do Aprendizado Profundo, com ênfase nas Redes Neurais Convolucionais (CNNs). Em vez de utilizar características projetadas manualmente, esses modelos aprendem automaticamente representações a partir de grandes conjuntos de dados, constituindo a base dos principais sistemas modernos de Visão Computacional para detecção, segmentação e reconhecimento de objetos.
8.12 🤖 Uso do Gemini Notebook como Apoio ao Estudo
O Gemini Notebook pode ser utilizado como ferramenta complementar para revisar os conceitos apresentados neste capítulo. A partir do conteúdo disponibilizado como referência, ele permite responder perguntas, elaborar resumos, esclarecer dúvidas e explorar os tópicos de forma interativa.
As respostas geradas pelo Gemini Notebook são produzidas automaticamente e podem conter imprecisões. Utilize-as como material complementar de estudo, confrontando as informações com o conteúdo deste livro e, quando necessário, com outras fontes acadêmicas.
8.13 Lista de Exercícios
Os exercícios a seguir consolidam os conceitos apresentados neste capítulo por meio de adaptações, experimentos e extensões dos algoritmos desenvolvidos ao longo do texto, utilizando a biblioteca didática morph.
(10%) Investigue a influência de rotações na estabilidade do detector e descritor ORB. Utilizando a imagem skimage.data.astronaut(), gere versões rotacionadas em ângulos de \({0^\circ,45^\circ,90^\circ,135^\circ,180^\circ}\). Para cada caso, determine o número de correspondências obtidas pelo BFMatcher e a fração de inliers identificados pelo RANSAC. Apresente os resultados em uma tabela e discuta a robustez do método às rotações analisadas.
(15%) Compare o detector de cantos FAST com o detector de Harris (Capítulo 6). Meça o tempo médio de execução e o número de pontos detectados nas imagens skimage.data.camera(), skimage.data.gravel() e skimage.data.brick(). Discuta as vantagens e limitações de cada detector em termos de velocidade e repetibilidade.
(15%) Investigue a sensibilidade do Haar Cascade à presença de ruído e desfoque. Adicione ruído gaussiano (\(\sigma\in{5,10,20}\)) e desfoque de movimento à imagem skimage.data.astronaut(). Varie os parâmetros scaleFactor e minNeighbors da função detectMultiScale() e discuta os efeitos sobre falsos positivos, falsos negativos e número de faces detectadas.
(15%) Implemente manualmente a métrica Intersection over Union (IoU) para caixas delimitadoras e compare seus resultados com a função mm.IoU. Em seguida, crie uma caixa de referência e dez caixas obtidas por deslocamentos e variações de escala, construindo um gráfico que relacione o deslocamento aplicado com o valor de IoU obtido.
(15%) Avalie o algoritmo de Supressão de Não-Máximos (NMS). Crie uma cena sintética contendo três objetos, cada um representado por pelo menos cinco caixas sobrepostas com diferentes pontuações de confiança. Execute o algoritmo para limiares de IoU iguais a \({0.2,0.4,0.6,0.8}\), visualize os resultados e discuta a influência desse parâmetro na quantidade de caixas mantidas.
(15%) Aplique o pipeline de segmentação apresentado neste capítulo (limiarização de Otsu, operações morfológicas e mm.measure) à imagem skimage.data.coins(). Exporte as medidas utilizando mm.saveMeasures(fmt="csv") e produza histogramas das distribuições de área, circularidade e solidez. Discuta quais desses atributos são mais adequados para caracterizar as moedas.
(15%) Construa uma imagem sintética contendo círculos, retângulos e triângulos, alguns parcialmente sobrepostos. Realize a segmentação, extraia as medidas com mm.measure e gere automaticamente as anotações no formato YOLO utilizando mm.saveMeasures(fmt="yolo"). Em seguida, modifique deliberadamente algumas bounding boxes e utilize mm.verifyBoundBox para avaliar, em diferentes limiares de IoU, quantas anotações permanecem válidas. Discuta a influência da sobreposição entre objetos na qualidade das anotações obtidas.
(Bônus – 10%) Desenvolva um classificador simples, como k-NN (Capítulo 7), utilizando apenas os atributos geométricos produzidos por mm.measure (área, circularidade, solidez e número de vértices). Utilize um conjunto de formas sintéticas (círculos, retângulos e triângulos) com diferentes escalas e rotações, avalie a acurácia obtida e discuta a capacidade desses descritores em distinguir diferentes classes de objetos.
Referências do Capítulo
Os conceitos e algoritmos apresentados neste capítulo foram fundamentados em referências clássicas e contemporâneas da literatura de PDI-VC:
Gonzalez; Woods (2018), para os fundamentos de segmentação de imagens, operações morfológicas, rotulação de componentes conexos e extração de atributos geométricos.
Szeliski (2022), para os conceitos de correspondência de características, transformações geométricas, homografias, detecção de objetos e segmentação em Visão Computacional.
(rublee2011orb?), para a descrição do algoritmo ORB (Oriented FAST and Rotated BRIEF), utilizado na detecção, descrição e correspondência de características locais.
(fischler1981random?), para a formulação original do algoritmo RANSAC (Random Sample Consensus), empregado na estimação robusta de modelos geométricos na presença de dados inconsistentes.
Viola; Jones (2001), Viola; Jones (2004) e Lienhart; Maydt (2002), para os fundamentos do detector Haar Cascade, incluindo características de Haar, imagem integral, classificadores em cascata e extensões utilizadas em implementações modernas.
(kirillov2019panoptic?), para a definição da segmentação panóptica, que integra segmentação semântica e segmentação de instâncias em uma única representação.
Redmon et al. (2016), (ren2015faster?), (ronneberger2015u?), (he2017mask?) e (kirillov2023segment?), para modelos modernos baseados em Aprendizado Profundo aplicados à detecção e segmentação de imagens, incluindo YOLO, Faster R-CNN, U-Net, Mask R-CNN e Segment Anything (SAM).
8.14 💻 Parte Prática com Exercícios de Programação
🚧 Em construção!
A presente lista de exercícios de programação (EP) consolida as formulações teóricas apresentadas ao longo do Capítulo 8 — Correspondência de Características, Detecção de Objetos e Segmentação Clássica — por meio de uma trilha prática aplicada. Assim como no capítulo anterior, os EPs isolam as grandezas intermediárias de cada técnica — a distância entre descritores binários, os termos de uma imagem integral, a contagem de inliers de um modelo candidato, a sobreposição entre caixas delimitadoras e o rótulo de cada componente conectado — permitindo validar manualmente cada etapa do raciocínio sem depender do OpenCV nem de imagens externas.
O encadeamento dos exercícios reproduz o fluxo conceitual do capítulo: inicia-se com a distância de Hamming, coração da correspondência de descritores binários como o ORB; avança-se para a contagem de inliers que sustenta o RANSAC na estimação robusta de uma homografia; prossegue-se com a imagem integral, o truque computacional que torna o Haar Cascade viável em tempo real; aprofunda-se em IoU e Supressão de Não-Máximos, o pós-processamento comum a praticamente todo detector de objetos; e conclui-se com a rotulagem de componentes conectados, a abordagem clássica — e suas limitações — para segmentar instâncias individuais em uma máscara binária.
ImportanteDiretrizes para a Resolução dos Exercícios de Programação
Em todos os exercícios deste capítulo, as etapas de discretização ou arredondamento numérico devem empregar o arredondamento padrão para o inteiro mais próximo (round half away from zero), mitigando ambiguidades em valores com fração exatamente igual a \(0{,}5\). Salvo indicação explícita em contrário: (i) caixas delimitadoras são especificadas no formato canto-a-canto \((x_{min}, y_{min}, x_{max}, y_{max})\); (ii) matrizes e imagens usam indexação a partir de \(0\), com a convenção [linha][coluna]; e (iii) comparações de limiar seguem exatamente a convenção descrita em cada exercício — preste atenção especial a se o limiar é excludente (\(<\)) ou inclusivo (\(\le\)), pois isso varia entre os exercícios, tal como no código de referência do próprio capítulo.
🎯 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:
Para avaliar os testes, execute TestSuite("EP08_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.
Para testar código Python diretamente, sem salvar arquivo, use run_code(codigo) passando o código como string numa variável codigo:
codigo ="""# ... seu código aqui ..."""TestSuite("EP08_01").run_code(codigo)
8.14.1 EP08_01 🟢 Distância de Hamming e Correspondência de Descritores Binários
O ORB, usado no Projeto Prático 1 deste capítulo, descreve a vizinhança de cada ponto de interesse como uma sequência de bits — e, por isso, a comparação entre dois descritores não usa a distância euclidiana do k-NN do Capítulo 7, e sim a distância de Hamming: o número de posições em que os bits diferem. Antes de chamar cv2.BFMatcher(cv2.NORM_HAMMING), você foi encarregado de implementar manualmente essa correspondência (matching) por força bruta — a mesma etapa que, executada internamente pelo OpenCV, precede a estimação robusta da homografia por RANSAC.
8.14.1.1 📋 Diretrizes de Implementação
Quantidades: Ler os inteiros \(N\) e \(M\) — número de descritores extraídos da imagem A e da imagem B, respectivamente.
Descritores de A: Ler \(N\) linhas, cada uma contendo um descritor binário (uma string de caracteres 0 e 1, todos do mesmo comprimento).
Descritores de B: Ler \(M\) linhas, no mesmo formato.
Limiar: Ler o inteiro \(\tau\) — distância de Hamming máxima aceitável para considerar uma correspondência válida.
Distância de Hamming: Para dois descritores binários \(a\) e \(b\) de mesmo comprimento, \[
d_H(a, b) = \sum_{k} \mathbb{1}[a_k \neq b_k],
\] ou seja, a contagem de posições em que os bits diferem.
Correspondência por vizinho mais próximo: Para cada descritor \(a_i\) de A (\(i\) na ordem de leitura, começando em \(0\)), calcule sua distância de Hamming a todos os descritores de B e encontre o de menor distância. Em caso de empate entre dois ou mais descritores de B com a mesma distância mínima, escolha o de menor índice.
Filtragem pelo limiar: Se a menor distância encontrada for \(\le \tau\), a correspondência é válida; caso contrário, \(a_i\) não possui correspondência.
Saída: Para cada \(i\) de \(0\) a \(N-1\), na ordem de leitura, imprimir uma linha: i j d se houver correspondência válida (onde \(j\) é o índice do descritor de B escolhido e \(d\) sua distância), ou i -1 caso contrário. Ao final, imprimir Total correspondências válidas: X.
8.14.1.2 📌 Restrições Computacionais
Mesmo comprimento: todos os descritores (de A e de B) têm exatamente o mesmo número de bits.
Força bruta: compare cada descritor de A a todos os de B — não é necessário nenhum tipo de indexação ou estrutura de aceleração.
Desempate por menor índice em B, e nunca por ordem de leitura de A (que já é natural, pois cada \(a_i\) é tratado de forma independente).
8.14.1.3 🧠 Fundamentação Teórica
Elemento
Papel na correspondência ORB
Descritor binário (BRIEF)
Cada bit é o resultado de uma comparação de intensidade entre dois pixels da vizinhança
Distância de Hamming
Métrica de dissimetria entre strings binárias; muito mais rápida de calcular que a distância euclidiana (operação XOR + contagem de bits)
Vizinho mais próximo
Critério de correspondência: cada ponto de A é pareado ao ponto de B com descritor mais similar
Limiar \(\tau\)
Filtra correspondências pouco confiáveis antes mesmo do RANSAC — mas, como discutido no capítulo, algumas correspondências incorretas ainda passam, exigindo a robustez do RANSAC
8.14.1.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiros \(N\) e \(M\).
Próximas \(N\) linhas: um descritor binário por linha (string de 0s e 1s).
Próximas \(M\) linhas: um descritor binário por linha, no mesmo formato.
Última linha: Inteiro \(\tau\).
Saída:
\(N\) linhas, uma por descritor de A, no formato i j d ou i -1.
O descritor 11110000 não encontra correspondência: seu vizinho mais próximo está a distância 4, acima do limiar \(\tau=2\).
🎮 Simulador: Distância de Hamming🟢 descritores de 8 bits
Clique em qualquer bit do descritor B para invertê-lo e observe a distância de Hamming mudar em tempo real.
Descritor A (fixo)
Descritor B (clique para inverter um bit)
Figura 8.15: Simulador: Distância de Hamming entre Dois Descritores Binários
%%writefile EP08_01.py# Código Python
Overwriting EP08_01.py
TestSuite("EP08_01.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/casos/EP08_01.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/cap8/EP8_1.cases
❌ Não foi possível baixar EP08_01.cases
8.14.2 EP08_02 🟡 Homografia e RANSAC: A Votação por Inliers
O RANSAC, apresentado na seção “Modelagem Matemática: Homografia e RANSAC”, repete um ciclo de três passos — sortear uma amostra mínima, estimar um modelo candidato, e contar quantas correspondências são consistentes com ele (os inliers) — mantendo ao final o modelo mais votado. A etapa de estimação do modelo a partir de 4 pontos (passo 2) envolve álgebra linear que foge ao escopo deste EP; aqui, você recebe diretamente um conjunto de homografias já candidatas — como se cada uma tivesse sido estimada a partir de uma amostra aleatória diferente — e é encarregado de reproduzir exatamente o passo decisivo do algoritmo: aplicar cada modelo a todas as correspondências e contar seus inliers, escolhendo o vencedor.
8.14.2.1 📋 Diretrizes de Implementação
Correspondências: Ler o inteiro \(N\) e, em seguida, \(N\) linhas com quatro reais cada, \(x\ y\ x'\ y'\) — um ponto da imagem A e seu correspondente (possivelmente incorreto) na imagem B, exatamente como produzido pela etapa de matching do EP08_01.
Modelos candidatos: Ler o inteiro \(K\) (número de homografias candidatas) e o real \(\varepsilon\) (limiar de erro de reprojeção). Em seguida, ler \(K\) linhas, cada uma com nove reais \(h_{11}\ h_{12}\ h_{13}\ h_{21}\ h_{22}\ h_{23}\ h_{31}\ h_{32}\ h_{33}\) — os elementos da matriz \(H\) candidata, em ordem de leitura por linha (row-major).
Reprojeção: Para cada correspondência \((x,y,x',y')\) e cada modelo candidato \(H_k\), calcular o ponto projetado \[
\begin{bmatrix} \hat x \\ \hat y \\ \hat w \end{bmatrix} = H_k \begin{bmatrix} x \\ y \\ 1 \end{bmatrix},
\qquad
(\hat x / \hat w,\ \hat y / \hat w)\ \text{é o ponto projetado.}
\]
Erro de reprojeção:\(e = \sqrt{(\hat x/\hat w - x')^2 + (\hat y /\hat w - y')^2}\).
Contagem de inliers: Uma correspondência é um inlier do modelo \(H_k\) se \(e \le \varepsilon\).
Seleção do melhor modelo: O modelo vencedor é o de maior número de inliers; em caso de empate, escolha o de menor índice\(k\) (o primeiro encontrado durante o ciclo iterativo do RANSAC).
Saída: Para cada modelo \(k\) de \(0\) a \(K-1\), na ordem de leitura, imprimir Modelo k: I inliers. Ao final, imprimir Melhor modelo: k_best com I_best inliers.
8.14.2.2 📌 Restrições Computacionais
Comparação inclusiva: um erro de reprojeção exatamente igual a \(\varepsilon\) conta como inlier (\(e \le \varepsilon\)).
Sem estimação de \(H\): as matrizes já são fornecidas prontas — não é necessário (nem esperado) resolver nenhum sistema linear.
Empate resolvido pelo menor índice, refletindo o comportamento natural de um algoritmo iterativo que percorre os modelos em ordem e só substitui o melhor encontrado até então quando um novo modelo o supera estritamente.
8.14.2.3 🧠 Fundamentação Teórica
Elemento
Papel no RANSAC
Amostra mínima (4 pares)
Suficiente para determinar os 8 graus de liberdade de uma homografia
Modelo candidato \(H_k\)
Estimado a partir de uma amostra mínima; pode ser bom ou ruim, dependendo se a amostra continha outliers
Erro de reprojeção
Mede o quão bem o modelo “prevê” cada correspondência observada
Inlier vs. outlier
Correspondências consistentes com o modelo vencedor (inliers) vs. as demais, tipicamente correspondências incorretas do matching
Refinamento final
Na prática, após escolher o melhor modelo, o RANSAC o recalcula usando apenas seus inliers — passo não exigido neste EP
8.14.2.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiro \(N\).
Próximas \(N\) linhas: quatro reais \(x\ y\ x'\ y'\).
Próxima linha: Inteiro \(K\) e real \(\varepsilon\).
Próximas \(K\) linhas: nove reais (elementos de \(H_k\), row-major).
Saída:
\(K\) linhas no formato Modelo k: I inliers.
Última linha: Melhor modelo: k_best com I_best inliers.
Modelo 0: 4 inliers
Modelo 1: 1 inliers
Melhor modelo: 0 com 4 inliers
O Modelo 0 (escala ×2) explica corretamente 4 das 5 correspondências; a 5ª, \((5,5)\to(1,1)\), é um outlier que nenhum dos dois modelos explica bem.
🎮 Simulador: RANSAC — Contagem de Inliers🟡 modelo: escala ×2
O modelo candidato mapeia (x,y) → (2x,2y). Ajuste o limiar ε e veja quais correspondências (pontos) tornam-se inliers (verde) ou outliers (vermelho).
0.50
Figura 8.16: Simulador: RANSAC — Votação por Inliers entre Modelos Candidatos
%%writefile EP08_02.py# Código Python
Overwriting EP08_02.py
TestSuite("EP08_02.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/casos/EP08_02.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/cap8/EP8_2.cases
❌ Não foi possível baixar EP08_02.cases
8.14.3 EP08_03 🟢 Imagem Integral: Somas Retangulares em Tempo Constante
O Haar Cascade avalia milhares de características retangulares por janela, em múltiplas posições e escalas — algo inviável em tempo real se cada retângulo exigisse somar seus pixels um a um. A imagem integral, definida na seção sobre Haar Cascade, resolve esse problema: uma vez pré-computada, a soma de intensidades de qualquer região retangular é obtida com apenas quatro consultas e três operações aritméticas, independentemente do tamanho do retângulo.
Você foi encarregado de implementar essa estrutura do zero: primeiro, calcular a imagem integral a partir da imagem original; em seguida, respondê-la para consultas retangulares arbitrárias.
8.14.3.1 📋 Diretrizes de Implementação
Entrada: Ler as dimensões \(H \times W\) da imagem e seus \(H \times W\) valores inteiros de intensidade.
Imagem integral: Calcular, para cada posição \((i,j)\) (indexação a partir de \(0\), [linha][coluna]), \[
II(i,j) = \sum_{i' \le i,\ j' \le j} I(i', j'),
\] ou seja, a soma de todos os pixels acima e à esquerda de \((i,j)\), incluindo a própria posição.
Consultas: Ler o inteiro \(Q\) e, em seguida, \(Q\) linhas, cada uma com quatro inteiros \(x_1\ y_1\ x_2\ y_2\) — os cantos superior-esquerdo e inferior-direito de um retângulo, ambos inclusivos, com \(0 \le x_1 \le x_2 < W\) e \(0 \le y_1 \le y_2 < H\).
Soma retangular em O(1): Para cada consulta, calcular a soma de intensidades dentro do retângulo usando exclusivamente valores já presentes em \(II\) (sem percorrer os pixels originais): \[
S(x_1,y_1,x_2,y_2) = II(y_2,x_2) - II(y_2, x_1{-}1) - II(y_1{-}1, x_2) + II(y_1{-}1, x_1{-}1),
\] tratando qualquer termo com índice de linha ou coluna igual a \(-1\) como \(0\).
Saída: Primeiro, imprimir a imagem integral completa — \(H\) linhas com \(W\) inteiros cada. Em seguida, para cada consulta, imprimir um único inteiro: a soma da região correspondente.
8.14.3.2 📌 Restrições Computacionais
Não recalcule por força bruta: a resposta a cada consulta deve usar a fórmula de quatro termos sobre \(II\), não uma soma direta dos pixels do retângulo (ainda que o resultado numérico seja o mesmo, o objetivo do exercício é justamente essa técnica).
Retângulos com coordenadas inclusivas:\((x_1,y_1)\) e \((x_2,y_2)\) pertencem à região somada.
Tratamento de borda: ao consultar \(II\) com índice \(-1\) (quando \(x_1=0\) ou \(y_1=0\)), utilize o valor \(0\).
8.14.3.3 🧠 Fundamentação Teórica
Elemento
Papel no Haar Cascade
Imagem integral \(II\)
Pré-computada uma única vez por imagem, em tempo \(O(HW)\)
Consulta em O(1)
Cada característica Haar (diferença entre somas de regiões retangulares) é avaliada com poucas operações, independentemente da área do retângulo
Escalabilidade
É essa constância que viabiliza avaliar milhares de características, em múltiplas posições e escalas, em tempo real
Princípio de inclusão-exclusão
Os quatro termos da fórmula somam a região desejada e subtraem exatamente as áreas contadas em excesso
8.14.3.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiros \(H\) e \(W\).
Próximas \(H\) linhas: \(W\) inteiros cada (imagem original).
Próxima linha: Inteiro \(Q\).
Próximas \(Q\) linhas: quatro inteiros \(x_1\ y_1\ x_2\ y_2\).
Saída:
\(H\) linhas com \(W\) inteiros cada (a imagem integral).
\(Q\) linhas, uma por consulta, com a soma da região correspondente.
8.14.3.5 📌 Exemplos
Entrada
Saída
Observação
3 3
1 2 3
4 5 6
7 8 9
1
0 0 2 2
1 3 6
5 12 21
12 27 45
45
A consulta cobre a imagem inteira; a soma coincide com \(II(2,2)\) e com a soma de todos os 9 valores.
3 3
1 2 3
4 5 6
7 8 9
2
1 1 2 2
0 0 1 1
1 3 6
5 12 21
12 27 45
28
12
A primeira consulta usa os quatro termos da fórmula; a segunda coincide diretamente com \(II(1,1)\), pois começa na origem.
🎮 Simulador: Imagem Integral🟢 consulta em 4 termos
Escolha o retângulo (i,j) inferior-direito da consulta, sempre a partir da origem (0,0) — a região destacada usa apenas o valor de II nesse canto.
(2,2)
Imagem original I (3×3)
Imagem integral II (3×3)
Figura 8.17: Simulador: Imagem Integral e Consulta Retangular em O(1)
%%writefile EP08_03.py# Código Python
Overwriting EP08_03.py
TestSuite("EP08_03.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/casos/EP08_03.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/cap8/EP8_3.cases
❌ Não foi possível baixar EP08_03.cases
8.14.4 EP08_04 🟡 IoU e Supressão de Não-Máximos (NMS)
A figura desta seção mostrou o efeito da Supressão de Não-Máximos sobre um conjunto de caixas produzidas por um detector do tipo sliding window: múltiplas detecções redundantes por objeto foram reduzidas a uma única caixa por objeto. Você foi encarregado de reimplementar, byte a byte, as duas funções que produziram aquele resultado — calcular_iou e supressao_nao_maximos — para confirmar, com suas próprias mãos, exatamente os números que o capítulo apresentou.
8.14.4.1 📋 Diretrizes de Implementação
Entrada: Ler o inteiro \(N\) (número de caixas) e o real \(\tau\) (limiar de IoU). Em seguida, ler \(N\) linhas, cada uma com cinco reais \(x_{min}\ y_{min}\ x_{max}\ y_{max}\ \text{score}\).
Interseção sobre União: Para duas caixas \(A\) e \(B\), \[
\mathrm{IoU}(A,B) = \frac{\text{área}(A \cap B)}{\text{área}(A) + \text{área}(B) - \text{área}(A \cap B)},
\] com área de interseção nula quando as caixas não se sobrepõem.
Algoritmo de NMS (exatamente como descrito no capítulo):
Ordene as caixas por score decrescente (empates mantêm a ordem de leitura original).
Selecione a caixa de maior pontuação entre as restantes; adicione-a à saída e remova-a da lista.
Descarte, da lista restante, todas as caixas cujo IoU com a caixa selecionada seja maior ou igual a \(\tau\) — apenas as caixas com \(\mathrm{IoU} < \tau\) permanecem candidatas.
Repita (b)–(c) até que a lista de restantes esteja vazia.
Saída: Para cada caixa mantida, na ordem em que foi selecionada, imprimir seu índice original (posição de leitura, a partir de \(0\)) e seu score, com 2 casas decimais. Ao final, imprimir Total mantidas: X.
8.14.4.2 📌 Restrições Computacionais
Atenção ao sentido do limiar: ao contrário do que se poderia supor, uma caixa é suprimida quando \(\mathrm{IoU} \ge \tau\) (não apenas quando \(\mathrm{IoU} > \tau\)) — siga exatamente esse critério, o mesmo do código de referência do capítulo.
Índices originais: a saída referencia a posição de leitura de cada caixa na entrada, não sua posição após a ordenação por score.
Área sem soma de 1 pixel: use área \(= (x_{max}-x_{min}) \times (y_{max}-y_{min})\), exatamente como no capítulo (sem o ajuste “+1” às vezes usado em outras convenções).
8.14.4.3 🧠 Fundamentação Teórica
Elemento
Papel no pós-processamento
IoU
Quantifica a sobreposição espacial entre duas caixas delimitadoras
Sliding window (Haar Cascade)
Produz tipicamente várias detecções sobrepostas para o mesmo objeto, em posições e escalas próximas
Limiar \(\tau\)
Controla a agressividade da supressão: baixo demais funde objetos próximos; alto demais deixa passar redundâncias
Ordenação por score
Garante que, entre caixas redundantes, a de maior confiança sempre sobrevive
8.14.4.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiro \(N\) e real \(\tau\).
Próximas \(N\) linhas: cinco reais \(x_{min}\ y_{min}\ x_{max}\ y_{max}\ \text{score}\).
Saída:
Uma linha por caixa mantida, na ordem de seleção: índice score (score com 2 casas decimais).
Exatamente o exemplo da figura do capítulo: 5 caixas redundantes (2 objetos) tornam-se 2 detecções finais. O IoU entre a 1ª e a 2ª caixas é \(\approx 0{,}775\), bem acima de \(\tau=0{,}4\).
🎮 Simulador: IoU e Supressão de Não-Máximos🟡 supressão se IoU ≥ τ
A caixa azul (score maior) já foi selecionada. Ajuste a sobreposição e o limiar τ para ver se a caixa vermelha (candidata) é suprimida.
3
0.40
Figura 8.18: Simulador: IoU e Supressão de Não-Máximos
%%writefile EP08_04.py# Código Python
Overwriting EP08_04.py
TestSuite("EP08_04.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/casos/EP08_04.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/cap8/EP8_4.cases
❌ Não foi possível baixar EP08_04.cases
8.14.5 EP08_05 🔴 Rotulagem de Componentes Conectados: Segmentação Clássica de Instâncias
O exemplo de segmentação clássica deste capítulo separou “instâncias” de moedas simplesmente pela sua desconexão espacial na máscara binária resultante da limiarização de Otsu. Essa etapa final — rotular cada componente conectado com um identificador de instância — é exatamente o que você foi encarregado de implementar aqui, do zero, sobre uma máscara binária já pronta (0 = fundo, 1 = objeto), como se fosse uma reimplementação manual de cv2.connectedComponents.
Este exercício também expõe, de forma muito concreta, a limitação discutida no capítulo: o resultado depende inteiramente de como se define “vizinhança” entre pixels — e, como você verá no segundo exemplo, dois pixels em diagonal podem ser considerados a mesma instância ou instâncias diferentes, dependendo exclusivamente da conectividade escolhida, não de qualquer noção semântica de objeto.
8.14.5.1 📋 Diretrizes de Implementação
Entrada: Ler as dimensões \(H \times W\) da máscara binária e seus \(H \times W\) valores (\(0\) ou \(1\)).
Conectividade: Ler o inteiro \(c \in \{4, 8\}\). Na conectividade \(4\), os vizinhos de \((i,j)\) são \((i{-}1,j)\), \((i{+}1,j)\), \((i,j{-}1)\) e \((i,j{+}1)\). Na conectividade \(8\), somam-se as quatro diagonais: \((i{-}1,j{-}1)\), \((i{-}1,j{+}1)\), \((i{+}1,j{-}1)\) e \((i{+}1,j{+}1)\).
Descoberta de componentes: Percorrendo a máscara em varredura linha a linha, da esquerda para a direita e de cima para baixo, sempre que um pixel de valor \(1\) ainda sem rótulo for encontrado, ele inicia um novo componente: atribua a ele o próximo rótulo disponível (o primeiro componente descoberto recebe o rótulo \(1\), o segundo o rótulo \(2\), e assim por diante) e propague esse mesmo rótulo a todos os pixels de valor \(1\) alcançáveis a partir dele por uma cadeia de vizinhos (de acordo com a conectividade escolhida) — por busca em largura, profundidade, ou union-find, à sua escolha.
Pixels de fundo: permanecem com rótulo \(0\) e não pertencem a nenhuma instância.
Saída: Primeiro, imprimir o mapa de rótulos completo — \(H\) linhas com \(W\) inteiros cada. Em seguida, para cada rótulo \(\ell\) de \(1\) a \(K\) (na ordem de descoberta), imprimir Instância l: A pixels, onde \(A\) é a quantidade de pixels com aquele rótulo. Por fim, imprimir Total de instâncias: K.
8.14.5.2 📌 Restrições Computacionais
Ordem de descoberta = ordem de varredura: os rótulos são numerados na ordem em que cada novo componente é encontrado pela varredura linha a linha, não por tamanho ou posição.
Conectividade explícita: dois pixels de valor \(1\) só pertencem à mesma instância se existir uma cadeia de vizinhos de acordo com \(c\) ligando um ao outro — não use a conectividade oposta por engano.
Máscara binária pura: todos os valores de entrada são exatamente \(0\) ou \(1\).
8.14.5.3 🧠 Fundamentação Teórica
Elemento
Papel na segmentação clássica de instâncias
Limiarização (Otsu, Cap. 4)
Etapa anterior que produz a máscara binária a partir da imagem de intensidade
Componente conectado
Cada instância é definida apenas por conectividade espacial dos pixels de objeto, sem qualquer noção de forma, classe ou aparência
Conectividade 4 vs. 8
Parâmetro que altera o resultado: sob conectividade 8, dois blobs unidos apenas na diagonal tornam-se uma única instância
Limitação central
A técnica funde instâncias que se tocam ou se sobrepõem (mesmo que sejam objetos claramente distintos), pois não há noção de “objeto” — apenas de “região conectada”
8.14.5.4 📦 Especificação de Entrada e Saída (VPL)
Entrada:
Linha 1: Inteiros \(H\) e \(W\).
Próximas \(H\) linhas: \(W\) inteiros (\(0\) ou \(1\)) cada.
Última linha: Inteiro \(c\) (\(4\) ou \(8\)).
Saída:
\(H\) linhas com \(W\) inteiros cada (o mapa de rótulos).
Uma linha por instância, na ordem de descoberta: Instância l: A pixels.
Dois blocos \(2\times2\) claramente separados: o resultado é o mesmo sob conectividade 4 ou 8.
2 2
1 0
0 1
8
1 0
0 1
Instância 1: 2 pixels
Total de instâncias: 1
Sob conectividade 8, os dois pixels em diagonal pertencem à mesma instância. Repita este exemplo com \(c=4\): o resultado passa a ser 2 instâncias de 1 pixel cada — puramente pela mudança de conectividade, sem qualquer diferença na máscara.
🎮 Simulador: Componentes Conectados🔴 mesma máscara, resultado diferente
A mesma máscara (dois pixels em diagonal) — alterne a conectividade e observe o número de instâncias e as cores dos rótulos mudarem.
Figura 8.19: Simulador: Rotulagem de Componentes Conectados — Conectividade 4 vs. 8
%%writefile EP08_05.py# Código Python
Overwriting EP08_05.py
TestSuite("EP08_05.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/casos/EP08_05.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap08/cap8/EP8_5.cases
❌ Não foi possível baixar EP08_05.cases
GONZALEZ, R. C.; WOODS, R. E. Digital Image Processing. 4th. ed. New York: Pearson, 2018.