terça-feira, 18 de agosto de 2015

Criando um Círculo de Cores HSV

Como descrito na última postagem, o HSV é um sistema de cores composto por três componentes. Neste post, será descrita a criação de um círculo de cores, gerado através de variações das combinações dessas componentes. Para tal, será utilizada a linguagem Python, juntamente com as bibliotecas Numpy e OpenCV.

A função para criação do círculo recebe dois parâmetros: tamanho da imagem a ser gerada e o raio do círculo.

 import numpy as np  
 import cv2  
   
 def geraCirculoHSV(tam, raio):  
   

Para construção da imagem será preciso montar cada banda separadamente e depois juntá-las em uma estrutura tridimensional.


Hue

A primeira banda, Hue ou matiz, contém a medida do comprimento de onda médio da luz que ele reflete ou emite. Define a cor ou tonalidade do objeto (vermelho, laranja, amarelo, etc). Os valores variam de 0º à 360º.

O círculo de cores irá variar todo o diagrama de cores, do vermelho ao vermelho (cor representada tanto pelo valor mínimo 0, quanto pelo valor máximo 360). É preciso representar cada ponto da imagem, portanto é definida uma matriz de duas dimensões para tal. O valor de cada ponto P pode ser encontrado através do cálculo ângulo entre o ponto P e a reta que uma reta que corte o círculo verticalmente, passando pelo centro.

Para substituir o processo de percorrer a matriz do modo tradicional, é utilizada a função "numpy.indices", que gera duas matrizes (aqui denominadas "ll" e "cc") representando índices de linha e coluna. Se executarmos, por exemplo, o código a seguir:

 ll, cc = np.indices((5,5))  
   
 print "ll:"  
 print ll  
 print  
 print "cc:"  
 print cc  

Teremos a seguinte saída:

 ll:  
 [[0 0 0 0 0]  
  [1 1 1 1 1]  
  [2 2 2 2 2]  
  [3 3 3 3 3]
  [4 4 4 4 4]]  
   
 cc:  
 [[0 1 2 3 4]  
  [0 1 2 3 4]  
  [0 1 2 3 4]  
  [0 1 2 3 4]
  [0 1 2 3 4]  

Para o cálculo da Matiz, ou do ângulo correspondente à cada ponto, é preciso as coordenadas do ponto. Com as matrizes de índices, já temos essa informação. O que precisa ser mudado é o centro da imagem. De acordo com as matrizes de índices, o centro (0,0) estaria na primeira posição, porém, precisamos que o centro corresponda com o centro da imagem (ou do círculo). Dessa forma, é feita uma translação. O valor dessa translação é obtido dividindo o tamanho da imagem por dois.

   div = np.floor(tam/2)  
     
   ll,cc = np.indices((tam,tam))  
   
   ll = ll - div    
   cc = cc - div  

Agora temos duas matrizes transladadas. No caso do exemplo numérico anterior, teríamos:

 ll:  
 [[-2 -2 -2 -2 -2]  
  [-1 -1 -1 -1 -1]  
  [ 0 0 0 0 0]  
  [ 1 1 1 1 1]  
  [ 2 2 2 2 2]]  
   
 cc:  
 [[-2 -1 0 1 2]  
  [-2 -1 0 1 2]  
  [-2 -1 0 1 2]  
  [-2 -1 0 1 2]  
  [-2 -1 0 1 2]]  

O ângulo entre um ponto e uma reta é obtido calculando arcotangente de X / Y, onde X e Y são as coordenadas do ponto. Utilizando a função "numpy,arctan", temos um resultado em radianos, que é convertido para graus através da multiplicação por 180 / Pi.

 hh = np.arctan(cc/ll) * 180/np.pi  

Apenas essas operações gerariam a seguinte imagem:


Portanto, é preciso fazer a correção da metade inferior. Isso é feito somando 180 à essa parte da imagem, através de mecanismos simples de amostragem de matrizes utilizados pelo Python.

 hh[-1:div-1:-1,::] += 180  

Agora temos na matriz "hh" todas as informações da matiz (não se preocupe ainda com a delimitação do círculo, isso acontecerá com a adição das outras bandas):



Saturation

A segunda banda, Saturation ou Saturação, é responsável pela profundidade ou "pureza" da cor (de esmaecida à intensa). Diferente do que intuitivamente pensaríamos, quanto mais saturada uma cor, mais "pura" ela será e, quanto menos saturada, mais próxima da cor branca. O valor da saturação varia entre 0 e 100%, ou entre 0 e 1.

Para calcular a saturação, utilizaremos o valor do raio do círculo. O ponto central da imagem possui valor 0 (não saturado) e os pontos ao redor dele, a medida que nos aproximamos da borda, vão aumentando de valor até atingir 1 (completamente saturado) na borda do círculo. Podemos perceber, então, que a saturação irá depender da distância entre o ponto e o centro do círculo, calculada através da fórmula da distância euclidiana. Como nesse caso um dos pontos é o (0,0), não é preciso fazer a subtração entre as coordenadas.

   ll,cc = np.indices((tam,tam))  
   ll = ll - div  
   cc = cc - div  
     
   ll = np.square(ll)  
   cc = np.square(cc)    
   ss = np.sqrt(ll+cc)
   ss = np.clip(ss,0,raio)  

Para simplificar a operação, foi gerada as matrizes de índices, como no caso da matiz, e o resultado dos quadrados foi mantido nas próprias matrizes. A operação de "clip" realizada na matriz serve para realizar a delimitação do círculo. Essa operação, "numpy.clip(M,a,b)",  substitui todos os valores menores que "a" contidos na matriz M pelo valor de "a", e todos os valores maiores que "b", pelo valor de "b".  Como as distâncias entre o ponto e o centro devem variar entre 0 e o valor do raio, todos os valores acima disso serão substituídos pelo valor do raio.

Agora é preciso normalizar os valores. A matriz  está variando de 0 ao raio, mas precisamos que varie de 0 à 1. Assim sendo, basta dividir o valor de cada ponto pelo valor do raio.

   ss = ss/raio  

Juntas as bandas de Hue e Saturation temos a imagem:



Value

A terceira banda, Value ou Valor, é responsável por definir o brilho da cor, ou a intensidade com que é percebida (mais clara ou mais escura). Os valores dessa componente variam entre 0 e 100%. A questão nesse exemplo é que estamos trabalhando com uma imagem bidimensional, não sendo possível descrever o que seria a altura do cilindro de cores que representa o sistema HSV. Consideremos então que esse círculo seja o topo do cilindro, onde o brilho tem valor 1 em seu interior e valor 0 no exterior.

Para delimitar o círculo desta maneira, basta lembrar que já temos as informações necessárias na matriz de saturação. Então é criada uma matriz do tamanho da imagem preenchida com o valor 1 e, em todas as posições da matriz onde a posição correspondente na matriz de saturação possuir valor 1, é colocado valor 0.

   vv = np.ones((tam,tam))  
   vv[ss==1] = 0  

Pronto!!!

Temos as três bandas que compõem a imagem e o seguinte resultado:



Para compor esse resultado, entretanto, é necessário juntar as bandas de maneira adequada, bem como converter as matrizes para o formato numérico apropriado para a exibição.

   hh = np.float32(hh)  
   ss = np.float32(ss)  
   vv = np.float32(vv)  
     
   hsv = np.dstack((np.dstack((hh,ss)),vv))  
   bgr_img = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)  
     
   cv2.imshow("Circulo",bgr_img)  
   cv2.waitKey(0)  
   cv2.destroyAllWindows()  

No código acima é feita (depois das conversões) a junção em profundidade da banda Hue com a a Saturação e, o resultado disto, com o Valor. A imagem então é convertida para o formato BGR e exibida através dos comandos OpenCV.

Dica: Não recrie sempre as matrizes, reutilize as operações já realizadas para otimizar o processamento.

Código completo (com os valores utilizados para gerar as imagens acima):

 import numpy as np  
 import cv2  
   
 def geraCirculoHSV(tam, raio):  
     
 ##HUE    
     
   #Criação das matrizes de índices    
   ll,cc = np.indices((tam,tam))    
   div = np.floor(tam/2)  
   
   #Translação  
   ll = ll - div  
   cc = cc - div  
     
   #Cálculo do ângulo    
   hh = np.arctan(cc/ll) * 180/np.pi  
     
   #Adição de 180 graus nos valores da metade inferior da imagem  
   hh[-1:div-1:-1,::] += 180  
     
 ##Saturação  
   
   #Criação das matrizes de índices    
   ll,cc = np.indices((tam,tam))  
     
   #Translação  
   ll = ll - div  
   cc = cc - div  
     
   #Cálculo da distância entre o ponto e o centro  
   ll = np.square(ll)  
   cc = np.square(cc)    
   ss = np.sqrt(ll+cc)  
     
   #Delimitando os valores da matriz para a faixa entre 0 e raio  
   ss = np.clip(ss,0,raio)  
   
   #Normalização  
   ss = ss/raio    
     
 ##VALOR  
     
   #Criação da matriz de uns  
   vv = np.ones((tam,tam))  
     
   #Seta para 0 as posições em vv onde as correspondentes em ss são 1  
   vv[ss==1] = 0  
     
   #Conversção de tipos  
   hh = np.float32(hh)  
   ss = np.float32(ss)  
   vv = np.float32(vv)  
     
   #Junção das bandas  
   hsv = np.dstack((np.dstack((hh,ss)),vv))  
     
   #Converção para o formato BGR  
   bgr_img = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR)  
     
   #Exibição da imagem  
   cv2.imshow("Circulo",bgr_img)  
   cv2.waitKey(0)  
   cv2.destroyAllWindows()  
       
 geraCirculoHSV(700,300)  

0 comentários:

Postar um comentário