Reconocimiento de piezas de distintos colores (I)

2010 LEGO color paletteEn el artículo anterior vimos como, teniendo un conjunto de colores, hallar la distancia mínima entre ellos, lo que nos sirve para calcular posteriormente un umbral aceptable que nos sirve para distinguir distintos colores por su RGB. En este artículo vamos a aplicar estos conocimientos a un programa que sea capaz de detectar varias piezas de diferentes colores, algunos de ellos muy parecidos. Si fueramos capaces de distinguir una pieza de determinado color entre piezas de más de diez colores distintos sería una gran hazaña, pues significaría que el programa tendría muchas aplicaciones.

Búsqueda del valor umbral:

Antes que nada necesitamos especificar el valor umbral que determina si un píxel pertenece a una pieza de un color u otro. Partimos de la distancia mínima entre dos colores, sabiendo que ésta se ha calculado como la distancia entre dos puntos en tres dimensiones. La pregunta es ¿cuánto podríamos aumentar o disminuir los valores base de RGB de un color para que no se acerque demasiado a otro color?.

Si cogemos de nuevo la formula de la distancia entre dos puntos en las tres dimensiones la podemos invertir, para calcular la relación entre el valor umbral que queremos hallar y la distancia que ya tenemos. En el programa que pondré después podréis ver la formula para calcular dicho umbral.

Una vez tengamos ese umbral lo usaremos para calcular si un píxel con determinado RGB pertenece o no a alguno de los colores que tenemos. Para que pertenezca tiene que cumplir las siguientes condiciones:

  • Que el valor de R del píxel menos el umbral sea menor que el valor de R de nuestro color.
  • Que el valor de R del píxel más el umbral sea mayor que el valor de R de nuestro color.
  • Que el valor de G del píxel menos el umbral sea menor que el valor de G de nuestro color.
  • Que el valor de G del píxel más el umbral sea mayor que el valor de G de nuestro color.
  • Que el valor de B del píxel menos el umbral sea menor que el valor de B de nuestro color.
  • Que el valor de B del píxel más el umbral sea mayor que el valor de B de nuestro color.

Si las cumple podremos decir que dicho píxel pertenece a nuestro color. Del mismo modo, si tenemos diez colores, por ejemplo, habrá que hacer las comprobaciones con los diez colores, hasta que coincida con alguno o no haya más colores con los que comparar.

Programa en OpenCV:

En este programa vamos a utilizar las clases implementadas en el artículo anterior, solo que modificadas y mejoradas, con una funcionalidad mayor. Así las podremos usar dentro de nuestro programa de reconocimiento de colores y nos quedará todo mucho más ordenado. En este programa se hace uso de clases, por eso os recomiendo que leáis el primer artículo de la serie LeJOS, que habla de la programación orientada a objetos. Vamos a partir de la siguiente imagen, donde podéis observar catorce piezas de distintos colores:

ladrillos

La calidad de la imagen es bastante buena en este caso, ¿pero serán los colores suficientemente parecidos a los originales como para poder identificarlos?.

Color.h
//=============================================================
// Nombre : Color.h
// Autor : Álvaro Peláez Santana
// Version : 1.2
// Copyright : www.electricbricks.com
// Descripción : Clase color
//=============================================================

#include
#include
#include
#include
#include
#include
#include

using namespace std;

class Color {
private:
string nombre;
int rojo, verde, azul;
int cont;
int x_total;
int y_total;
public:
void SetRGB(int R, int G, int B, string nm);
int Rojo();
int Verde();
int Azul();
string Nombre();
float Distancia(Color color);
void ActualizarPixel(int x, int y);
void PuntoMedio(uchar *data, int anchura_fila, int canales);
};

Se le han añadido datos como el nombre del color, o las variables que usaremos para el cálculo del punto medio de ese color, así como la función para calcularlo. Como estamos hablando de clases, los valores de R, G y B, así como el nombre y demás serán los atributos de la clase, mientras que las funciones son sus métodos.

Color.cpp
//=============================================================
// Nombre : Color.cpp
// Autor : Álvaro Peláez Santana
// Version : 1.2
// Copyright : www.electricbricks.com
// Descripción : Clase color
//=============================================================

#include "Color.h"
#include

using namespace std;

void Color::SetRGB(int R, int G, int B, string nm){
rojo = R;
verde = G;
azul = B;
cont= 0;
x_total = 0;
y_total = 0;
nombre = nm;
}

int Color::Rojo(){
return rojo;
}
int Color::Verde(){
return verde;
}

int Color::Azul(){
return azul;
}

string Color::Nombre(){
return nombre;
}

float Color::Distancia(Color color2){
float aux;
aux = sqrt(pow((rojo - color2.rojo),2) + pow((azul - color2.azul),2) +
pow((verde - color2.verde),2));
return aux;
}

void Color::ActualizarPixel(int x, int y){
x_total = x_total + x;
y_total = y_total + y;
cont++;
}

void Color::PuntoMedio(uchar *data, int anchura_fila, int canales){
if (cont == 0){
cout << "Pieza color " << nombre << " no encontradan"; }else{ int x_medio = x_total / cont; int y_medio = y_total / cont; cout << "Punto medio de la pieza color " << nombre << " en " << x_medio << " " << y_medio << "n"; for(int i = y_medio - 5; i <= y_medio + 5; i++) for (int j = x_medio - 5; j <= x_medio + 5; j++){ data[i*anchura_fila+j*canales + 2] = rojo; data[i*anchura_fila+j*canales + 1] = verde; data[i*anchura_fila+j*canales + 0] = azul; } } }

Una pequeña explicación de lo que hacen las funciones (o mejor dicho métodos):

14 - Con este método inicializamos los valores de los atributos de nuestro objeto de la clase color, como sus valores RGB, o su nombre.

24, 28, 32 y 36 - Estos métodos sirven para devolver el valor de los atributos del objeto. Si por ejemplo necesitamos saber el valor R de nuestro color lila no tendremos más que llamar a lila.Rojo().

40 - Haya la distancia entre dos colores como la distancia entre dos puntos en las tres dimensiones.

47 - Tenemos que utilizar está función cada vez que encontremos un píxel en la imagen que corresponda con nuestro color.

53 - Halla el punto medio del objeto de determinado color en una imagen, y lo dibuja como un cuadrado de 5x5.

ListaColores.h
//=============================================================
// Nombre : ListaColores.h
// Autor : Álvaro Peláez Santana
// Version : 1.3
// Copyright : www.electricbricks.com
// Descripción : Clase ListaColores
//=============================================================

#include "Color.h"

class ListaColores {
private:
int num_colores, colores_act;
Color colores[100];

public:
void NumColores(int num);
void AnadirColor(Color color1);
float DistanciaMinima();
void ComprobarColor(int R, int G, int B, int umbral, int x, int y);
void PuntosMedios(uchar *data, int anchura_fila, int canales);
};

Los nuevos métodos son ComprobarColor y PuntosMedios.

ListaColores.cpp
//=============================================================
// Nombre : ListaColores.cpp
// Autor : Álvaro Peláez Santana
// Version : 1.3
// Copyright : www.electricbricks.com
// Descripción : Clase ListaColores
//=============================================================

#include "ListaColores.h"

using namespace std;

void ListaColores::NumColores(int num){
num_colores = num;
colores_act = 0;
}

void ListaColores::AnadirColor(Color color1){
if (colores_act < num_colores){ colores[colores_act] = color1; colores_act++; } } float ListaColores::DistanciaMinima(){ float min = 4000; int id_col1, id_col2; for(int i = 0; i < num_colores; i++){ for(int j = i + 1; j < num_colores; j++){ if (colores[i].Distancia(colores[j]) < min){ min = colores[i].Distancia(colores[j]);; id_col1 = i; id_col2 = j; } } } cout << "La distancia mínima es de: " << min << " entre el color " << colores[id_col1].Nombre() << " y el color " << colores[id_col2].Nombre() << "n"; return min; } void ListaColores::ComprobarColor(int R, int G, int B, int umbral, int x, int y){ int rojo, azul, verde; for(int i = 0; i < num_colores; i++){ rojo = colores[i].Rojo(); azul = colores[i].Azul(); verde = colores[i].Verde(); if((((R - umbral) < rojo) && (rojo < (R + umbral))) && (((G - umbral) < verde) && (verde < (G + umbral))) && (((B - umbral) < azul) && (azul < (B + umbral)))){ colores[i].ActualizarPixel(x,y); break; } } } void ListaColores::PuntosMedios(uchar *data, int anchura_fila, int canales){ for(int i = 0; i < num_colores; i++){ colores[i].PuntoMedio(data, anchura_fila, canales); } }

Donde los métodos hacen las siguientes funciones:

13 - Inicializa el número de colores total que vamos a manejar.

18 - Añade un color a nuestra lista de colores.

25 - Calcula la distancia mínima entre todos los colores.

43 - Comprueba si determinado píxel pertenece a alguno de los colores que tenemos en la lista.

58 - Calcula los puntos medios de todos los objetos de cada color.

Y el programa principal queda de la siguiente manera:

DeteccionObjetos.cpp
//=============================================================
// Nombre : DeteccionObjetos.cpp
// Autor : Álvaro Peláez Santana
// Version : 3.0
// Copyright : www.electricbricks.com
// Descripción : Programa principal que calcula los puntos
// medios de objetos de varios colores
//=============================================================

#include "ListaColores.h"

using namespace std;

int main(int argc, char *argv[])
{
ListaColores lista;
lista.NumColores(8);

/////////Color 1
Color amarillo;
amarillo.SetRGB(242,225,47,"amarillo");
lista.AnadirColor(amarillo);

/////////Color 2
/* Color rojo;
rojo.SetRGB(222,0,13,"rojo");
lista.AnadirColor(rojo);*/

/////////Color 3
Color azul;
azul.SetRGB(0,87,168,"azul");
lista.AnadirColor(azul);

/////////Color 4
Color verde;
verde.SetRGB(0,123,40,"verde");
lista.AnadirColor(verde);

/////////Color 5
Color negro;
negro.SetRGB(1,1,1,"negro");
lista.AnadirColor(negro);

/////////Color 6
/* Color blanco;
blanco.SetRGB(244,244,244,"blanco");
lista.AnadirColor(blanco);*/

/////////Color 7
Color lima;
lima.SetRGB(149,185,11,"lima");
lista.AnadirColor(lima);

/////////Color 8
/*Color tierra;
tierra.SetRGB(217,187,123,"tierra");
lista.AnadirColor(tierra);*/

/////////Color 9
Color marron_rojizo;
marron_rojizo.SetRGB(91,28,12,"marron rojizo");
lista.AnadirColor(marron_rojizo);

/////////Color 10
/*Color gris_o_azulado;
gris_o_azulado.SetRGB(76,81,86,"gris oscuro azulado");
lista.AnadirColor(gris_o_azulado);*/

/////////Color 11
/*Color gris_c_azulado;
gris_c_azulado.SetRGB(156,146,145,"gris claro azulado");
lista.AnadirColor(gris_c_azulado);

/////////Color 12
Color azul_medio;
azul_medio.SetRGB(71,140,198,"azul medio");
lista.AnadirColor(azul_medio);*/

/////////Color 13
Color tierra_oscuro;
tierra_oscuro.SetRGB(141,116,82,"tierra oscuro");
lista.AnadirColor(tierra_oscuro);

/////////Color 14
Color rojo_oscuro;
rojo_oscuro.SetRGB(128,8,27,"rojo oscuro");
lista.AnadirColor(rojo_oscuro);

/////////Color 15
/*Color rosa;
rosa.SetRGB(238,157,195,"rosa");
lista.AnadirColor(rosa);

/////////Color 16
Color rosa_oscuro;
rosa_oscuro.SetRGB(222,55,139, "rosa oscuro");
lista.AnadirColor(rosa_oscuro);*/

float dist_color = lista.DistanciaMinima();
float umbral = sqrt(pow(dist_color,2)/3);
umbral = umbral*0.75;
cout << "Umbral: " << umbral << "n"; IplImage* img = 0; int altura,anchura,anchura_fila,canales; uchar *data; int i,j; if(argc<2){ printf("Uso: main n7");
exit(0);
}

// cargamos la imagen
img=cvLoadImage(argv[1]);
if(!img){
printf("No se ha podido cargar la imagen: %sn",argv[1]);
exit(0);
}

// cogemos la información de la imagen
altura = img->height;
anchura = img->width;
anchura_fila = img->widthStep;
canales = img->nChannels;
data = (uchar *)img->imageData;
printf("Procesando una imagen de %dx%d píxeles con %d canalesn",
altura, anchura, canales);

// creamos una ventana
cvNamedWindow("mainWin", CV_WINDOW_AUTOSIZE);
cvMoveWindow("mainWin", 100, 100);

// recorremos la imagen

int R, G, B;

for(i=0;i

Y aquí una breve explicación del algunas líneas:

17 a 98 - Creamos todos los colores y la lista donde los metemos.

100 - Hallamos la distancia mínima.

101 y 102 - Calculamos la distancia umbral invirtiendo la fórmula de la distancia (teniendo en cuenta que la distancia mínima es igual a la raíz cuadrada de tres veces el umbral al cuadrado.

104 a 128 - Cargamos la imagen con todos sus datos.

138 a 143 - Recorremos la imagen identificando a que objeto de que color pertenece cada píxel.

145 - Calculamos los puntos medios de dichos objetos.

Al ejecutar el programa con la imagen anterior nos da el siguiente resultado:

resultado ladrillos

Y nos muestra la imagen procesada, con los puntos medios pintados:

Ladrillos procesados

Como podéis observar hay colores que detecta bien, y otros que detecta mal o incluso no son detectados. Esto es debido a que hemos metido el valor original de los colores de LEGO, y muchos de ellos no se parecen a los colores que se ven realmente en la imagen (debido entre otras cosas a las condiciones de iluminación). Es decir, existe una discrepancia entre los valores RGB teóricos con los que han sido fabricados los ladrillos LEGO, y los valores RGB que tenemos en nuestra imagen. La magnitud de esa discrepancia es la que impide que algunos de los colores no sean detectados. Si usáramos los colores de la imagen detectaría los puntos medios perfectamente. En un futuro experimento nos plantearemos que el programe detecte que valores de RGB de la imagen corresponden en realidad con los del color que buscamos. El problema de la iluminación y, en general, de las condiciones en las que se están capturando las imágenes, es de tal magnitud en visión que muchas de las aplicaciones fracasan precisamente por problemas derivados de ella. Una forma práctica de resolver este problema es trabajar con condiciones muy controladas y conocidas de iluminación. En este contexto y la vista de los resultados obtenidos sabemos que el valor teórico del color Marrón Rojizo viene dado por:


/////////Color 9
Color marron_rojizo;
marron_rojizo.SetRGB(91,28,12,"marron rojizo");
lista.AnadirColor(marron_rojizo);

...pero hemos visto que en nuestras condiciones dichas coordenadas RGB no se corresponden con el marrón rojizo que esta viendo la cámara: prueba de ello es que el centro de masas calculado no se corresponde con el del ladrillo de este color. La forma más simple de resolver el problema consistirá por tanto en ver cuáles son las coordenadas RGB que está devolviendo la cámara para este color y reemplazar los parámetros anteriores por los reales. Esta forma de proceder es simple pero no es adaptativa a posibles cambios.

Aplicaciones
Este experimento puede tener aplicaciones en manipuladores que vayan a trabajar con piezas de distintos colores, ya que el primer paso es ser capaces de identificarlas.

Comments are closed.