domingo, 30 de mayo de 2010

Entrada final

Hola a todos,

después de un duro cuatrimestre de trabajo, el proyecto del Checkersbot y este blog llegan a su fin. Hemos logrado acoplar el sistema a nuestras necesidades, y ya sólo nos queda adjuntaros el material final para que podáis valorar nuestro trabajo.

Memoria en formato (.pdf) de la práctica:
http://www.megaupload.com/?d=O8XTUFC2

Código de la práctica:
http://www.megaupload.com/?d=TMO1AX05

Los vídeos son el material más explicativo:

Vídeo 1 - Demo del sistema
http://www.youtube.com/watch?v=gNRJRDM6ndg&playnext_from=TL&videos=i8SfH2MbnOM&feature=rec-LGOUT-exp_fresh%2Bdiv-1r-3-HM

Vídeo 2 - Explicación del sistema
http://www.youtube.com/watch?v=i3hEQho7XNA&feature=related

Vídeo 3 - Tomas falsas (para que os riáis un poco ^^)
http://www.youtube.com/watch?v=nc4eoAN33zc&feature=related

Y nada más, así acaba nuestro proyecto. Desde aquí queremos agradeceros a todos vosotros por seguir nuestro blog y apoyarnos a los autores en este trabajo, que ha sido duro y costoso.

Si nos ponemos a nombrar individuos no acabaríamos, pero en especial queríamos agradecer a Juancho por tutorizarnos la práctica y darnos todo el material necesario. ¡Infinitas gracias! ;)

Si sois alumnos pendientes de cursar LSED y habéis visto nuestros vídeos para inspiraros y/o ver ejemplos de prácticas, deciros que nos sentimos muy halagados, y os deseamos mucha suerte con vuestras respectivas prácticas. Y si no sois alumnos o ya habéis cursado LSED, suerte también, aunque no para esta asignatura, claro. ^^

Christian Greciano
Alberto González

jueves, 13 de mayo de 2010

4ª revisión de prácticas especiales: documentación

Hoy hemos tenido la cuarta y última revisión de las prácticas especiales y por lo tanto tenemos material que ofrecer. Una presentación resumen que os podéis descargar aquí:

http://www.megaupload.com/?d=28PPB9FO

Y aquí un vídeo en el que se ve la luz de lo que pretendemos con el sistema final:

http://www.youtube.com/watch?v=dpgTWCCwXZU

Todavía queda un empujón final, fijando todas las partes (brazo, tablero, cámara) en un único módulo estable, y también hay que pulir bien algunos movimientos del brazo para que realice lo mejor posible las jugadas.

sábado, 8 de mayo de 2010

Detección de jugadas

    El algoritmo de detección de jugadas es algo que ha sufrido bastantes modificaciones para encontrar la forma más óptima de adaptarlo a las condiciones específicas de nuestra práctica.

Para intentar resumir el tema, voy a comentar las claves a tener en cuenta, y a explicaros el algoritmo final que hemos desarrollado.

El algoritmo de detección de jugadas se basa en un concepto muy simple: detectamos movimiento y esperamos a que haya pasado un tiempo sin movimiento. Hay que tener en cuenta que cuanto más acotada esté la zona de detección, más fiabilidad vamos a tener. Normalmente, el problema es que esta zona no puede ser acotada, ya que hay que detectar movimiento en todo el rango de visión. Pero en nuestro casi si que se podía acotar, ya que nosotros sólo queremos ver si hay movimiento en el tablero o no, no nos interesa el resto de la zona. Por tanto, nuestro algoritmo de detección se aplica siempre sobre la imagen del tablero ya tratada, no sobre la original obtenida por la webcam.

Lo primero es detectar movimiento. Para esto lo que hacemos es comparar la imagen actual umbralizada con la anterior umbralizada, para eliminar ruido y que se produzcan falsos positivos por el tema de la diferencia de iluminación. Nosotros hemos modificado un poco este concepto, y lo que hacemos es tener una imagen para comparar que se actualiza cada 5 frames. El por qué de esto y no comparar con el frame anterior, es que a veces la diferencia entre imágenes consecutivas es demasiado pequeña, pero entre 3, 4 o 5 frames es ya suficiente como para saber si hay movimiento o no. Podríamos poner más frames, ya que para detectar movimiento no nos importa, pero si nos perjudicaría para la “no detección de movimiento”, como veremos a continuación.

Lo segundo es, que una vez detectado movimiento, hay que esperar un número de frames sin movimiento. Cuando se ha producido movimiento, empezamos a contar frames sin movimiento. Cuando se llega a un número determinado de frames sin movimiento, es que se ha realizado una jugada. Cuantos más frames pongamos como condición, más lents será la detección, pero más fiable, y cuanto menos frames pongamos como condición, más rápida será la detección, pero menos fiable.

Y la gran pregunta es obligada: ¿cómo hacer un sistema de detección rápido pero fiable?

La repuesta es inmediata: haz un sistema de detección rápido, que no es fiable, y luego elimina los falsos positivos. Para detectar los falsos positivos es muy sencillo. ¿Os acordáis del post anterior, en la detección de fichas, que poníamos un límite superior de 25 px? Pues aqui está su aplicación: si una casilla blanca tiene más de pi r_max^2 de negro, es que hemos producido un falso positivo de detección de jugada. Seguramente el jugador todavía tiene el brazo encima del tablero. Para más seguridad, nosotros también miramos que las casillas negras no tengan un porcentaje considerable de blanco, ya que el jugador podría tener el brazo encima del tablero pero ir vestido de blanco (o un color claro).

 

Alternativas descartadas

Surgieron varias opciones para la detección de jugadas como poner un control mecánico que pulsar cada vez que se realiza una jugada, como se hace en ajedrez. Era más fiable, pero no cumplía con los requisitos del proyecto de realizar todo el reconocimiento mediante Visión Computacional.

 

** Como en el post anterior, subiré las imágenes y el código el próximo día.

Detección de fichas en el tablero

    Para detectar las fichas en el tablero, hay dos opciones:

    1ª) Partir de una posición inicial conocida. Detectamos los cambios en la imagen después de que se produzca movimiento. En la imagen diferencia, comprobamos si hay fichas.

    2ª) No partimos de posición inicial. Detectamos todas las fichas después de cada movimiento.

 

1ª IMPLEMENTANCIÓN

En un principio, nosotros escogimos la 1ª opción. ¿Qué es más fácil, detectar en una “imagen diferencia” 2 fichas, o detectar todo el rato todas las fichas en la imagen general?

Siguiendo este principio, desarrollamos el programa detectando los movimientos de fichas. Cuando terminamos de implementarla, encontramos 2 problemas serios.

1) Teníamos que empezar el juego con el tablero vacío, e ir colocando las fichas. No se podía comenzar con el tablero vacío.

2) La imagen diferencia, pese a umbralizarla (pasarle un cvThreshold), tenía ruido.

Para detectar las fichas en la imagen diferencia, utilizamos una trasformada de Hough (cvHoughCircles). Al haber ruido, la diferencia de imagenes no era perfecta, y por lo tanto los círculos no eran perfectos. La detección de círculos aplicando la transformada de Hough, a pesar del ajuste de parámetros, tenía falsos positivos y un porcentaje considerable de fichas no detectadas.

El resultado de esta implementación fue un modelo funcional, pero que no cumplía con los objetivos del proyecto. Tener que repetir jugadas y que se detectaran trampas cuando no las había, era aspectos que no podíamos permitirnos.

Voy a poner sólo la parte del código de detección de círculos, ya que el resto no merece la pena, entre otras cosas porque hay una implementación mucho más eficaz que se expondrá a continuación.

 

2ª IMPLEMENTANCIÓN

Visto los problemas del modelo anterior, teníamos que conseguir varios objetivos:

1) Poder arrancar el programa con las fichas en cualquier posición. La partida no empezaría hasta que las fichas estuvieran colocadas como corresponde en el juego de damas.

2) Detección de fichas con una fiabilidad del 100%. Si no se detecta una ficha, el programa no puede seguir funcionando, por lo que hay que detectar las fichas siempre.

Para cumplir el primer objetivo, estaba claro que había que tener una imagen del tablero vacía para poder hacer una diferencia entre imágenes, y sacar las fichas. El problema era tener esa imagen. No nos valía sacar una imagen del tablero vacío y guardarla para comprar, ya que la imagen del tablero va a variar en cada ejecución del programa. Además, volveríamos a tener el problema del ruido de antes.

Y entonces se nos ocurrió: ¿por qué no nos dibujamos, en tiempo de ejecución, el tablero, ya que tenemos las coordenadas de cada casilla? Así compararíamos con un tablero ideal vacío y nos quitaríamos el problema del ruido.

 

Cumplido el primer objetivo, nos quedaba el segundo. Juancho, nuestro tutor, nos dio una idea que sería vital: ¿por qué no buscáis las fichas casilla por casilla? Si tenéis donde están las casillas, buscar dentro de cada casilla la ficha. El problema era que aún así, la transformada de Hough no era 100% fiable. Pero, ¿de verdad necesitábamos la transformada de Hough? Es decir, ¿de verdad vamos a querer distinguir si lo que hay dentro es una ficha redonda y tenerla en cuenta, de una ficha de cuadrada o triangular? Nosotros jugaremos con fichas redondas, pero puede que alguien juegue con fichas triangulares o cuadradas.

Así que el problema que teníamos, quedó solucionado de una manera muy sencilla. Recorremos todas las casillas donde pueda haber una ficha (recuerdo que en las damas solo se utilizan las casillas de un color, en nuestro caso las de color blanco), y comprobamos el porcentaje de color negro que hay. Para establecer el radio minimo y maximo tenemos en cuenta lo siguiente: la imagen sobre la que trabajamos es de 480x480 (la webcam las coge de 640x480, aunque podría ser de cualquier tamaño, pero luego nosotros la recortamos a 480x480), y el tablero como tiene 8x8 casillas iguales, tenemos que la dimensión de cada casilla es de 60x60. Luego el radio máximo será de 30 px. Pero como las fichas suele ocupar la mitad de la casilla, tenemos un radio aproximado de 15px. Los límites que establecimos fue un radio minimo de 5px y uno máximo de 25px. ¿Por qué el radio máximo? Si la casilla supera un porcentaje de negro mayor a una ficha de 25 px de radio, es que la casilla es toda negra, y eso será una detección errónea (seguramente hayamos detectado mal el movimiento, y haya un objeto encima de esa casilla).

Por tanto, estableciendo los límites de negro entre  pi r_min^2 y pi r_max^2 para r_min = 5 y r_max = 25, establecemos si hay una ficha o no en la casilla.

Esta implementación nos ha dado unos resultado de fiabilidad bastante sorprendentes, ya que la fiabilidad podemos calificarla como del 100%.

 

DISTINGUIR FICHA BLANCAS Y NEGRAS

Para distinguir fichas blancas y negras (en nuestro caso, de momento, azules y negras), lo que hacemos es:

- Umbralizamos la imagen diferencia con un threshold alto para que solo queden las fichas negras.

- Detecamos las fichas negras.

- Hacemos una sustitución de color de azul a negro en la imagen diferencia.

- Eliminamos las fichas negras (que hemos detectado anteriormente)

- Detectamos fichas negras (que serán las azules que hemos convertido a negro).

 

 

** Tengo que subir las imágenes y el código, que lo haré el próximo día reeditando el post.

martes, 4 de mayo de 2010

Primera toma de contacto con los brazos robóticos

Como el laboratorio no cuenta con un plotter (un tipo de brazo robótico como el que aparece en la portada de las transparencias de presentación de esta práctica), vamos a arreglarnos con un par de brazos robóticos normales.

El pasado jueves día 29 de abril tuvimos una revisión de prácticas y expusimos esta presentación, que resume bien nuestros primeros pasos con los mismos. El enlace de descarga lo podéis encontrar aquí:

http://www.megaupload.com/?d=EMRYL0WV

Básicamente el trabajo hasta ahora ha consistido en montar los brazos (o alargar los que ya estaban montados para que tuvieran más alcance), familiarizarse con el funcionamiento de los brazos y la controladora de los servos y probar a enviar datos por el puerto serie del ordenador y así controlar el brazo. De momento hemos logrado controlar el brazo y enviar datos por el puerto serie tanto con una interfaz gráfica de usuario proporcionada por el fabricante como con el siguiente código para VisualC++:

 

////////////////////////////////////////////////////////////////////////////

// PuertoSerie.cpp: permite la transmisión de datos a través del puerto serie del PC.
//////////////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "windows.h"

#define

int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hSerial;
LPCWSTR port2 = TEXT("COM5");
hSerial = CreateFile(port2,
GENERIC_WRITE,
0,
0,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0);
if(hSerial==INVALID_HANDLE_VALUE){
if(GetLastError()==ERROR_FILE_NOT_FOUND){
//serial port does not exist. Inform user.

}
//some other error occurred. Inform user.
}

DCB dcbSerial = {0};
DCB dcbSerialParams = {0};
dcbSerial.DCBlength=sizeof(dcbSerialParams);
if (!GetCommState(hSerial, &dcbSerialParams)) {
//error getting state
}
dcbSerialParams.BaudRate=CBR_19200;
dcbSerialParams.ByteSize=8;
dcbSerialParams.StopBits=ONESTOPBIT;
dcbSerialParams.Parity=NOPARITY;
if(!SetCommState(hSerial, &dcbSerialParams)){
//error setting serial port state
}

COMMTIMEOUTS timeouts={0};
timeouts.ReadIntervalTimeout=50;
timeouts.ReadTotalTimeoutConstant=50;
timeouts.ReadTotalTimeoutMultiplier=10;

timeouts.WriteTotalTimeoutConstant=50;
timeouts.WriteTotalTimeoutMultiplier=10;
if(!SetCommTimeouts(hSerial, &timeouts)){
//error occureed. Inform user
}

int n = 5;
char szBuff[6] = {'>', '1', '1', 'a', 127};
DWORD dwBytesRead = 0;
if(!WriteFile(hSerial, szBuff, n, &dwBytesRead, NULL)){
//error occurred. Report to user.
}

return 0;
}


Seguiremos informando y ofreciendo detalles en entradas posteriores.

sábado, 24 de abril de 2010

Mapeando el tablero: consiguiendo las coordenadas de cada casilla

    Una vez localizadas las esquinas interiores (organizadas por filas y columnas, pero sin saber el orden), lo primero que haremos es ordenarlas para que siempre sigan un orden determinado: ordenadas horizontalmente de izquierda a derecha, de arriba a abajo.

Para poder ordenarlas, necesitamos saber antes que orden están siguiendo. El método utilizado para encontrar el orden sique el siguiente esquema:

 

orden

 

La numeración corresponde a cada caso particular:

0 –> Ordenado verticalmente, de derecha a izquierda, de abajo a arriba
1 –> Ordenado verticalmente, de derecha a izquierda, de arriba a abajo
2 –> Ordenado verticalmente, de izquierda a derecha, de abajo a arriba 3 –> Ordenado verticalmente, de izquierda a derecha, de arriba a abajo
4 –> Ordenado horizontalmente, de derecha a izquierda, de abajo a arriba
5 –> Ordenado horizontalmente, de derecha a izquierda, de arriba a abajo
6 –> Ordenado horizontalmente, de izquierda a derecha, de abajo a arriba
7 –> Ordenado horizontalmente, de izquierda a derecha, de arriba a abajo

Para que se cumplan los ordenados verticalmente, comprobamos a que punto es igual la esquina nº [alto – 1], y para los ordenados horizontalmente, la esquina nº [ancho –1].

0 => esquina[alto – 1] = Esquina Superior Derecha
1 => esquina[alto – 1] = Esquina Inferior Derecha
2 => esquina[alto – 1] = Esquina Superior Izquierda
3 => esquina[alto – 1] = Esquina Inferior Izquierda
4 => esquina[ancho – 1] = Esquina Inferior Izquierda
5 => esquina[ancho – 1] = Esquina Superior Izquierda
6 => esquina[ancho – 1] = Esquina Inferior Derecha
7 => esquina[ancho – 1] = Esquina Superior Derecha

 

///////////////////////////////////////////////////////////////////////////////
//    check_order
//
//    Check the order (horizontal/vertical, left/right, top/bottom) of the points
//
//    Parameters:
//        CvPoint2D32f* points -> Pointer to the corners
//        int pointsCount-> Number of corners
//        CvSize cornerSize -> size of the corners [i.e. CvSize(7,7)]
//        CvSize size -> size of the image in px [i.e. CvSize(480,480)]
//
//    Return:
//        0) Ordered vertical, from rigth to left, from bottom to up
//        1) Ordered vertical, from rigth to left, from up to bottom
//        2) Ordered vertical, from left to rigth, from bottom to up
//        3) Ordered vertical, from left to rigth, from up to bottom
//        4) Ordered horizontal, from rigth to left, from bottom to up
//        5) Ordered horizontal, from rigth to left, from up to bottom
//        6) Ordered horizontal, from left to rigth, from bottom to up
//        7) Ordered horizontal, from left to rigth, from up to bottom
//
///////////////////////////////////////////////////////////////////////////////
 
int check_order(CvPoint2D32f *points, int pointsCount, CvSize cornerSize, CvSize size)
{
    CvPoint2D32f top_left = closest_to(points, pointsCount, cvPoint2D32f(0,0));
    CvPoint2D32f top_right = closest_to(points, pointsCount, cvPoint2D32f(size.width, 0));
    CvPoint2D32f bottom_right = closest_to(points, pointsCount, cvPoint2D32f(size.width, size.height));
    CvPoint2D32f bottom_left = closest_to(points, pointsCount, cvPoint2D32f(0, size.height));
    int width = cornerSize.width;
    int height = cornerSize.height;
 
    if(top_left.x == points[0].x && top_left.y == points[0].y)
        if(top_right.x == points[width - 1].x && top_right.y == points[width - 1].y)
            return 7;
        else if(bottom_left.x == points[height - 1].x && bottom_left.y == points[height - 1].y)
            return 3;
        else
            throw "Error checking order: points start at top_left";
 
    if(top_right.x == points[0].x && top_right.y == points[0].y)
        if(top_left.x == points[width  - 1].x && top_left.y == points[width - 1].y)
            return 5;
        else if(bottom_right.x == points[height - 1].x && bottom_right.y == points[height - 1].y)
            return 1;
        else
            throw "Error checking order: points start at top_right";
 
    if(bottom_left.x == points[0].x && bottom_left.y == points[0].y)
        if(bottom_right.x == points[width - 1].x && bottom_right.y == points[width - 1].y)
            return 6;
        else if(top_left.x == points[height - 1].x && top_left.y == points[height - 1].y)
            return 2;
        else
            throw "Error checking order: points start at bottom_left"; 
    
    if(bottom_right.x == points[0].x && bottom_right.y == points[0].y)
        if(bottom_left.x == points[width - 1].x && bottom_left.y == points[width - 1].y)
            return 4;
        else if(top_right.x == points[height - 1].x && top_right.y == points[height - 1].y)
            return 0;
        else
            throw "Error checking order: points start at bottom_right"; 
 
    throw "Error checking order: no point start";
}

 

ORDENACIÓN DE LAS ESQUINAS

   Una vez obtenido el orden, rellenamos una matriz de 2 dimensiones (filas x columnas) que va a representar las esquinas del tablero, ordenado horizontalmente, de izquierda a derecha, de arriba a abajo.

[0][0] –> Esquina Superior Izquierda
[0][ancho - 1] –> Esquina Superior Derecha
[alto][0] –> Esquina Inferior Izquierda
[alto][ancho - 1] –> Esquina Inferior Derecha



///////////////////////////////////////////////////////////////////////////////
//    ordenate
//
//    Finds all the corners of the checkerboard with the internal corners
//
//    Parameters:
//        CvPoint2D32f *points -> Pointer to the corners
//        int cornerCount -> Number of corners
//        CvSize cornerSize -> Size of the corners [i.e. CvSize(7,7)]
//        CvSize size -> Size of the image [i.e. CvSize(480,480)]
//
///////////////////////////////////////////////////////////////////////////////
 
void ordenate(CvPoint2D32f *points, int cornerCount, CvSize cornerSize,  CvSize size)
{
    int state = check_order(points, cornerCount, cornerSize, size);
    int NUM_V_CORNERS = cornerSize.width;
    int NUM_H_CORNERS = cornerSize.height;
    
    CvPoint2D32f** temp = (CvPoint2D32f**)malloc(NUM_H_CORNERS * sizeof(CvPoint2D32f*));
 
    for(int i = 0;i < NUM_H_CORNERS; i++)
        temp[i] = (CvPoint2D32f*) malloc(NUM_V_CORNERS * sizeof(CvPoint2D32f));
 
    //CvPoint2D32f** temp = new CvPoint2D32f* [NUM_H_CORNERS];
    //for(int i = 0;i < NUM_H_CORNERS; i++)
    //    *(temp+i)=new CvPoint2D32f[NUM_V_CORNERS];
    
    switch(state)
    {
        case 7:        // Ordered horizontal, from left to rigth, from up to bottom
                    for(int i = 0; i < cornerCount; i++)
                        temp[i / NUM_H_CORNERS][i % NUM_H_CORNERS] = points[i];
                    break;
 
        case 6:        // Ordered horizontal, from left to rigth, from bottom to up
                    break;
 
        case 5:        // Ordered horizontal, from rigth to left, from up to bottom
                    break;
 
        case 4:        // Ordered horizontal, from rigth to left, from bottom to up
                    break;
 
        case 3:        // Ordered vertical, from left to rigth, from up to bottom
                    break;
 
        case 2:        // Ordered vertical, from left to rigth, from bottom to up
                    for(int i = 0; i < cornerCount; i++)
                        temp[(NUM_V_CORNERS - 1) - (i % NUM_V_CORNERS)][i / NUM_V_CORNERS] = points[i];
                    break;
 
        case 1:        // Ordered vertical, from rigth to left, from up to bottom
                    for(int i = 0; i < cornerCount; i++)
                        temp[i % NUM_V_CORNERS][(NUM_H_CORNERS - 1) - i / NUM_V_CORNERS] = points[i];
                    break;
 
        case 0:        // Ordered vertical, from rigth to left, from bottom to up
                    break;
    }
 
    for(int i = 0; i < NUM_V_CORNERS; i++)
        for(int j = 0; j < NUM_H_CORNERS; j++)
        {
            points[i * NUM_H_CORNERS + j] = temp[i][j];
            //printf("Corner %d en (%d, %d)\n", i * NUM_H_CORNERS + j, cvRound(points[i * NUM_H_CORNERS + j].x), cvRound(points[i * NUM_H_CORNERS + j].y));
        }
}

 


ALGORITMO DE RECONSTRUCCIÓN


    Ahora que disponemos de las casillas ordenadas siempre igual (horizontalmente, de izquierda a derecha, de arriba a abajo), ya simplemente nos queda aplicar el algoritmo de reconstrucción: estimar la posición de las esquinas “no interiores”. Es importantes resaltar que ahora estamos trabajando con la proyección sobre el PH, por lo que tenemos la imagen sin distorsión. Esto nos simplifica muchísimo la complejidad del algoritmo de reconstrucción:


estimacion estimacionPH



Para la estimación de puntos en una imagen real (la de la izquierda), vemos que para estimar habría que calcular el punto de fuga, y hacer una relación de proporcionalidad para estimar los puntos.


Para la estimación de puntos en la imagen proyectada sobre el PH, simplemente tenemos que sumarle una distancia constante.


Esta vez, en principio, el código no lo vamos a adjuntar. Este algoritmo es un poco confuso a la hora de tomar los índices de las matrices (imaginarios como sería para una imagen real o para una sobre el PH con los puntos desordenados… y ahora para una real con los puntos desordenados!!!)


 


FORMANDO LAS CASILLAS


Ahora que tenemos todas las esquinas (tanto interiores como exteriores) y ordenadas, ya simplemente tenemos que formar rectángulos con ellas.

El proceso seguido es bastante intuitivo:  creamos un array con todas las casillas, y por cada una escogemos las 4 esquinas correspondientes.
El tema de los índices no es tan confuso como en el algoritmo de reconstrucción, pero aún así hay que tener cuidado.

 



///////////////////////////////////////////////////////////////////////////////
//
//    Finds all the corners of the checkerboard with the internal corners
//
//    Parameters:
//        CvPoint2D32f* corners -> Pointer to the corners
//        CvSize patternSize -> Board Size in squares (width, height)
//
//    Return:
//        CvSquare* -> Pointer to the squares of the board
//
///////////////////////////////////////////////////////////////////////////////
 
CvSquare* findSquaresFromCorners(CvPoint2D32f* corners, CvSize patternSize)
{
    int NUM_H_SQUARES = patternSize.width;
    int NUM_V_SQUARE = patternSize.height;
    CvSquare* squares = new CvSquare[NUM_H_SQUARES * NUM_V_SQUARE];
 
    for(int i = 0; i < NUM_V_SQUARE; i++)
    {
        for(int j = 0; j < NUM_H_SQUARES; j++)
        {
            double bottom_left_x = corners[(i+1) * (NUM_H_SQUARES + 1) + j].x;
            double bottom_left_y = corners[(i+1) * (NUM_H_SQUARES + 1) + j].y;
            CvPoint2D32f bottom_left = cvPoint2D32f(bottom_left_x, bottom_left_y);
 
            double bottom_right_x = corners[(i+1) * (NUM_H_SQUARES + 1) + j + 1].x;
            double bottom_right_y = corners[(i+1) * (NUM_H_SQUARES + 1) + j + 1].y;
            CvPoint2D32f bottom_right = cvPoint2D32f(bottom_right_x, bottom_right_y);
 
            double top_left_x = corners[ i * (NUM_H_SQUARES + 1) + j].x;
            double top_left_y = corners[ i * (NUM_H_SQUARES + 1) + j].y;
            CvPoint2D32f top_left = cvPoint2D32f(top_left_x, top_left_y);
 
            double top_right_x = corners[ i * (NUM_H_SQUARES + 1) + j + 1].x;
            double top_right_y = corners[ i * (NUM_H_SQUARES + 1) + j + 1].y;
            CvPoint2D32f top_right = cvPoint2D32f(top_right_x, top_right_y);
 
            CvPoint2D32f* points = new CvPoint2D32f[4];
            points[0] = bottom_left;
            points[1] = bottom_right;
            points[2] = top_right;
            points[3] = top_left;
            squares[i * NUM_H_SQUARES + j].points = points;
            printf("Square %d (%d,%d) - (%d,%d) - (%d,%d) - (%d,%d)\n", i * NUM_H_SQUARES + j, cvRound(bottom_left.x), cvRound(bottom_left.y), cvRound(bottom_right.x), cvRound(bottom_right.y), cvRound(top_right.x), cvRound(top_right.y), cvRound(top_left.x), cvRound(top_left.y));
        }
    }
 
    return squares;
}

Y el resultado lo podemos comprobar en la siguiente imagen


algoritmo1