Tuesday 9 October 2012

Game Player - Step 1.5 - Tidying Up The Code

We have done a lot of work over the last few posts and looked at some interesting features of OpenCV.

As it stands there is a lot of this stuff, although handy for debugging, we don't need in our final system. Things like the calls to printf and cout slow the system down unnecessarily so we can remove them. We can also take this opportunity to separate the majority of the code into its own method that completes the task of getting the game state. This will make the flow of the call to C easier to see and allow us to easily replace ways of completing a task (such as finding the colour of a game piece in an image) easier which is handy if we want to try out new algorithms.



Finally there is one important change that really needs made. It appears OpenCV has a class called Vector (with a big V) and normal C / C++ has a class called vector (with a small v). They appear to work the same way but don't, as I found out when I separated the code into separate methods and the data didn't return correctly. So, even if you do nothing else, do this: change all the Vector or Vector<Vector<> >'s to vector and vector<vector<> >'s.

So before we continue let's tidy this up before it becomes an unmanageable, tangled mess. You can download the tidied code from the bottom of the page or you can take care of it yourself.

Here is the differences for those who want the pleasure of reading code without the hardship of downloading a zip.

Original code:
#include "gameplayer_GamePlayer.h"
#include <iostream>
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"

/*
 * Class:     gameplayer_GamePlayer
 * Method:    printMsg
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_gameplayer_GamePlayer_printMsg
(JNIEnv *env, jclass obj) {
    std::cout << "GamePlayer library loaded.\n";
}

/*
 * Class:     gameplayer_GamePlayer
 * Method:    calcMove
 * Signature: ([I)[III
 */
JNIEXPORT jintArray JNICALL Java_gameplayer_GamePlayer_calcMove
(JNIEnv *env, jclass obj, jintArray data, jint width, jint height) {
    using namespace std;
    using namespace cv;
    jsize len = env->GetArrayLength(data);
    jint* elem = env->GetIntArrayElements(data, 0);
    printf("Array size from C: %d\n", len);
    cout << "pixelData[0] A = " << ((elem[0] >> 24) & 0xFF) << "\n";
    cout << "pixelData[0] R = " << ((elem[0] >> 16) & 0xFF) << "\n";
    cout << "pixelData[0] G = " << ((elem[0] >> 8) & 0xFF) << "\n";
    cout << "pixelData[0] B = " << (elem[0] & 0xFF) << "\n";
    fflush(stdout);
    //Make the ARGB image out of the data
    Mat screen(height, width, CV_8UC4, elem);

    //Filter for purple (B,G,R,A)
    Mat purpleOnly;
    inRange(screen, Scalar(150, 0, 150, 0), Scalar(255, 50, 255, 255), purpleOnly);

    //Get greyscale image
    //    Mat grey;
    //    cvtColor(screen, grey, CV_RGB2GRAY);

    //Detect edges using canny
    //    Mat canny_output;
    //    int thresh = 100;
    //    Canny(grey, canny_output, thresh, thresh * 2, 3);

    //Find contours
    vector<vector<Point> > contours;
    vector<Vec4i> hierarchy;
    findContours(purpleOnly, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));

    //Find hulls
    //        vector<vector<Point> >hull(contours.size());
    //        for (int i = 0; i < contours.size(); i++) {
    //            convexHull(Mat(contours[i]), hull[i], false);
    //        }

    //Find the bounding box of each contour and then combine them
    Rect bounds;
    Mat drawing = Mat::zeros(purpleOnly.size(), CV_8UC3);
    int j = 0;
    for (int i = 0; i < contours.size(); i++) {
        if (arcLength(contours[i], true) > 500) {
            Rect temp = boundingRect(contours[i]);
            rectangle(drawing, temp, Scalar(255, 0, 0), 2, 8);
            char height [4];
            sprintf(height, "%d", temp.height);
            putText(drawing, height, Point(temp.x, temp.y), FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 0, 0));
            if (j == 0) {
                bounds = temp;
            } else {
                bounds = bounds | temp;
            }
            j++;
        }
    }

    //Draw contours
    for (int i = 0; i < contours.size(); i++) {
        Scalar color = Scalar(0, 255, 0);
        drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point());
    }
    rectangle(drawing, bounds, Scalar(0, 0, 255), 2, 8);

    //We only enter this point if we have detected something we believe to be the game board
    if (j > 0) {
        //Tell us some info about bounds and make it our 'Region of Interest' image
        cout << "Board starts at: (" << bounds.x << "," << bounds.y << ")\n";
        fflush(stdout);
        Mat roi(screen, bounds);
        //Draw onto the image red grid lines in a 10 by 10 structure
        int tenthWidth = (bounds.width - 22) / 10;
        int tenthHeight = (bounds.height - 22) / 10;
        for (int x = 0; x < 11; x++) {
            Point p1 = Point((tenthWidth * x) + 11, 11);
            Point p2 = Point(((tenthWidth * x) + 11), (bounds.height - 11));
            Point p3 = Point(11, ((tenthHeight * x) + 11));
            Point p4 = Point((bounds.width - 11), ((tenthHeight * x) + 11));
            line(roi, p1, p2, Scalar(0, 0, 255), 3);
            line(roi, p3, p4, Scalar(0, 0, 255), 3);
        }
        //Create a Mat out each of the game pieces and store it in the vector
        Vector<Mat> gamePieces;
        for (int x = 0; x < 10; x++) {
            for (int y = 0; y < 10; y++) {
                Point p1 = Point((tenthWidth * x) + 11, (tenthHeight * y) + 11);
                gamePieces.push_back(Mat(roi, Rect(p1.x, p1.y, tenthHeight, tenthWidth)));
            }
        }
        //Display each piece in its own window
        for (int i = 0; i < gamePieces.size(); i++) {
            char name [4];
            sprintf(name, "w%d", i);
            namedWindow(name, CV_WINDOW_AUTOSIZE);
            imshow(name, gamePieces[i]);
        }
        //Declare a place to keep our game state in and some variables we need for reasoning.
        int gameState[10][10];
        int PURPLE_PIECE = 1, RED_PIECE = 2, GREEN_PIECE = 4, BLUE_PIECE = 8;
        Scalar PURPLE(120, 32, 116, 255), RED(95, 86, 199, 255), GREEN(30, 178, 23, 255), BLUE(199, 110, 33, 255);
        Vector<Scalar> possibleColours;
        possibleColours.push_back(PURPLE);
        possibleColours.push_back(RED);
        possibleColours.push_back(GREEN);
        possibleColours.push_back(BLUE);
        //Get the colour of the middle pixel in each image and save it in our 2D array
        for (int row = 0; row < 10; row++) {
            for (int column = 0; column < 10; column++) {
                Mat currentGamePeice = gamePieces[(column * 10) + row];
                Vec4b midPixelColour = currentGamePeice.at<Vec4b > (currentGamePeice.cols / 2, currentGamePeice.rows / 2);
                printf("[%d][%d] %d, %d, %d, %d\n", row, column, midPixelColour[0], midPixelColour[1], midPixelColour[2], midPixelColour[3]);
                fflush(stdout);
                Scalar current(midPixelColour);
                int bestMatch = INT_MAX;
                int pieceColour = 0;
                //for each possible colour compare against our current colour and
                //save the int for the best match in our game state
                for (int i = 0; i < possibleColours.size(); i++) {
                    Scalar testColour = possibleColours[i];
                    int db = current[0] - testColour[0];
                    int dg = current[1] - testColour[1];
                    int dr = current[2] - testColour[2];
                    float dist = sqrt((db * db) + (dg * dg) + (dr * dr));
                    printf("distance %i: %f \n", i, dist);
                    if (dist < bestMatch) {
                        bestMatch = dist;
                        printf("New best match: %d ", bestMatch);
                        if (i == 0) {
                            pieceColour = PURPLE_PIECE;
                            printf("purple.\n");
                        }
                        if (i == 1) {
                            pieceColour = RED_PIECE;
                            printf("red.\n");
                        }
                        if (i == 2) {
                            pieceColour = GREEN_PIECE;
                            printf("green.\n");
                        }
                        if (i == 3) {
                            pieceColour = BLUE_PIECE;
                            printf("blue.\n");
                        }
                    }
                }
                gameState[row][column] = pieceColour;
            }
        }
        cout << "Game state:\n";
        for (int x = 0; x < 10; x++) {
            for (int y = 0; y < 10; y++) {
                cout << gameState[x][y] << " ";
            }
            cout << "\n";
        }
        fflush(stdout);

        //display
        namedWindow("ROI", CV_WINDOW_AUTOSIZE);
        imshow("ROI", roi);
    }
    namedWindow("Contours", CV_WINDOW_AUTOSIZE);
    imshow("Contours", drawing);

    cvWaitKey(0);
    env->ReleaseIntArrayElements(data, elem, 0);
}

New code:
#include "gameplayer_GamePlayer.h"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <vector>

using namespace std;
using namespace cv;

//Possible game piece colours
const int PURPLE_PIECE = 1, RED_PIECE = 2, GREEN_PIECE = 4, BLUE_PIECE = 8;
const Scalar PURPLE(120, 32, 116, 255), RED(95, 86, 199, 255), GREEN(30, 178, 23, 255), BLUE(199, 110, 33, 255);

//Function prototypes
vector<vector<int> > getGameState(vector<Mat> gamePieceImages);

/*
 * Class:     gameplayer_GamePlayer
 * Method:    calcMove
 * Signature: ([I)[III
 */
JNIEXPORT jintArray JNICALL Java_gameplayer_GamePlayer_calcMove
(JNIEnv *env, jclass obj, jintArray data, jint width, jint height) {
    jsize len = env->GetArrayLength(data);
    jint* elem = env->GetIntArrayElements(data, 0);

    //Make the ARGB image out of the data
    Mat screen(height, width, CV_8UC4, elem);

    //Filter for the purple border (Scalars are made (B,G,R,A))
    Mat purpleOnly;
    inRange(screen, Scalar(150, 0, 150, 0), Scalar(255, 50, 255, 255), purpleOnly);

    //Find contours to get all the bits of the border since the blue gates splits them up
    vector<vector<Point> > contours;
    vector<Vec4i> hierarchy;
    findContours(purpleOnly, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));

    //Find the bounding box of each contour and then combine them to get the full game board
    Rect bounds;
    int j = 0;
    for (int i = 0; i < contours.size(); i++) {
        if (arcLength(contours[i], true) > 500) {
            bounds = j++ == 0 ? boundingRect(contours[i]) : bounds | boundingRect(contours[i]);
        }
    }
    
    if (j > 0) {
        //We only enter this point if we have detected something we believe to be the game board
        //Make the bounding rectangle we worked out our 'Region of Interest' image
        Mat roi(screen, bounds);
        //Work out the size of each game piece which is a tenth the image minus the border
        int tenthWidth = (bounds.width - 22) / 10;
        int tenthHeight = (bounds.height - 22) / 10;
        //Create a Mat containing each of the game pieces and store it in the vector
        vector<Mat> gamePieces;
        for (int x = 0; x < 10; x++) {
            for (int y = 0; y < 10; y++) {
                Point p1 = Point((tenthWidth * x) + 11, (tenthHeight * y) + 11);
                gamePieces.push_back(Mat(roi, Rect(p1.x, p1.y, tenthHeight, tenthWidth)));
            }
        }
        //Get our game state from the pieces
        vector<vector<int> > gameState = getGameState(gamePieces);

        cout << "Game state:\n";
        for (int x = 0; x < 10; x++) {
            for (int y = 0; y < 10; y++) {
                cout << gameState[x][y] << " ";
            }
            cout << "\n";
        }
        fflush(stdout);
    }
    env->ReleaseIntArrayElements(data, elem, 0);
}

vector<vector<int> > getGameState(vector<Mat> gamePieceImages) {
    //Declare a place to keep our game state in and some variables we need for reasoning.
    vector<vector<int> > gameState(10, vector<int> (10));
    vector<Scalar> possibleColours;
    possibleColours.push_back(PURPLE);
    possibleColours.push_back(RED);
    possibleColours.push_back(GREEN);
    possibleColours.push_back(BLUE);
    //Get the colour of the middle pixel in each image and save it in our 2D array
    for (int row = 0; row < 10; row++) {
        for (int column = 0; column < 10; column++) {
            Mat currentGamePeice = gamePieceImages[(column * 10) + row];
            Vec4b midPixelColour = currentGamePeice.at<Vec4b > (currentGamePeice.cols / 2, currentGamePeice.rows / 2);
            Scalar current(midPixelColour);
            int bestMatch = INT_MAX;
            //for each possible colour compare against our current colour and
            //save the int for the best match in our game state
            for (int i = 0; i < possibleColours.size(); i++) {
                Scalar testColour = possibleColours[i];
                int db = current[0] - testColour[0];
                int dg = current[1] - testColour[1];
                int dr = current[2] - testColour[2];
                int dist = sqrt((db * db) + (dg * dg) + (dr * dr));
                if (dist < bestMatch) {
                    bestMatch = dist;
                    switch (i) {
                        case 0:
                            gameState[row][column] = PURPLE_PIECE;
                            break;
                        case 1:
                            gameState[row][column] = RED_PIECE;
                            break;
                        case 2:
                            gameState[row][column] = GREEN_PIECE;
                            break;
                        case 3:
                            gameState[row][column] = BLUE_PIECE;
                            break;
                    }
                }
            }
        }
    }
    return gameState;
}

// <editor-fold desc="Interesting functions that I'm not using just now">
//Get greyscale image
//    Mat grey;
//    cvtColor(screen, grey, CV_RGB2GRAY);

//Detect edges using canny
//    Mat canny_output;
//    int thresh = 100;
//    Canny(grey, canny_output, thresh, thresh * 2, 3);

//Find hulls
//    vector<vector<Point> >hull(contours.size());
//    for (int i = 0; i < contours.size(); i++) {
//        convexHull(Mat(contours[i]), hull[i], false);
//    }
// </editor-fold>
Ok it may not look that amazing when laid out here but trust me, having the code in separate methods will make life a lot easier. Oh and you may have noticed I've left in one call to the cout loop. This will be removed once we actually do something with the game state but for now we just print it out to see it's working.

Next up, Step 2!

Code as promised.

No comments:

Post a Comment