• Unidade 2
  • Capítulo 5 - Visão Robótica
  • Atividades
  • 2 - Identificação de Objetos

Identificação de Objetos

Nesta atividade, vamos explorar como identificar objetos encontrando seus contornos a partir de segmentação de cores.

Detecção de contornos é uma técnica importante em visão computacional. A ideia é que os contornos são as bordas dos objetos, e portanto, podem ser usados para detectar a localização de objetos, além de outras métricas como centro, área, perímetro, etc.

Fonte das imagens: http://time.com/4299724/coca-cola-diet-coke-redesign/

Componentes conexos e contornos

Neste exercicio vamos trabalhar na segunte imagem. Nosso objetivo é encontrar os contornos da parte vermelha das latinhas de coca-cola.

image.png

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread("img/coke-cans.jpg")
coke_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
Matplotlib is building the font cache; this may take a moment.

Filtrando o objeto

Revisando o círculo de cores HSV, temos:

Conforme ilustrado acima, a cor vermelha aparece em duas regiões distintas do círculo HSV: uma entre 0 e 30 graus e outra entre 330 e 360 graus.

Uma limitação do OpenCV é sua incapacidade de realizar seleções em intervalos não contínuos diretamente, como seria necessário para a cor vermelha (-30 a 30 graus). Isso representa um desafio na segmentação eficaz do vermelho.

No entanto, uma abordagem viável é realizar duas seleções de intervalos separadas para cada faixa do vermelho, aplicar a segmentação em ambas e, posteriormente, unir as duas máscaras resultantes. Esta técnica permite a segmentação eficiente da cor vermelha, contornando a restrição de seleção de intervalos do OpenCV.

lembrando que o valor HSV na OpenCV fica adaptado para:

Hue (H): No espaço de cores HSV original, o matiz varia de 0 a 360 graus. No entanto, na OpenCV, esse intervalo é mapeado para 0 a 180. Portanto, para converter um valor de matiz do espaço de cores HSV padrão para o utilizado pela OpenCV, você deve dividir o valor por 2 (H/2).

Saturation (S): A saturação no espaço de cores HSV padrão varia de 0 a 100% (representando a intensidade da cor). Na OpenCV, esse intervalo é mapeado para 0 a 255. Portanto, para converter um valor de saturação do espaço de cores HSV padrão para o utilizado pela OpenCV, você pode usar a seguinte regra de três: (valor de saturação * 255) / 100.

Value (V): O valor (ou brilho) no espaço de cores HSV padrão também varia de 0 a 100%. Na OpenCV, assim como a saturação, esse intervalo é mapeado para 0 a 255. A conversão é feita da mesma maneira que a saturação: (valor de brilho * 255) / 100.

Abaixo, temos o código para realizar a segmentação da cor vermelha, neste código, combinamos as duas máscaras resultantes utilizando o operador lógico OR do OpenCV, que retorna branco se pelo menos um dos pixels for branco. A função cv2.bitwise_or recebe os seguintes parâmetros:

  • cv2.bitwise_or(src, src2, **mask)
    • src1: primeira imagem, ou máscara, de entrada ou matriz real.
    • src2: segunda imagem, ou máscara, de entrada ou matriz real.
    • mask (opcional): máscara de entrada de 8 bits. A operação será realizada apenas nos elementos especificados pela máscara.

Também temos o operator lógico AND, que retorna branco se ambos os pixels forem brancos, na função cv2.bitwise_and.

Depois da execução do código abaixo, podemos notar que a segmentação resultante está muito boa, mas ainda há alguns ruídos.

#                       H , S , V 
cor_menor1 = np.array([172, 50, 50])
cor_maior1 = np.array([180, 255, 255])
mask_coke1 = cv2.inRange(coke_hsv, cor_menor1, cor_maior1)

cor_menor2 = np.array([0, 50, 50])
cor_maior2 = np.array([8, 255, 255])
mask_coke2 = cv2.inRange(coke_hsv, cor_menor2, cor_maior2)

mask_coke = cv2.bitwise_or(mask_coke1, mask_coke2)

cv2.imshow("image", img)
cv2.imshow("mask 1", mask_coke1)
cv2.imshow("mask 2", mask_coke2)
cv2.imshow("mask", mask_coke)
cv2.waitKey()
cv2.destroyAllWindows()

Componentes conexos

Após a segmentação da imagem por mascaramento, podemos observar que os pixels de interesse podem formar um ou mais grupos conectados entre si, ou seja, conjuntos de pixels que se comunicam através de algum caminho que passa apenas por pixels de interesse (brancos)

No OpenCV, é possível encontrar componente conexos em imagens tons de cinza através da função cv2.findContours(). Ela considera pixels de valor maior do que 0 como pixels de interesse.

Usamos a seguinte função para encontrar os contornos:

contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

onde: * mask é a imagem com a máscara binária de entrada. * cv2.RETR_CCOMP indica que queremos organizar os contornos em componentes conexos e buracos dentro deles - veja mais detalhes em Contours Hierarchy. * cv2.CHAIN_APPROX_NONE indica que queremos armazenar todos os pontos do contorno. * contours é uma lista de contornos. Cada contorno é uma lista de pontos (x, y) que formam o polígono que delimita o contorno. * hierarchy é uma lista indicando a organização dos contornos em termos dos componentes e de seus buracos.

Os componentes conexos são representados através de seus contornos internos, ou seja, dos pixels de cada componente conexo que são vizinhos a pixels de fundo. Para desenhar os contornos em uma imagem, usamos a função cv2.drawContours(), que usamos da forma:

cv2.drawContours(img, contours, indice, cor, espessura)
  • img é a imagem colorida ou tons de cinza onde serão desenhados os contornos.
  • contours é a lista de contornos obtida com cv2.findContours(), ou seja, recebe uma lista de lista. Então assumindo que contours[i] seja um contorno, a função esperaria uma sintaxe como cv2.drawContours(img, [contours[i]], indice, cor).
  • indice é o índice do contorno dentro da lista a ser desenhado; se -1 desenha todos os contornos
  • cor é a cor do pixel a ser usada para desenhar o contorno, por exemplo, (255, 0, 0) para azul.
  • espessura é a espessura do contorno. Se for -1, o contorno é preenchido com a cor.
contornos, arvore = cv2.findContours(mask_coke.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) 
print(f'Numero de Contornos Encontrados: {len(contornos)}')

contornos_img = img.copy()
cv2.drawContours(contornos_img, contornos, -1, [255, 0, 0], 3)

cv2.imshow("contornos_img", contornos_img)
cv2.waitKey()
cv2.destroyAllWindows()
Numero de Contornos Encontrados: 111

Medidas dos contornos

A partir dos contornos, podemos tirar uma série de medidas como: - Área: número de pixels pertencentes ao contorno, calculada com cv2.contourArea(contour) - Centro de massa: linha e coluna do centro de massa do contorno - Caixa delimitadora: menor retângulo que contém o contorno, calculada com cv2.boundingRect(contour)

Maior contorno

Utilizando a função cv2.contourArea() podemos calcular a área de cada contorno e assim encontrar o maior contorno.

Na célula abaixo, apresentamos duas formas de encontrar o maior contorno dentre os contornos encontrados na imagem. Repare como a segunda forma é mais performática comparada a primeira.

import time

## Utilizando laço
start = time.perf_counter()
maior = None
maior_area = 0
for c in contornos:
    area = cv2.contourArea(c)
    if area > maior_area:
        maior_area = area
        maior = c

print(f"Tempo de execução: {time.perf_counter() - start:.5f}s")

## Utilizando max e key
start = time.perf_counter()
maior = max(contornos, key=cv2.contourArea)
print(f"Tempo de execução: {time.perf_counter() - start:.5f}s")
Tempo de execução: 0.00154s
Tempo de execução: 0.00037s

contornos_img = img.copy()
cv2.drawContours(contornos_img, [maior], -1, [255, 0, 0], 3)

cv2.imshow("contornos_img", contornos_img)
cv2.waitKey()
cv2.destroyAllWindows()

Centro de massa do contorno

O centro de massa de um contorno é calculado através da função cv2.moments(contour), que retorna um dicionário com as seguintes chaves:

  • m00: área do contorno
  • m10: soma das coordenadas x dos pixels do contorno
  • m01: soma das coordenadas y dos pixels do contorno

Essas chaves são usadas para calcular o centro de massa do contorno, que é dado por:

cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])

Além disso, a função cv2.moments(contour) também retorna outras chaves que podem ser usadas para calcular outras medidas do contorno. Essas chaves não serão usadas no curso, mas são apresentadas abaixo para referência:

  • m20: soma das coordenadas x^2 dos pixels do contorno
  • m11: soma das coordenadas x*y dos pixels do contorno
  • m02: soma das coordenadas y^2 dos pixels do contorno
  • m30: soma das coordenadas x^3 dos pixels do contorno
  • m21: soma das coordenadas x^2*y dos pixels do contorno
  • m12: soma das coordenadas x*y^2 dos pixels do contorno
  • m03: soma das coordenadas y^3 dos pixels do contorno
def crosshair(img, point, size, color):
    """ Desenha um crosshair centrado no point.
        point deve ser uma tupla (x,y)
        color é uma tupla R,G,B uint8
    """
    x,y = point
    cv2.line(img,(x - size,y),(x + size,y),color,5)
    cv2.line(img,(x,y - size),(x, y + size),color,5)

    return img

""" Retorna uma tupla (cx, cy) que desenha o centro do contorno"""
M = cv2.moments(maior)
# Usando a expressão do centróide definida em: https://en.wikipedia.org/wiki/Image_moment
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])

contornos_img = crosshair(contornos_img, (cX,cY), 10, (255,0,0))

cv2.imshow("contornos_img", contornos_img)
cv2.waitKey()
cv2.destroyAllWindows()

Caixa delimitadora

Em visão computacional, uma caixa delimitadora é um retângulo que delimita as coordenadas de um objeto. A caixa delimitadora é definida pelas coordenadas de seu canto superior esquerdo e sua largura e altura.

Vamos calcular a caixa delimitadora da maior latinha de refrigerante na imagem.

# get bounding rect
x, y, w, h = cv2.boundingRect(maior)

cv2.rectangle(contornos_img, (x, y), (x+w, y+h), (0, 255, 0), 2)

cv2.imshow("contornos_img", contornos_img)
cv2.waitKey()
cv2.destroyAllWindows()

Prática

Prática 5.1

Neste exercício, vamos encontrar os contornos das 4 latinhas de Coca-Cola e depois filtrar apenas a latinha da Coca-Cola Life (a que tem a parte verde no topo).

Dica: Pode utilizar a função sorted para ordenar os contornos por área, use o método que foi utilizado para obter a maior área como referência.

Dica: Use as funções de contornos para analisar cada latinha individualmente. Depois disso, veja se a latinha contém uma quantidade "grande" de pixels verdes.

Crie um arquivo chamado latinhas.py que contenha uma classe chamada ProcessImage com as seguintes especificações:

Dica: Pode testar adiantando o método run_image

Classe ProcessImage

Método filter_bw

  • Recebe uma imagem como entrada, o limite inferior e o limite superior.
  • Converte a imagem para o espaço de cores GRAY.
  • Filtra a imagem e aplica ajustes de morfologia para remover ruídos.
  • Encontra os contornos da máscara resultante.
  • Retorna os contornos encontrados.

Método filter_hsv

  • Recebe uma imagem como entrada, o limite inferior e o limite superior.
  • Converte a imagem para o espaço de cores HSV.
  • Filtra a imagem e aplica ajustes de morfologia para remover ruídos.
  • Encontra os contornos da máscara resultante.
  • Retorna os contornos encontrados.

Método get_center

  • Recebe uma lista de contornos.
  • Calcula o centro de massa de cada contorno e armazene o como uma tupla, (cx,cy), em uma lista.
  • Retorna a lista de centros de massa.

Método find_latinhas

  • Recebe uma imagem e uma lista de contornos
  • Filtra os contornos para encontrar apenas os contornos das 4 latinhas. (vide dicas)
  • Retorna a imagem com os contornos desenhados e os contornos das latinhas.

Método find_latinha_life

  • Recebe uma imagem com os contornos das latinhas, os contornos das 4 latinhas.
  • Chama o método get_center para encontrar o centro de massa de cada latinha.
  • Chama o método filter_hsv para encontrar o contorno da parte verde da Coca-Cola Life.
  • Filtra apenas o maior contorno encontrado.
  • Chama o método get_center para encontrar o centro de massa do contorno.
  • Em um loop, compara a distância entre o centro de massa da parte verde e o centro de massa de cada latinha.
  • A menor distância será da latinha da Coca-Cola Life.
  • Retorna a imagem com os contornos desenhados e a latinha da Coca-Cola Life.

Método run_image

  • Recebe uma imagem como entrada.
  • Chama o método filter_bw para encontrar o os contornors das latinhas (e tambem de ruido)
  • Chama o método find_latinhas para encontrar os contornos das latinhas, removendo o ruido.
  • Chama o método find_latinha_life para encontrar o contorno da Coca-Cola Life.
  • Armazena a imagem resultante em uma variável da classe chamada self.bgr.

Método show_image

  • Exibe a imagem processada.

Função main

  • Carrega uma das três imagens das latinhas (coke-cans.jpg, coke-cans2.jpg, coke-cans3.jpg).
  • Utiliza o método run_image para processar a imagem capturada.
  • Chama o método show_image para exibir a imagem processada.