Lab13 - Features
Features e Descritores em Visão Computacional¶
Objetivos da aula:¶
- Compreender o conceito de FEATURES (características) em imagens
- Aplicar algoritmos de detecção de pontos-chave (keypoints)
- Utilizar DESCRITORES para reconhecimento de objetos
- Implementar matching de imagens com invariância a escala e rotação
## Vou fazer o download das imagens do laboratório diretamente do repositório para ficar mais facil....
import requests
import os
# Define o laboratório
laboratorio = 'lab13' ### altere para o laboratório desejado
diretorio = 'lab_images' ### altere para o diretório que deseja salvar as imagens
# Download de um arquivo
def download_file(url, destination):
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(destination, 'wb') as file:
for chunk in response.iter_content(chunk_size=8192):
file.write(chunk)
print(f"Baixado: {destination}")
else:
print(f"Erro ao baixar {url}")
# Monta a URL completa
api_url = "https://api.github.com/repos/arnaldojr/cognitivecomputing/contents/material/aulas/PDI/"
url_completa = api_url + laboratorio
print(f"Fazendo o download de: {url_completa}")
# checa se a URL está acessível
response = requests.get(url_completa)
if response.status_code != 200:
raise Exception(f"Erro ao acessar o repositório: {response.status_code}")
files = response.json()
# Faz o download de cada arquivo
os.makedirs(diretorio, exist_ok=True) # Cria a pasta downloads
for file in files:
file_name = file['name']
if file_name.endswith(('.png', '.jpg', '.jpeg', '.mp4')): # Adicione mais extensões se necessário
file_url = file['download_url']
destination = os.path.join(diretorio, file_name)
download_file(file_url, destination)
print(f"Download concluído. Arquivos salvos na pasta {diretorio}.")
Contextualização do Problema¶
Em aulas anteriores, exploramos várias técnicas de processamento de imagens, incluindo o template matching. Embora essa técnica seja eficaz para encontrar padrões exatos em imagens, ela apresenta limitações significativas:
- Sensibilidade a mudanças de escala
- Problemas com rotação do objeto
- Vulnerabilidade a variações de iluminação
- Dificuldade com oclusões parciais
Para superar essas limitações, vamos explorar técnicas baseadas em features locais, que são mais robustas a estas transformações.
O Desafio: Detectar um Objeto em uma Cena¶
Queremos localizar esta caixa específica:
Dentro desta cena mais complexa:
Observe que a caixa aparece em uma posição diferente, com rotação e escala alteradas, além de estar parcialmente obstruída.
Reflexão Inicial¶
🤔 Pense: Com as técnicas que já conhecemos (como template matching, detecção de contornos ou segmentação por cor), seria possível resolver este problema de forma robusta?
- Quais seriam as limitações?
- Como poderíamos detectar o objeto mesmo quando ele está rotacionado?
- Como lidar com diferentes escalas?
Introdução às Features em Visão Computacional¶
O que são Features?¶
Features (características) são pontos ou regiões de interesse em uma imagem que possuem propriedades distintivas, podendo ser identificadas de forma consistente mesmo sob diferentes transformações.Podem ser:
- Pontos (como cantos ou junções)
- Bordas (mudanças abruptas de intensidade)
- Blobs (regiões com propriedades aproximadamente constantes)
- Regiões (áreas com textura específica)
Features Ideais devem ser:¶
- Repetíveis: Detectáveis mesmo com mudanças de iluminação, ruído, etc.
- Distintivas: Facilmente diferenciáveis de outras features
- Locais: Ocupando uma pequena área da imagem
- Numerosas: Em quantidade suficiente para representar o objeto
- Precisas: Localizáveis com exatidão
- Eficientes: Rápidas de computar
Principais Algoritmos de Detecção e Descrição de Features:¶
- SIFT (Scale-Invariant Feature Transform) - Robusto mas computacionalmente intensivo
- SURF (Speeded-Up Robust Features) - Mais rápido que SIFT, também patenteado
- ORB (Oriented FAST and Rotated BRIEF) - Código aberto, mais rápido
- FAST (Features from Accelerated Segment Test) - Detector rápido, sem descritor
- BRISK (Binary Robust Invariant Scalable Keypoints) - Alternativa binária
Aplicações:¶
- Reconhecimento de objetos
- Stitching de imagens (panoramas)
- Reconstrução 3D
- Rastreamento de objetos (tracking)
- Realidade aumentada
- Indexação e recuperação de imagens
Detecção de Features com ORB¶
Vamos usar o algoritmo ORB (Oriented FAST and Rotated BRIEF), que é uma excelente alternativa aos algoritmos patenteados como SIFT e SURF. O ORB foi desenvolvido pela OpenCV Labs e combina:
- O detector FAST modificado para detecção de keypoints
- O descritor BRIEF modificado para descrição de features
- Adição de orientação para garantir invariância à rotação
Documentação: OpenCV ORB
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
# Configurações para melhor visualização
plt.rcParams['figure.figsize'] = [12, 8]
plt.rcParams['font.size'] = 12
img1 = cv2.imread('box.png', 0) # Template (objeto de referência)
img2 = cv2.imread('box_in_scene.png', 0) # Imagem de busca
# Exibir o template
plt.figure(figsize=(8, 8))
plt.title("Template - Objeto de Referência")
plt.imshow(img1, cmap="gray")
plt.axis('on')
plt.show()
# Exibir a imagem de busca
plt.figure(figsize=(12, 10))
plt.title("Imagem de Busca - Cena Completa")
plt.imshow(img2, cmap="gray")
plt.axis('on')
plt.show()
O que são Descritores?¶
Um descritor é uma "assinatura" matemática que codifica a informação ao redor de uma feature detectada. É tipicamente um vetor numérico que caracteriza:
- O padrão de intensidade ao redor do ponto
- A distribuição de gradientes na vizinhança
- Outros atributos que permitem a correspondência entre features em diferentes imagens
# Inicializar o detector e descritor ORB
# Por padrão, detectará até 500 keypoints
orb = cv2.ORB_create()
print("Detector ORB inicializado com sucesso!")
# Detectar keypoints e calcular os descritores na imagem template
# Abordagem separada (passo-a-passo):
# 1. Detectar keypoints
# kp = orb.detect(img1, None)
# 2. Calcular descritores
# kp, des = orb.compute(img1, kp)
# Abordagem combinada (mais eficiente):
kp1, des1 = orb.detectAndCompute(img1, None)
print(f"Foram detectados {len(kp1)} keypoints na imagem template")
print(f"Formato dos descritores: {des1.shape if des1 is not None else 'Nenhum descritor calculado'}")
# Visualizar os keypoints detectados (versão simples)
img_keypoints = cv2.drawKeypoints(img1, kp1, outImage=None, color=(0,255,0),
flags=cv2.DrawMatchesFlags_DEFAULT)
plt.figure(figsize=(10, 10))
plt.title("Keypoints detectados na imagem template")
plt.imshow(cv2.cvtColor(img_keypoints, cv2.COLOR_BGR2RGB))
plt.show()
# Visualizar os keypoints com informação de escala e orientação
img_keypoints_rich = cv2.drawKeypoints(img1, kp1, outImage=None,
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
plt.figure(figsize=(10, 10))
plt.title("Keypoints com escala e orientação")
plt.imshow(cv2.cvtColor(img_keypoints_rich, cv2.COLOR_BGR2RGB))
plt.xlabel("As setas indicam a orientação do gradiente dominante em cada ponto")
plt.show()
# Explicação dos círculos e linhas:
print("Os círculos representam a escala onde o keypoint foi detectado")
print("As linhas mostram a orientação dominante do gradiente naquele ponto")
print("Essas propriedades garantem invariância à escala e rotação")
🔍 Desafio 1: Explorando Parâmetros do ORB¶
Por padrão, o detector ORB é configurado para encontrar 500 features na imagem.
Sua missão:¶
- Consulte a documentação do ORB
- Descubra como alterar o número máximo de features detectadas
- Identifique outros parâmetros importantes do ORB
- Implemente um exemplo que use parâmetros personalizados
- Compare o resultado com a implementação padrão
Dica: Alguns parâmetros importantes incluem nfeatures
, scaleFactor
, nlevels
e edgeThreshold
.
# Implemente sua solução para o Desafio 1 aqui
# Exemplo de implementação com parâmetros customizados
orb_custom = cv2.ORB_create(
nfeatures=100, # Número máximo de features a serem retidas (padrão: 500)
scaleFactor=1.2, # Fator de escala entre níveis na pirâmide (padrão: 1.2)
nlevels=8, # Número de níveis na pirâmide (padrão: 8)
edgeThreshold=31, # Tamanho da borda (padrão: 31)
firstLevel=0, # Nível da pirâmide onde é colocada a imagem de entrada (padrão: 0)
WTA_K=2, # Número de pontos para produzir cada elemento do descritor BRIEF (2 ou 3, padrão: 2)
scoreType=cv2.ORB_HARRIS_SCORE, # Tipo de pontuação (HARRIS_SCORE ou FAST_SCORE)
patchSize=31, # Tamanho do patch usado para orientação (padrão: 31)
fastThreshold=20 # Limiar para o detector de cantos FAST (padrão: 20)
)
# Compare os resultados com diferentes configurações
# ...
Exemplo de resultado com poucas features¶
Com apenas 4 features detectadas, é possível visualizar melhor cada keypoint, sua escala e orientação:
Cada círculo representa um keypoint, com:
- O raio do círculo indicando a escala
- A linha dentro do círculo representando a orientação
Detectando o Objeto na Cena¶
Agora que já extraímos os keypoints e descritores da imagem de referência (template), precisamos:
- Detectar keypoints e calcular descritores na imagem da cena
- Encontrar correspondências (matches) entre os descritores das duas imagens
- Filtrar as melhores correspondências
- Determinar a localização do objeto
🔍 Desafio 2: Comparando Keypoints¶
Sua missão:¶
- Calcule os keypoints e descritores para a imagem da cena (box_in_scene.png)
- Visualize os keypoints detectados nas duas imagens
- Compare os resultados e analise:
- Quantos keypoints foram detectados em cada imagem?
- Existe alguma região com maior concentração de keypoints?
- Você consegue identificar visualmente pontos correspondentes entre as duas imagens?
Dica: Use a mesma configuração do ORB para ambas as imagens para uma comparação justa.
# Implemente sua solução para o Desafio 2 aqui
# Detectar keypoints e calcular descritores na imagem da cena
kp2, des2 = orb.detectAndCompute(img2, None)
print(f"Keypoints na imagem template: {len(kp1)}")
print(f"Keypoints na imagem da cena: {len(kp2)}")
# Visualizar os keypoints nas duas imagens
# ...
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
# Carregar as imagens
img1 = cv2.imread('box.png', 0) # Template (objeto de referência)
img2 = cv2.imread('box_in_scene.png', 0) # Imagem de busca
# Criar detector ORB
orb = cv2.ORB_create(nfeatures=1000) # Aumentando o número de features para melhor visualização
# Calcular keypoints e descritores
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
print(f"Keypoints na imagem template: {len(kp1)}")
print(f"Keypoints na imagem da cena: {len(kp2)}")
# Desenhar keypoints nas imagens
img1_keypoints = cv2.drawKeypoints(img1, kp1, outImage=None, color=(0, 255, 0),
flags=cv2.DrawMatchesFlags_DEFAULT)
img2_keypoints = cv2.drawKeypoints(img2, kp2, outImage=None, color=(0, 255, 0),
flags=cv2.DrawMatchesFlags_DEFAULT)
# Visualizar os resultados
plt.figure(figsize=(15, 7))
plt.subplot(1, 2, 1)
plt.title("Keypoints no Template")
plt.imshow(img1_keypoints)
plt.subplot(1, 2, 2)
plt.title("Keypoints na Cena")
plt.imshow(img2_keypoints)
plt.tight_layout()
plt.show()
Matching de Features: Encontrando Correspondências¶
Agora que detectamos keypoints em ambas as imagens, precisamos encontrar quais pontos da imagem de referência correspondem aos pontos da imagem de busca.
Algoritmos de Matching:¶
Brute-Force Matcher: Compara cada descritor da primeira imagem com todos os descritores da segunda imagem.
FLANN (Fast Library for Approximate Nearest Neighbors): Mais rápido para grandes conjuntos de dados.
Vamos usar o Brute-Force Matcher que é mais simples e adequado para nosso exemplo:
# Criar o objeto BFMatcher (Brute-Force Matcher)
# NORM_HAMMING é usado para descritores binários como ORB
# crossCheck=True garante que as correspondências sejam mutuamente melhores
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
# Encontrar correspondências entre os descritores
matches = bf.match(des1, des2)
print(f"Foram encontradas {len(matches)} correspondências entre as imagens")
# Desenhar todas as correspondências
img_matches = cv2.drawMatches(img1, kp1, img2, kp2, matches, None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
plt.figure(figsize=(20, 10))
plt.title('Todas as correspondências encontradas')
plt.imshow(img_matches)
plt.show()
Filtragem de Correspondências¶
Como podemos observar, nem todas as correspondências encontradas são corretas. Muitas são falsos positivos. Podemos melhorar o resultado ordenando as correspondências por qualidade e selecionando apenas as melhores.
Como filtrar?¶
A qualidade de uma correspondência é determinada pelo atributo distance
do objeto DMatch
. Quanto menor a distância, melhor é a correspondência.
Vamos ordenar as correspondências por distância e ficar apenas com as melhores:
Sobre ordenação em Python com sorted()
e funções lambda¶
Em Python, podemos usar a função sorted()
para ordenar uma lista. Quando queremos ordenar por um critério específico, usamos a função lambda
como uma função de chave:
# Exemplo: ordenação em Python com sorted() e funções lambda
# Lista de compras: (item, preço)
lista = [
('Banana', 18),
('Maçã', 1),
('Goiaba', 20),
('Uva', 22),
('Pera', 12)
]
print("Lista não ordenada:", lista)
# Ordenar por ordem alfabética (primeiro elemento da tupla)
lista_ord = sorted(lista)
print("\nLista ordenada por nome (ordem alfabética):", lista_ord)
# Ordenar por preço (segundo elemento da tupla)
lista_ord_preco = sorted(lista, key=lambda x: x[1])
print("\nLista ordenada por preço (crescente):", lista_ord_preco)
# Ordenar por preço decrescente
lista_ord_preco_desc = sorted(lista, key=lambda x: x[1], reverse=True)
print("\nLista ordenada por preço (decrescente):", lista_ord_preco_desc)
Aplicando Filtragem às Correspondências¶
Agora vamos ordenar as correspondências (matches) encontradas pelo atributo distance
e selecionar apenas as melhores para visualização:
Propriedades do objeto DMatch:¶
- DMatch.distance: Distância entre os descritores (menor = melhor)
- DMatch.queryIdx: Índice do keypoint na imagem de consulta (template)
- DMatch.trainIdx: Índice do keypoint na imagem de teste (cena)
- DMatch.imgIdx: Índice da imagem de teste (útil em casos com múltiplas imagens)
help(cv2.DMatch)
# Ordenar as correspondências por distância (menor para maior)
matches = sorted(matches, key=lambda x: x.distance)
# Ver as distâncias das 10 melhores correspondências
print("Distâncias das 10 melhores correspondências:")
for i, match in enumerate(matches[:10]):
print(f"Match {i+1}: {match.distance:.2f}")
# Desenhar apenas as 15 melhores correspondências
num_best_matches = 15
img_best_matches = cv2.drawMatches(img1, kp1, img2, kp2, matches[:num_best_matches], None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
plt.figure(figsize=(20, 10))
plt.title(f'As {num_best_matches} melhores correspondências')
plt.imshow(img_best_matches)
plt.show()
🔍 Desafio 3: Implementando SIFT¶
SIFT (Scale-Invariant Feature Transform) é um dos algoritmos mais robustos para detecção e descrição de features. Vamos implementar o mesmo exemplo usando SIFT em vez de ORB.
Sua missão:¶
- Modifique o código para usar o algoritmo SIFT
- Para matching, use o método
knnMatch()
do BFMatcher que permite encontrar os k vizinhos mais próximos - Implemente a filtragem de correspondências usando o teste de ratio de Lowe
- Este teste compara a distância do melhor match com o segundo melhor
- Se ratio = d1/d2 < 0.75, então é um bom match
Dica: Use cv2.SIFT_create()
para criar o detector e descritor SIFT. Para o BFMatcher, use cv2.NORM_L2
em vez de cv2.NORM_HAMMING
.
# Implemente sua solução para o Desafio 3 aqui
# Exemplo de implementação com SIFT
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Carregar as imagens
img1 = cv2.imread('box.png', 0) # Template
img2 = cv2.imread('box_in_scene.png', 0) # Cena
# Inicializar SIFT
sift = cv2.SIFT_create()
# Detectar keypoints e descritores
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# Criar o BFMatcher - NORM_L2 para descritores baseados em gradiente como SIFT
bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False)
# Encontrar os 2 melhores matches para cada descritor
matches = bf.knnMatch(des1, des2, k=2)
# Aplicar o teste de ratio de Lowe
good_matches = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good_matches.append(m)
print(f"Foram encontrados {len(good_matches)} bons matches após filtro de ratio")
# Desenhar os matches
img_matches = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
plt.figure(figsize=(20, 10))
plt.title('SIFT: Matches após filtro de ratio de Lowe')
plt.imshow(img_matches)
plt.show()
🔍 Desafio 4: Detecção em Tempo Real¶
IMPORTANTE: Este desafio deve ser executado localmente em sua máquina, não no Google Colab.
Sua missão:¶
- Crie um script Python (.py) que capture vídeo da webcam
- Implemente a detecção de um objeto em tempo real usando técnicas de feature matching
- Desenhe um contorno ao redor do objeto quando for encontrado
Procedimentos:¶
- Escolha uma imagem de referência (template) - sugestão: um livro ou outro objeto plano com textura
- Extraia features do template usando ORB ou SIFT
- Para cada frame do vídeo:
- Extraia features
- Encontre matches
- Filtre os bons matches
- Use
findHomography
para mapear as coordenadas - Desenhe um polígono ao redor do objeto detectado
Função auxiliar para desenhar o contorno:¶
def desenhaContorno(qp, tp, refImg, frame):
"""
essa função do tipo void que desenha o contorno quando existe matches das duas imagens
recebe:
- qp,tp que representa a conversão de keypoints em argumentos para o findhomography ref:https://answers.opencv.org/question/122802/how-to-convert-keypoints-to-an-argument-for-findhomography/
- refImg = imagem de referência
- frame = imagem de destino onde será desenhado o contorno
"""
# o findHomography mapeia os pontos de um plano em outro.
# ou seja, mapeia os keypoints da imagem ref em frame
H,status=cv2.findHomography(qp,tp,cv2.RANSAC,3.0)
# extrai o shape da imagem de referencia
h,w=refImg.shape
# Mapeia os pontos das bordas com base no shape refImg (imagem de referncia), são 4 pontos
# [0,0] [w-1,0]
#
#
# [0,h-1] [w-1,h-1]
#
refBorda=np.float32([[[0,0],[0,h-1],[w-1,h-1],[w-1,0]]])
# Usa refBorda e a matrix de homografia H para calcular a matrix transformação de pespectiva
frameBorda=cv2.perspectiveTransform(refBorda,H)
# polylines desenha poligonos ou qualquer imagem, na cor verde e largura do traço igual a 5.
cv2.polylines(frame,[np.int32(frameBorda)],True,(0,255,0),5)
# ATENÇÃO: Este desafio deve ser executado em sua máquina local
# Não execute este código no navegador ou no Google Colab
Aplicação Prática: Criando Imagens Panorâmicas¶
Uma aplicação muito comum de feature matching é a criação de imagens panorâmicas (stitching). Este processo envolve:
- Detectar keypoints e calcular descritores em ambas as imagens
- Encontrar correspondências entre os descritores
- Calcular a transformação homográfica entre as imagens
- Aplicar warping para alinhar as imagens
- Combinar (blend) as imagens alinhadas
A OpenCV fornece a classe cv2.Stitcher
que implementa todo esse processo de forma otimizada.
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
img1 = cv2.imread("q11.jpg")
img2 = cv2.imread("q22.jpg")
# Exibir as imagens que serão combinadas
plt.figure(figsize=(15, 8))
plt.subplot(1, 2, 1)
plt.title("Imagem 1")
plt.imshow(cv2.cvtColor(img1, cv2.COLOR_BGR2RGB))
plt.subplot(1, 2, 2)
plt.title("Imagem 2")
plt.imshow(cv2.cvtColor(img2, cv2.COLOR_BGR2RGB))
plt.tight_layout()
plt.show()
# Criar o objeto Stitcher
# Modos disponíveis: PANORAMA (padrão) ou SCANS
stitcher = cv2.Stitcher.create(mode=cv2.Stitcher_PANORAMA)
# Realizar o stitching
# status: código de erro/sucesso
# result: imagem panorâmica resultante
(status, result) = stitcher.stitch((img1, img2))
# Verificar o resultado
if status == cv2.Stitcher_OK:
print("✅ Sucesso! Panorama criado com sucesso.")
print(f"Dimensões da imagem resultante: {result.shape}")
else:
print("❌ Falha ao criar panorama.")
print(f"Status: {status}")
# Códigos de erro possíveis:
# cv2.Stitcher_ERR_NEED_MORE_IMGS (1): Precisa de mais imagens
# cv2.Stitcher_ERR_HOMOGRAPHY_EST_FAIL (2): Falha na estimativa de homografia
# cv2.Stitcher_ERR_CAMERA_PARAMS_ADJUST_FAIL (3): Falha no ajuste de parâmetros da câmera
# Exibir o resultado
if status == cv2.Stitcher_OK:
plt.figure(figsize=(15, 10))
plt.title("Panorama resultante")
plt.imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
plt.axis('off')
plt.show()
Pós-processamento: Recortando as Bordas¶
Como podemos observar, o panorama resultante possui bordas irregulares devido ao processo de warping. Podemos realizar um recorte (crop) para obter uma imagem retangular limpa.
# Recortando as bordas da imagem panorâmica
if status == cv2.Stitcher_OK:
# Obter dimensões da imagem
h, w = result.shape[:2]
print(f"Dimensões originais: {h} x {w}")
# Método simples: recorte manual
# Ajuste estes valores conforme necessário para sua imagem específica
margin_top = int(h * 0.05) # 5% de margem superior
margin_bottom = int(h * 0.15) # 15% de margem inferior
margin_left = int(w * 0.01) # 1% de margem esquerda
margin_right = int(w * 0.01) # 1% de margem direita
crop = result[margin_top:h-margin_bottom,
margin_left:w-margin_right]
print(f"Dimensões após recorte: {crop.shape[0]} x {crop.shape[1]}")
plt.figure(figsize=(15, 10))
plt.title("Panorama recortado")
plt.imshow(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
plt.axis('off')
plt.show()
# Alternativa: Para um recorte mais preciso, você poderia:
# 1. Converter para escala de cinza
# 2. Binarizar a imagem para encontrar a região não-preta
# 3. Encontrar o retângulo de recorte otimizado
# Este é um exercício adicional que você pode implementar!