Por que funciona: Unidade 1.3: A imagem como dado

Achar a faixa HSV de um objeto no OpenCV

Leia os pixels de uma mesa de sinuca em HSV com cv2.cvtColor e ache a faixa que o pano ocupa: o valor de verde cai quase dez vezes entre a ponta iluminada e uma sombra, enquanto o matiz fica entre 86 e 90.

Luis Condados · · 5 min de leitura Iniciante

Tradução assistida por máquina do original em inglês. Números, código e referências foram conferidos com ele; o texto não foi revisado por um falante nativo. Ler o original. Os labs interativos e o código estão em inglês.

O canal de matiz da foto da sinuca: o pano é um único cinza liso.
O canal de matiz da foto da sinuca: o pano é um único cinza liso.

Para separar um objeto de uma foto pela cor, você precisa da faixa de números que essa cor ocupa, e o jeito mais rápido de achá-la é ler alguns dos seus pixels em HSV.

A foto da mesa de sinuca com quatro pontos de amostra circulados, ao lado do seu canal de matiz, onde o pano inteiro é um único cinza liso e as bolas se destacam.
À esquerda: a foto, com os quatro pontos que esta lição lê circulados. À direita: só o canal de matiz, esticado para 0–255 para exibição. O pano é um único cinza liso da ponta próxima até a tabela do fundo, e a sala escura no alto é ruído. Foto “Billiards table 2.JPG” de MarkBuckawicki, CC0 [4].

Onde você já viu isso: o filtro de chroma key (tela verde) do OBS e dos editores de vídeo; Selecionar › Por cor no GIMP e Intervalo de cores no Photoshop; as máquinas de triagem óptica que descartam grãos e frutas fora da cor.

TL;DR. Uma cor parece a mesma para nós na luz e na sombra, mas os números que a câmera guarda para ela mudam muito, e um deles, o matiz, quase não se mexe, então é esse o número a testar. Na mesa de sinuca o valor de verde do pano cai quase dez vezes, de 201 na ponta iluminada para 21 na sombra de uma bola, enquanto o matiz fica entre 86 e 90 na escala 0–179 do OpenCV.

O problema

Este módulo acha todas as bolas numa mesa de sinuca (o projeto). As bolas vêm em todas as cores, mas o pano embaixo delas tem uma só, então o plano é achar o pano e ficar com o que não é pano. Para isso é preciso uma regra para “pano” que um computador consiga testar em cada pixel.

Leia os pixels em HSV

cv2.cvtColor converte uma imagem inteira de um espaço de cor para outro. Com COLOR_BGR2HSV cada pixel vira três números novos: matiz (qual cor, 0–179), saturação (quão pura, 0–255) e valor (quão clara, 0–255) [1].

Python · OpenCV
import cv2
import numpy as np

img = cv2.imread("pool.webp")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# (x, y) de quatro pontos da foto
points = {"cloth, near": (400, 240),
          "cloth, far": (700, 95),
          "shadow": (790, 336),
          "green ball": (727, 155)}
for name, (x, y) in points.items():
    print(f"{name:12} BGR {img[y, x]}  HSV {hsv[y, x]}")
Saída
cloth, near  BGR [198 201   9]  HSV [ 90 244 201]
cloth, far   BGR [32 37  2]  HSV [ 86 241  37]
shadow       BGR [18 21  0]  HSV [ 86 255  21]
green ball   BGR [ 86 117   5]  HSV [ 82 244 117]
ChamadaO que faz
cv2.cvtColor(img, cv2.COLOR_BGR2HSV)uma imagem nova do mesmo tamanho, com H, S e V no lugar de B, G e R
img[y, x]os três números de um pixel; primeiro a linha (y), depois a coluna (x)

Os três pontos do pano parecem uma cor só para nós. Em BGR eles ficam longe um do outro: o canal verde cai de 201 perto da luminária para 37 na ponta do fundo e 21 na sombra da bola branca, e azul e verde caem juntos, então nenhuma faixa única em nenhum canal cobre os três sem cobrir também a maior parte das outras cores. Em HSV o matiz é 90, 86 e 86 e só o valor muda. É nessa propriedade que a máscara vai se apoiar: o brilho tem seu próprio número, então uma faixa no matiz ignora luz e sombra [2].

A bola verde é o aviso. O matiz dela é 82, a quatro passos do pano do fundo. Uma faixa larga o bastante para o pano em qualquer luz pode engoli-la; a lição 5 conta quanto isso custa.

Ache a faixa numa região

Um pixel só pode dar azar. O canto inferior direito da foto é só pano, então a dispersão dele é a faixa por onde começar:

Python · OpenCV
# uma região da foto que é só pano
patch = hsv[550:, 650:]
for i, name in enumerate(["H", "S", "V"]):
    lo, hi = np.percentile(patch[..., i], [2, 98])
    print(f"{name}: {lo:.0f} to {hi:.0f}")
cv2.imshow("hue (0-179)", hsv[..., 0])
Saída
H: 85 to 88
S: 233 to 253
V: 118 to 163
hue (0-179)
hue (0-179)

Os percentis descartam os 2% extremos de cada ponta, para que um cisco perdido não defina a faixa. Nesta região o matiz vai de 85 a 88 e o valor de 118 a 163. A ponta do fundo da mesa e as sombras são mais escuras que qualquer coisa na região (o pano do fundo estava em 37), então a faixa de valor tem de descer bem abaixo de 118. O módulo começa com uma janela folgada, que a lição 2 transforma numa máscara:

HSV
inferior758040
superior100255255

A imagem do matiz mostra o pano como um só cinza de frente a fundo. A sala preta acima da mesa é ruído: quando um pixel é quase preto, os três canais estão todos perto de 0 e a fórmula divide diferenças pequenas por um número pequeno, então o matiz dele é quase aleatório [2].

Quebre

cvtColor confia no flag que você passa. cv2.imread devolve BGR, e o flag para isso é COLOR_BGR2HSV. Passe COLOR_RGB2HSV e vermelho e azul são trocados antes da conversão:

Quebre
# RGB2HSV numa imagem BGR: vermelho e azul trocados
wrong = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
x, y = points["cloth, near"]
print("right:", hsv[y, x], " wrong:", wrong[y, x])
cv2.imshow("hue, wrong flag", wrong[..., 0])
Saída
right: [ 90 244 201]  wrong: [ 30 244 201]
hue, wrong flag
hue, wrong flag

O matiz do pano sai como 30, que é amarelo na escala do OpenCV, e uma máscara construída sobre ele não selecionaria nada na mesa. Não há erro para te avisar.

Sua vez

Leia a bola roxa em x = 414, y = 109 e guarde o matiz dela em hue.

Sua vez
hue = None  # substitua esta linha
print(hue)

Por que funciona

Uma câmera registra quanto de luz vermelha, verde e azul chegou a cada pixel, então uma superfície na sombra dá valores menores nos três canais ao mesmo tempo. O HSV toma o maior dos três como valor e descreve a cor pela posição dos três uns em relação aos outros, que não muda quando os três encolhem juntos [2]. Gonzalez e Woods derivam a mesma ideia para o modelo HSI, muito próximo, e o usam para segmentar por cor [3]. Colour spaces: choosing axes you can threshold (em inglês) faz a conversão passo a passo num único patch medido de uma carta de cores, e how close is your colour (em inglês) mede o quanto as cores de uma câmera se afastam das reais.

Erros comuns

  • Matiz em graus. Seletores de cor dão o matiz em 0–360. O OpenCV divide por dois para caber num byte: 0–179 para imagens de 8 bits [2]. O pano em 88 fica em cerca de 176 graus no GIMP.
  • O flag de conversão errado. imread devolve BGR; use COLOR_BGR2HSV, como a célula Quebre mostra.
  • Confiar no matiz de pixels escuros ou cinzentos. Quando a saturação ou o valor estão perto de 0, o matiz é ruído. A bola branca, branco puro, tem saturação 11: o matiz dela não diz nada.
  • O vermelho dá a volta. O vermelho fica nas duas pontas da escala de matiz, perto de 0 e perto de 179, então uma faixa só não o contém. Colour spaces mostra a correção com duas máscaras.
  • Ler um pixel só. Pegue uma região e seus percentis, e olhe tanto a parte iluminada quanto a sombreada.

Referências

[1] OpenCV. Changing Colorspaces (Python tutorial, doc/py_tutorials/py_imgproc/py_colorspaces/py_colorspaces.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv

[2] OpenCV. Color conversions, section “RGB ↔ HSV” (modules/imgproc/doc/colors.markdown). OpenCV 4.11.0, commit 31b0eee. github.com/opencv/opencv

[3] Gonzalez, R. C., & Woods, R. E. (2018). Digital Image Processing (4th ed.), section 7.2 “Color Models”, The HSI Color Model, p. 541, and section 7.7 “Using Color in Image Segmentation”, p. 575. Pearson.

[4] MarkBuckawicki. Billiards table 2.JPG (27 Feb 2014). Wikimedia Commons, CC0 1.0. commons.wikimedia.org