Lab05 - Espaço de cor e contorno
Espaço de Cor HSV, Segmentação e Detecção de Contornos¶
Objetivos da Aula¶
Ao final desta aula você será capaz de:
- Compreender o espaço de cor HSV e por que ele é superior ao RGB para detectar cores
- Criar máscaras de cor com
cv2.inRangepara isolar objetos por cor - Detectar e desenhar contornos de objetos com
cv2.findContours - Calcular o centro de massa e propriedades geométricas via momentos de imagem
- Conectar esses conceitos a aplicações reais: filtros de redes sociais, visão industrial e rastreamento de objetos
## 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 = 'lab05' ### 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}.")
O Problema do RGB para Detectar Cores¶
Por que não usar RGB diretamente?¶
No modelo RGB, a cor de um pixel é definida pela mistura de vermelho, verde e azul. O problema: a mesma cor real pode ter valores RGB completamente diferentes dependendo da iluminação.
Exemplo: uma bola vermelha iluminada normalmente → (200, 30, 30)
A mesma bola em luz intensa → (255, 120, 120)
A mesma bola na sombra → (90, 10, 10)
Tentar fazer if pixel == vermelho em RGB é frágil e não funciona em condições reais.
Espaço de Cor HSV¶
O HSV separa a informação de cor da informação de brilho, tornando a detecção de cor muito mais robusta.
| Canal | Nome | O que representa | Range (OpenCV) |
|---|---|---|---|
| H | Hue (matiz) | O pigmento da cor: vermelho, verde, azul... | 0–180 ⚠️ |
| S | Saturation (saturação) | Quão pura/viva é a cor (0 = cinza, 255 = cor pura) | 0–255 |
| V | Value (brilho) | Quão clara ou escura está (0 = preto, 255 = brilhante) | 0–255 |
⚠️ Atenção: Convenção do OpenCV para H¶
O Hue vai de 0° a 360° na teoria, mas o OpenCV divide esse valor por 2 para caber em 8 bits (0–255).
Então H no OpenCV vai de 0 a 180. Isso é uma fonte clássica de bug!
Cor real → H teórico → H OpenCV
Vermelho → 0° → 0
Amarelo → 60° → 30
Verde → 120° → 60
Ciano → 180° → 90
Azul → 240° → 120
Magenta → 300° → 150
Vermelho → 360° → 180
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
img = cv2.imread('hsv_colorspace.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.figure(figsize = (20,20))
plt.imshow(img); plt.show()
A matiz descreve o pigmento de uma cor e é medido em graus de 0 a 359 graus.
A saturação descreve a vivacidade ou o esmaecimento de uma cor e é medida em porcentagem de 0 a 100 (0 = cor "diluida" 100 = cor pura).
O brilho determina a intensidade percebida (0 = preto 100 = brilho maximo);
Dica: Pra entender bem o que é cada componente, da uma olhada neste link ou digita no google "colorpicker"
lembrete super importante!! a OpenCV trabalha com valores de 8bits (0-255), ou seja o valor da matriz tem que ser divido por 2
Conversão para HSV
Na OpenCV a conversão de BGR para RGB é muito simples, podemos converter diretamete da imagem em BGR usando o cv2.COLOR_BGR2HSV.
img = cv2.imread('bolinha.png')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
plt.subplot(1, 2, 1)
plt.imshow(img_rgb)
plt.subplot(1, 2, 2)
plt.imshow(img_hsv)
plt.show()
Máscaras de Cor com cv2.inRange¶
Com o HSV, criar uma máscara de cor é simples:
lower = np.array([H_min, S_min, V_min]) # limite inferior
upper = np.array([H_max, S_max, V_max]) # limite superior
mask = cv2.inRange(img_hsv, lower, upper)
O resultado é uma imagem binária: 255 onde a cor está dentro do intervalo, 0 fora.
Vermelho ocupa duas faixas no HSV (próximo de 0 e próximo de 180) porque o círculo de matizes é fechado — o vermelho está nas duas extremidades. Nesse caso, crie duas máscaras e una com
cv2.bitwise_or.
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
img = cv2.imread('bolinha.png')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# Definição dos valores minimo e max da mascara
# o magenta tem h=300 mais ou menos ou 150 para a OpenCV
image_lower_hsv = np.array([140, 100, 40])
image_upper_hsv = np.array([175, 255, 255])
mask_hsv = cv2.inRange(img_hsv, image_lower_hsv, image_upper_hsv)
plt.subplot(1, 2, 1)
plt.imshow(img_rgb)
plt.subplot(1, 2, 2)
plt.imshow(mask_hsv, cmap="Greys_r", vmin=0, vmax=255)
plt.show()
DESAFIO 1¶
Faça a segmentação da meia lua da imagem "melancia.png". O seu resultado deve ser proximo/parecido com a imagem "melancia_filtrada.png".
Dica: talvez você precise usar mais que uma faixa de valores, se necessário use a função "cv2.bitwise_or" para juntar as partes.
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
img = cv2.imread('melancia.png')
img_res = cv2.imread('melancia_filtrada.png')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_res_rgb = cv2.cvtColor(img_res, cv2.COLOR_BGR2RGB)
fig = plt.figure(figsize=(20,20))
plt.subplot(1, 2, 1)
plt.imshow(img_rgb)
plt.subplot(1, 2, 2)
plt.imshow(img_res_rgb)
plt.show()
#Implemente seu código
DESAFIO 2¶
Usando a imagem "melancia_filtrada.png", devolva a cor original que era antes da filtragem.
Dica: Use as funções de "cv2.bitwise_and" para juntar.
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
img = cv2.imread('melancia_filtrada.png')
img_res = cv2.imread('melancia_filtrada_rgb.png')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_res_rgb = cv2.cvtColor(img_res, cv2.COLOR_BGR2RGB)
fig = plt.figure(figsize=(20,20))
plt.subplot(1, 2, 1)
plt.imshow(img_rgb)
plt.subplot(1, 2, 2)
plt.imshow(img_res_rgb)
plt.show()
#Implemente seu código
Detecção de Contornos¶
Após criar uma máscara binária, podemos encontrar os contornos dos objetos segmentados.
cv2.findContours — Parâmetros importantes¶
contornos, hierarquia = cv2.findContours(
imagem_binaria, # entrada: imagem em preto e branco
modo_recuperacao, # como organizar os contornos
metodo_aproximacao # quanto detalhe guardar
)
modo_recuperacao |
O que faz |
|---|---|
RETR_EXTERNAL |
Apenas os contornos mais externos (ignora buracos) |
RETR_LIST |
Todos os contornos, sem hierarquia |
RETR_TREE |
Todos os contornos com hierarquia pai-filho |
metodo_aproximacao |
O que faz |
|---|---|
CHAIN_APPROX_NONE |
Guarda todos os pontos do contorno (pesado) |
CHAIN_APPROX_SIMPLE |
Guarda só os vértices essenciais (recomendado) |
O que retorna?¶
contornos é uma lista de arrays, onde cada array contém os pontos (x, y) de um contorno.
Para acessar o contorno mais largo: contornos[0].
Para filtrar pelo maior: max(contornos, key=cv2.contourArea).
#recarregando o nosso exemplo...
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
img = cv2.imread('bolinha.png')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# Definição dos valores minimo e max da mascara
# o magenta tem h=300 mais ou menos ou 150 para a OpenCV
image_lower_hsv = np.array([140, 50, 100])
image_upper_hsv = np.array([170, 255, 255])
mask_hsv = cv2.inRange(img_hsv, image_lower_hsv, image_upper_hsv)
# realizando o contorno da imagem
contornos, _ = cv2.findContours(mask_hsv, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# posso ordenar os contornos por área, por exemplo, para pegar o maior contorno (que provavelmente é a bolinha)
contornos = sorted(contornos, key=cv2.contourArea, reverse=True) # ordena os contornos por área, do maior para o menor
#contorno_bolinha = contornos[0] # pega o maior contorno, que provavelmente é a bolinha
# para desenhar o contorno primeiro faz uma copia da imagem
mask_rgb = cv2.cvtColor(mask_hsv, cv2.COLOR_GRAY2RGB)
contornos_img = mask_rgb.copy() # Cópia da máscara para ser desenhada "por cima"
cv2.drawContours(contornos_img, contornos, -1, [255, 0, 0], 5)
plt.figure(figsize=(8,6))
plt.imshow(contornos_img)
plt.show()
# para desenhar o contorno primeiro faz uma copia da imagem
mask_rgb = cv2.cvtColor(mask_hsv, cv2.COLOR_GRAY2RGB)
contornos_img = mask_rgb.copy() # Cópia da máscara para ser desenhada "por cima"
cv2.drawContours(img_rgb, contornos, -1, [0, 0, 255], 10);
plt.figure(figsize=(8,6))
plt.imshow(img_rgb);
Note que a função findContours devolve uma lista com os contornos detectados.
print(f"Número de contornos encontrados: {len(contornos)}")
for i, cnt in enumerate(contornos):
area = cv2.contourArea(cnt) # calcula a área do contorno
perimetro = cv2.arcLength(cnt, closed=True) # calcula o perímetro do contorno
print(f" Contorno {i}: {len(cnt)} pontos | Área = {area:.0f} px² | Perímetro = {perimetro:.0f} px")
📊 Número de contornos encontrados: 1 Contorno 0: 216 pontos | Área = 20954 px² | Perímetro = 543 px
Desafio 2 — Inspeção de Qualidade Industrial¶
Contexto: Você está desenvolvendo um sistema de visão para uma esteira de produção. O sistema precisa identificar quantos objetos passaram, qual o maior e o menor, e sinalizar objetos "fora do padrão" de tamanho.
Objetivo: Analisar formas.png como se fosse uma imagem da esteira.
Instruções:
- Detecte todos os contornos da imagem
img_formas - Para cada contorno, calcule: área, perímetro e razão de aspecto (
largura / alturado bounding box) - Defina um critério de "fora do padrão": qualquer objeto com área < 2000 px² ou > 15000 px²
- Desenhe os contornos em verde para os aprovados e vermelho para os reprovados
- Adicione um texto na imagem com
cv2.putText:"OK"ou"FORA"no centroide de cada objeto - Exiba a imagem final e um relatório textual no terminal
Dica: cv2.boundingRect(cnt) retorna (x, y, w, h) — largura e altura do bounding box.
%matplotlib inline
import cv2
from matplotlib import pyplot as plt
import numpy as np
img = cv2.imread('formas.png')
#### seu código aqui
# exibe os resultados
fig = plt.figure(figsize=(20,20))
plt.subplot(1, 2, 1)
plt.imshow(img_rgb)
plt.subplot(1, 2, 2)
plt.imshow(img_res_rgb)
plt.show()
Momentos de Imagem e Propriedades Geométricas¶
Os momentos de imagem são estatísticas calculadas sobre a distribuição de pixels em um contorno.
A partir deles, podemos extrair propriedades geométricas fundamentais.
A Matemática¶
O momento de ordem $(p, q)$ é definido como:
$$m_{pq} = \sum_x \sum_y x^p \cdot y^q \cdot I(x, y)$$
Onde $I(x, y)$ é a intensidade do pixel (na máscara binária, vale 1 ou 0).
Com isso calculamos:
| Propriedade | Fórmula | Significado |
|---|---|---|
| Área | $m_{00}$ | Número de pixels no objeto |
| Centróide X | $m_{10} / m_{00}$ | Posição horizontal do centro de massa |
| Centróide Y | $m_{01} / m_{00}$ | Posição vertical do centro de massa |
| Circularidade | $4\pi \cdot A / P^2$ | 1.0 = círculo perfeito, < 1 = forma irregular |
| Solidez | $A_{contorno} / A_{convex\ hull}$ | 1.0 = forma convexa, < 1 = formas côncavas/irregulares |
cv2.moments(contorno)retorna um dicionário com todos os momentos até ordem 3 calculados de uma vez.
# usando o exemplo da documentação https://docs.opencv.org/master/dd/d49/tutorial_py_contour_features.html
# notamos que a função devolve um dicionario.
cnt = contornos[0]
M = cv2.moments(cnt)
print( M )
{'m00': 20954.5, 'm10': 8414785.5, 'm01': 4813317.5, 'm20': 3414210172.083333, 'm11': 1932427931.625, 'm02': 1140509199.0833333, 'm30': 1399201096317.6501, 'm21': 783868070420.7833, 'm12': 457787216119.7167, 'm03': 278003040454.85004, 'mu20': 35049847.99971116, 'mu11': -475946.1051416383, 'mu02': 34874354.262113385, 'mu30': -7672941.921116273, 'mu21': -4968840.38085369, 'mu12': 6858123.621494233, 'mu03': 2822544.575257195, 'nu20': 0.07982364109512692, 'nu11': -0.0010839348312655381, 'nu02': 0.07942396606302514, 'nu30': -0.0001207170613283252, 'nu21': -7.817390189484327e-05, 'nu12': 0.0001078976666739466, 'nu03': 4.440660311210652e-05}
# Calculo das coordenadas do centro de massa
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
print("centro de massa na possição: ",cx, cy)
centro de massa na possição: 401 229
Vamos plotar isso na imagem para saber se esta correto. A função "cv2.line" vai nos ajudar a desenhar uma cruz. e função "cv2.putText" a escrever na imagem as coordenadas.
## para desenhar a cruz vamos passar a cor e o tamanho em pixel
size = 20
color = (128,128,0)
cv2.line(contornos_img,(cx - size,cy),(cx + size,cy),color,5)
cv2.line(contornos_img,(cx,cy - size),(cx, cy + size),color,5)
# Para escrever vamos definir uma fonte
font = cv2.FONT_HERSHEY_SIMPLEX
text = cy , cx
origem = (0,50)
cv2.putText(contornos_img, str(text), origem, font,1,(200,50,0),2,cv2.LINE_AA)
plt.imshow(contornos_img);
DESAFIO 5¶
O desafio é juntar o que aprendemos em um video, use como base "webcam.py". Você deve seguimentar a cor de um objeto, encontrar seu contorno e montar a imagem segmentada com o centro de massa e suas coordenadas. Video de referência "segmenta_melancia.mp4"
#### seu código aqui...