1

I have the below C++ code, which aims to detect shapes from a pre-designated image and draw around the shapes' perimeters. However, I wish to take this to the next step, and track the shapes from a camera feed rather than just an image. However, I am not to familiar with how I could make this transition.

#include <opencv2\opencv.hpp>
#include <opencv2\highgui\highgui.hpp>

int main()
{

    IplImage* img = cvLoadImage("C:/Users/Ayush/Desktop/FindingContours.png");

    //show the original image
    cvNamedWindow("Raw");
    cvShowImage("Raw", img);

    //converting the original image into grayscale
    IplImage* imgGrayScale = cvCreateImage(cvGetSize(img), 8, 1);
    cvCvtColor(img, imgGrayScale, CV_BGR2GRAY);

    //thresholding the grayscale image to get better results
    cvThreshold(imgGrayScale, imgGrayScale, 128, 255, CV_THRESH_BINARY);

    CvSeq* contours;  //hold the pointer to a contour in the memory block
    CvSeq* result;   //hold sequence of points of a contour
    CvMemStorage *storage = cvCreateMemStorage(0); //storage area for all contours

    //finding all contours in the image
    cvFindContours(imgGrayScale, storage, &contours, sizeof(CvContour), CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, cvPoint(0, 0));

    //iterating through each contour
    while (contours) {
        //obtain a sequence of points of contour, pointed by the variable 'contour'
        result = cvApproxPoly(contours, sizeof(CvContour), storage, CV_POLY_APPROX_DP, cvContourPerimeter(contours)*0.02, 0);

        //if there are 3  vertices  in the contour(It should be a triangle)
        if (result->total == 3) {
            //iterating through each point
            CvPoint *pt[3];
            for (int i = 0; i < 3; i++) {
                pt[i] = (CvPoint*)cvGetSeqElem(result, i);
            }

            //drawing lines around the triangle
            cvLine(img, *pt[0], *pt[1], cvScalar(255, 0, 0), 4);
            cvLine(img, *pt[1], *pt[2], cvScalar(255, 0, 0), 4);
            cvLine(img, *pt[2], *pt[0], cvScalar(255, 0, 0), 4);

        }

        //if there are 4 vertices in the contour(It should be a quadrilateral)
        else if (result->total == 4) {
            //iterating through each point
            CvPoint *pt[4];
            for (int i = 0; i < 4; i++) {
                pt[i] = (CvPoint*)cvGetSeqElem(result, i);
            }

            //drawing lines around the quadrilateral
            cvLine(img, *pt[0], *pt[1], cvScalar(0, 255, 0), 4);
            cvLine(img, *pt[1], *pt[2], cvScalar(0, 255, 0), 4);
            cvLine(img, *pt[2], *pt[3], cvScalar(0, 255, 0), 4);
            cvLine(img, *pt[3], *pt[0], cvScalar(0, 255, 0), 4);
        }

        //if there are 7  vertices  in the contour(It should be a heptagon)
        else if (result->total == 7) {
            //iterating through each point
            CvPoint *pt[7];
            for (int i = 0; i < 7; i++) {
                pt[i] = (CvPoint*)cvGetSeqElem(result, i);
            }

            //drawing lines around the heptagon
            cvLine(img, *pt[0], *pt[1], cvScalar(0, 0, 255), 4);
            cvLine(img, *pt[1], *pt[2], cvScalar(0, 0, 255), 4);
            cvLine(img, *pt[2], *pt[3], cvScalar(0, 0, 255), 4);
            cvLine(img, *pt[3], *pt[4], cvScalar(0, 0, 255), 4);
            cvLine(img, *pt[4], *pt[5], cvScalar(0, 0, 255), 4);
            cvLine(img, *pt[5], *pt[6], cvScalar(0, 0, 255), 4);
            cvLine(img, *pt[6], *pt[0], cvScalar(0, 0, 255), 4);
        }

        //obtain the next contour
        contours = contours->h_next;
    }

    //show the image in which identified shapes are marked   
    cvNamedWindow("Tracked");
    cvShowImage("Tracked", img);

    cvWaitKey(0); //wait for a key press

    //cleaning up
    cvDestroyAllWindows();
    cvReleaseMemStorage(&storage);
    cvReleaseImage(&img);
    cvReleaseImage(&imgGrayScale);

    return 0;
}

Any help in this matter is more than appreciated. Thank you!

Dan Mašek
  • 17,852
  • 6
  • 57
  • 85
Crazed
  • 77
  • 12
  • If you're using C++, I'd start by rewriting this to use the C++ OpenCV API, instead of this old C one. – Dan Mašek Jul 21 '17 at 18:55
  • Then, refactor this, so you have a function which takes an image, does the processing, and returns the processed image (basically what you have between those two calls to `cvShowIMage`. | Then consider that a video is just a sequence of images, so you just keep repeating "get next frame", "process it", "do something with the result". – Dan Mašek Jul 21 '17 at 18:59
  • I've tried making some changes, but I believe some elements are still innately wrong and there are probably more than a few other issues. – Crazed Jul 21 '17 at 19:09
  • Ya, use `vector`s for the contours and hierarchy -- check out [this OpenCV tutorial](http://docs.opencv.org/2.4/doc/tutorials/imgproc/shapedescriptors/find_contours/find_contours.html) – Dan Mašek Jul 21 '17 at 19:16
  • Have a look at this sample: https://github.com/opencv/opencv/blob/master/samples/cpp/contours2.cpp#L85 – Dan Mašek Jul 21 '17 at 19:24
  • I edited my code again, and understand your examples, but am still having trouble in this application. – Crazed Jul 21 '17 at 19:37
  • Maybe something like this: https://pastebin.com/tA4YZXFE Don't really feel like writing it up as a proper answer at this point tho. | Amongst other, notice how I split it up into several smaller functions, and replaced those sequences of calls to `line` by a single call to `polylines`. – Dan Mašek Jul 21 '17 at 20:17
  • Wow - that is really well written! I am slowly trying to understand each bit written. Unfortunately, it seems like `findContours` causes the program to crash, but that is something I can tackle and you have given me a lot I can work with. – Crazed Jul 21 '17 at 20:48
  • See this link https://www.learnopencv.com/object-tracking-using-opencv-cpp-python/ – svtag Jul 21 '17 at 20:50
  • If it's crashing, make a new question about it, I suspect it's a somewhat different problem which only became apparent once you switched to using the C++ API. Make sure you link with a version of OpenCV built for your version of compiler, and make sure you use the debug lib when in debug mode, and runtime one otherwise. – Dan Mašek Jul 21 '17 at 20:57
  • @Crazed I modified the code a little bit from that pastebin example, so that it more closely corresponds to what you had there. I've also taken the liberty to revert the code you show to the original version, so that the answer makes sense. You've made a good attempt at updating the code yourself tho. If there are some parts of the answer unclear, let me know. – Dan Mašek Jul 21 '17 at 22:08
  • This answer is incredibly clear- all of it makes complete sense as I read through it after looking at the code myself for a while. I'm still trying to solve the "crash" issue (more accurately, the issue is a breakpoint trigger due to corruption of the heap, but this varies each time I run it), but otherwise it all makes sense. Thank you for all the time you put in trying to help me as well as make sure I understand everything going on. – Crazed Jul 21 '17 at 22:13
  • @Crazed No problem, you're welcome :) A heap corruption, that definitely stinks of mixing runtimes. Which version of OpenCV are you using? Where did you get it from (download some package or compile yourself?) But as I said, it would be better to post a new question about this. – Dan Mašek Jul 21 '17 at 22:32
  • I did end up posting a new [question](https://stackoverflow.com/questions/45247914/opencv-cvfindcontours-triggers-breakpoint), where I detail all of this (I have OpenCV 3.2 from the SourceForge download link). However, considering you've already done quite a bit in the ways of helping me, I hope to find a solution on my own grounds. – Crazed Jul 21 '17 at 23:02

1 Answers1

1

If you're using C++, I'd start by rewriting this to use the C++ OpenCV API, instead of this old C one -- IMHO it's easier to use. In the process, refactor the code into smaller functions and decouple processing from I/O. Finally, consider that a video is just a sequence of images. If you can process one image, then you can process a video, one frame at a time.


So, how to achieve this. Let's start from the top, and write a main() function.

To read a video stream, we'll use cv::VideoCapture. We'll begin by initializing (and making sure that worked), and preparing some named windows to display the input and output frames.

Then we will start handling the individual frames in an infinite loop, quitting only when frame acquisition fails or the user hits an escape key. In each iteration we will:

  • Read the frame from the video stream (and make sure this succeeded)
  • Process the frame (we'll write a function for this later)
  • Display the raw and processed frames in our named windows
  • Wait a bit, and check if user pressed escape key, handling that appropriately

Code:

int main()
{
    cv::VideoCapture cap(0); // open the video camera no. 0

    if (!cap.isOpened())  // if not success, exit program
    {
        std::cout << "Cannot open the video cam\n";
        return -1;
    }

    cv::namedWindow("Original", CV_WINDOW_AUTOSIZE);
    cv::namedWindow("Tracked", CV_WINDOW_AUTOSIZE);

    // Process frames from the video stream...
    for(;;) {
        cv::Mat frame, result_frame;

        // read a new frame from video
        if (!cap.read(frame)) {
            std::cout << "Cannot read a frame from video stream\n";
            break;
        }

        process_frame(frame, result_frame);

        cv::imshow("Original", frame);
        cv::imshow("Tracked", result_frame);
        if (cv::waitKey(20) == 27) { // Quit on ESC
            break;
        }
    }

    return 0;
}

NB: The use of cv::waitKey at an appropriate time is essential for the GUI to work. Read the documentation carefully.


With that done, it's time to implement our process_frame function, but first, let's make some useful global typedefs.

In the C++ API, a contour is a std::vector of cv::Point objects, and since more than one contour can be detected, we also need a std::vector of contours. Similarly, hierarchy is represented as a std::vector of cv::Vec4i objects. (the "is" is a lie-to-children, as it could be other data types too, but this is not important right now).

typedef std::vector<cv::Point> contour_t;
typedef std::vector<contour_t> contour_vector_t;
typedef std::vector<cv::Vec4i> hierarchy_t;

Let's work on the function -- it needs to take two parameters:

  • Input frame (cv::Mat) which we don't want to modify, we just analyze it.
  • Output frame, into which we draw the result of processing.

We need to:

  • Copy the original frame into result, so that we can later draw over it.
  • Make a grayscale version using cv::cvtColor, so that we can
  • cv::threshold it, binarizing the image
  • cv::findContours on the binary image
  • Finally, process each detected contour (possibly drawing into the result frame).

Code:

void process_frame(cv::Mat const& frame, cv::Mat& result_frame)
{
    frame.copyTo(result_frame);

    cv::Mat feedGrayScale;
    cv::cvtColor(frame, feedGrayScale, cv::COLOR_BGR2GRAY);

    //thresholding the grayscale image to get better results
    cv::threshold(feedGrayScale, feedGrayScale, 128, 255, cv::THRESH_BINARY);

    contour_vector_t contours;
    hierarchy_t hierarchy;
    cv::findContours(feedGrayScale, contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
    for (size_t k(0); k < contours.size(); ++k) {
        process_contour(result_frame, contours[k]);
    }
}

Last step, function to process a single contour. It needs:

  • An image (cv::Mat) to draw on
  • A contour to work with

First, we want to approximate a polygon, using fraction of the perimeter length (we can use cv::arcLength to calculate that) as a parameter. We will continue by processing this approximated contour.

Next, we want to handle 3 specific cases: triangles, quadrilaterals and heptagons. We want to draw the contour of each of those using a different colour, otherwise we don't do anything. To draw the sequence of lines making up the contour, we can use cv::polylines.

Code:

void process_contour(cv::Mat& frame, contour_t const& contour)
{
    contour_t approx_contour;
    cv::approxPolyDP(contour, approx_contour, cv::arcLength(contour, true) * 0.02, true);

    cv::Scalar TRIANGLE_COLOR(255, 0, 0);
    cv::Scalar QUADRILATERAL_COLOR(0, 255, 0);
    cv::Scalar HEPTAGON_COLOR(0, 0, 255);

    cv::Scalar colour;
    if (approx_contour.size() == 3) {
        colour = TRIANGLE_COLOR;
    } else if (approx_contour.size() == 4) {
        colour = QUADRILATERAL_COLOR;
    } else if (approx_contour.size() == 7) {
        colour = HEPTAGON_COLOR;
    } else {
        return;
    }

    cv::Point const* points(&approx_contour[0]);
    int n_points(static_cast<int>(approx_contour.size()));

    polylines(frame, &points, &n_points, 1, true, colour, 4);
}

NB: std::vector is guaranteed to be continuous. That's why we can safely take a pointer by getting the address of the first element (&approx_contour[0]).


NB: Avoid using

using namespace std;
using namespace cv;

For more info, see Why is “using namespace std” considered bad practice?

Dan Mašek
  • 17,852
  • 6
  • 57
  • 85