Search code examples
c++opencvshapesobject-detection

OpenCV and C++ - Shape and road signs detection


I have to write a program that detect 3 types of road signs (speed limit, no parking and warnings). I know how to detect a circle using HoughCircles but I have several images and the parameters for HoughCircles are different for each image. There's a general way to detect circles without changing parameters for each image?

Moreover I need to detect triangle (warning signs) so I'm searching for a general shape detector. Have you any suggestions/code that can help me in this task?

Finally for detect the number on speed limit signs I thought to use SIFT and compare the image with some templates in order to identify the number on the sign. Could it be a good approach?

Thank you for the answer!


Solution

  • I know this is a pretty old question but I had been through the same problem and now I show you how I solved it. The following images show some of the most accurate results that are displayed by the opencv program. In the following images the street signs detected are circled with three different colors that distinguish the three kinds of street signs (warning, no parking, speed limit).

    • Red for warning signs
    • Blue for no parking signs
    • Fuchsia for speed limit signs

    The speed limit value is written in green above the speed limit signs

    [![example][1]][1]
    [![example][2]][2]
    [![example][3]][3]
    [![example][4]][4]
    

    As you can see the program performs quite well, it is able to detect and distinguish the three kinds of sign and to recognize the speed limit value in case of speed limit signs. Everything is done without computing too many false positives when, for instance, in the image there are some signs that do not belong to one of the three categories. In order to achieve this result the software computes the detection in three main steps. The first step involves a color based approach where the red objects in the image are detected and their region are extract to be analyzed. This step is particularly useful in order to prevent the detection of false positives, because only a small part of the image is processed. The second step works with a machine learning algorithm: in particular we use a Cascade Classifier to compute the detection. This operation firstly requires to train the classifiers and on a later stage to use them to detect the signs. In the last step the speed limit values inside the speed limit signs are read, also in this case through a machine learning algorithm but using the k-nearest neighbor algorithm. Now we are going to see in detail each step.

    COLOR BASED STEP

    Since the street signs are always circled by a red frame, we can afford to take out and analyze only the regions where the red objects are detected. In order to select the red objects, we consider all the ranges of the red color: even if this may produce some false positives, they will be easily discarded in the next steps.

    inRange(image, Scalar(0, 70, 50), Scalar(10, 255, 255), mask1);
    inRange(image, Scalar(170, 70, 50), Scalar(180, 255, 255), mask2);
    

    In the image below we can see an example of the red objects detected with this method.

    enter image description here

    After having found the red pixels we can gather them to find the regions using a clustering algorithm, I use the method

    partition(<#_ForwardIterator __first#>, _ForwardIterator __last, <#_Predicate __pred#>)
    

    After the execution of this method we can save all the points in the same cluster in a vector (one for each cluster) and extract the bounding boxes which represent the regions to be analyzed in the next step.

    HAAR CASCADE CLASSIFIERS FOR SIGNS DETECTION

    This is the real detection step where the street signs are detected. In order to perform a cascade classifier the first step consist in building a dataset of positives and negatives images. Now I explain how I have built my own datasets of images. The first thing to note is that we need to train three different Haar cascades in order to distinguish between the three kind of signs that we have to detect, hence we must repeat the following steps for each of the three kinds of sign.

    We need two datasets: one for the positive samples (which must be a set of images that contains the road signs that we are going to detect) and another one for the negative samples which can be any kind of image without street signs. After collecting a set of 100 images for the positive samples and a set of 200 images for the negatives in two different folders, we need to write two text files:

    1. Signs.info which contains a list of file names like the one below, one for each positive sample in the positive folder.

      pos/image_name.png 1 0 0 50 45
      

      Here, the numbers after the name represent respectively the number of street signs in the image, the coordinate of the upper left corner of the street sign, his height and his width.

    2. Bg.txt which contains a list of file names like the one below, one for each sign in the negative folder.

      neg/street15.png
      

    With the command line below we generate the .vect file which contains all the information that the software retrieves from the positive samples.

    opencv_createsamples -info sign.info -num 100 -w 50 -h 50 -vec signs.vec
    

    Afterwards we train the cascade classifier with the following command:

    opencv_traincascade -data data -vec signs.vec -bg bg.txt -numPos 60 -numNeg 200 -numStages 15 -w 50 -h 50 -featureType LBP
    

    where the number of stages indicates the number of classifiers that will be generated in order to build the cascade. At the end of this process we gain a file cascade.xml which will be used from the CascadeClassifier program in order to detect the objects in the image. Now we have trained our algorithm and we can declare a CascadeClassifier for each kind of street sign, than we detect the signs in the image through

    detectMultiScale(<#InputArray image#>, <#std::vector<Rect> &objects#>)
    

    this method creates a Rect around each object that has been detected. It is important to note that exactly as every machine learning algorithm, in order to perform well, we need a large number of samples in the dataset. The dataset that I have built, is not extremely large, thus in some situations it is not able to detect all the signs. This mostly happens when a small part of the street sign is not visible in the image like in the warning sign below:

    enter image description here

    I have expanded my dataset up to the point where I have obtained a fairly accurate result without too many errors.

    SPEED LIMIT VALUE DETECTION

    Like for the street signs detection also here I used a machine learning algorithm but with a different approach. After some work, I realized that an OCR (tesseract) solution does not perform well, so I decided to build my own ocr software.

    For the machine learning algorithm I took the image below as training data which contains some speed limit values:

    enter image description here

    The amount of training data is small. But, since in speed limit signs all letters have the same font, it is not a huge problem. To prepare the data for training, I made a small code in OpenCV. It does the following things:

    1. It loads the image on the left;
    2. It selects the digits (obviously by contour finding and applying constraints on area and height of letters to avoid false detections).
    3. It draws the bounding rectangle around one letter and it waits for the key to be manually pressed. This time the user presses the digit key corresponding to the letter in box by himself.
    4. Once the corresponding digit key is pressed, it saves 100 pixel values in an array and the correspondent manually entered digit in another array.
    5. Eventually it saves both the arrays in separate txt files.

    Following the manual digit classification all the digits in the train data( train.png) are manually labeled, and the image will look like the one below. enter image description here

    Now we enter into training and testing part.

    For training we do as follows:

    1. Load the txt files we already saved earlier
    2. Create an instance of classifier that we are going to use ( KNearest)
    3. Then we use KNearest.train function to train the data

    Now the detection:

    1. We load the image with the speed limit sign detected
    2. Process the image as before and extract each digit using contour methods
    3. Draw bounding box for it, then resize to 10x10, and store its pixel values in an array as done earlier.
    4. Then we use KNearest.find_nearest() function to find the nearest item to the one we gave.
      And it recognizes the correct digit.

    I tested this little OCR on many images, and just with this small dataset I have obtained an accuracy of about 90%.

    CODE

    Below I post all my openCv c++ code in a single class, following my instruction you should be able to achive my result.

    #include "opencv2/objdetect/objdetect.hpp"
    #include "opencv2/imgproc/imgproc.hpp"
    #include <iostream>
    #include <stdio.h>
    #include <cmath>
    #include <stdlib.h>
    #include "opencv2/core/core.hpp"
    #include "opencv2/highgui.hpp"
    #include <string.h>
    #include <opencv2/ml/ml.hpp>
    
    using namespace std;
    using namespace cv;
    
    std::vector<cv::Rect> getRedObjects(cv::Mat image);
    vector<Mat> detectAndDisplaySpeedLimit( Mat frame );
    vector<Mat> detectAndDisplayNoParking( Mat frame );
    vector<Mat> detectAndDisplayWarning( Mat frame );
    void trainDigitClassifier();
    string getDigits(Mat image);
    vector<Mat> loadAllImage();
    int getSpeedLimit(string speed);
    
    //path of the haar cascade files
    String no_parking_signs_cascade = "/Users/giuliopettenuzzo/Desktop/cascade_classifiers/no_parking_cascade.xml";
    String speed_signs_cascade = "/Users/giuliopettenuzzo/Desktop/cascade_classifiers/speed_limit_cascade.xml";
    String warning_signs_cascade = "/Users/giuliopettenuzzo/Desktop/cascade_classifiers/warning_cascade.xml";
    
    CascadeClassifier speed_limit_cascade;
    CascadeClassifier no_parking_cascade;
    CascadeClassifier warning_cascade;
    
    int main(int argc, char** argv)
    {
        //train the classifier for digit recognition, this require a manually train, read the report for more details
        trainDigitClassifier();
    
        cv::Mat sceneImage;
        vector<Mat> allImages = loadAllImage();
    
        for(int i = 0;i<=allImages.size();i++){
            sceneImage = allImages[i];
    
            //load the haar cascade files
            if( !speed_limit_cascade.load( speed_signs_cascade ) ){ printf("--(!)Error loading\n"); return -1; };
            if( !no_parking_cascade.load( no_parking_signs_cascade ) ){ printf("--(!)Error loading\n"); return -1; };
            if( !warning_cascade.load( warning_signs_cascade ) ){ printf("--(!)Error loading\n"); return -1; };
    
            Mat scene = sceneImage.clone();
    
            //detect the red objects
            std::vector<cv::Rect> allObj = getRedObjects(scene);
    
            //use the three cascade classifier for each object detected by the getRedObjects() method
            for(int j = 0;j<allObj.size();j++){
                Mat img = sceneImage(Rect(allObj[j]));
                vector<Mat> warningVec = detectAndDisplayWarning(img);
                if(warningVec.size()>0){
                    Rect box = allObj[j];
                }
                vector<Mat> noParkVec = detectAndDisplayNoParking(img);
                if(noParkVec.size()>0){
                    Rect box = allObj[j];
                }
                vector<Mat> speedLitmitVec = detectAndDisplaySpeedLimit(img);
                if(speedLitmitVec.size()>0){
                    Rect box = allObj[j];
                    for(int i = 0; i<speedLitmitVec.size();i++){
                        //get speed limit and skatch it in the image
                        int digit = getSpeedLimit(getDigits(speedLitmitVec[i]));
                        if(digit > 0){
                            Point point = box.tl();
                            point.y = point.y + 30;
                            cv::putText(sceneImage,
                                        "SPEED LIMIT " + to_string(digit),
                                        point,
                                        cv::FONT_HERSHEY_COMPLEX_SMALL,
                                        0.7,
                                        cv::Scalar(0,255,0),
                                        1,
                                        cv::CV__CAP_PROP_LATEST);
                        }
                    }
                }
            }
            imshow("currentobj",sceneImage);
            waitKey(0);
        }
    }
    
    /*
     *  detect the red object in the image given in the param,
     *  return a vector containing all the Rect of the red objects
     */
    std::vector<cv::Rect> getRedObjects(cv::Mat image)
    {
        Mat3b res = image.clone();
        std::vector<cv::Rect> result;
    
        cvtColor(image, image, COLOR_BGR2HSV);
    
        Mat1b mask1, mask2;
        //ranges of red color
        inRange(image, Scalar(0, 70, 50), Scalar(10, 255, 255), mask1);
        inRange(image, Scalar(170, 70, 50), Scalar(180, 255, 255), mask2);
    
        Mat1b mask = mask1 | mask2;
        Mat nonZeroCoordinates;
        vector<Point> pts;
    
        findNonZero(mask, pts);
        for (int i = 0; i < nonZeroCoordinates.total(); i++ ) {
            cout << "Zero#" << i << ": " << nonZeroCoordinates.at<Point>(i).x << ", " << nonZeroCoordinates.at<Point>(i).y << endl;
        }
    
        int th_distance = 2; // radius tolerance
    
         // Apply partition
         // All pixels within the radius tolerance distance will belong to the same class (same label)
        vector<int> labels;
    
         // With lambda function (require C++11)
        int th2 = th_distance * th_distance;
        int n_labels = partition(pts, labels, [th2](const Point& lhs, const Point& rhs) {
            return ((lhs.x - rhs.x)*(lhs.x - rhs.x) + (lhs.y - rhs.y)*(lhs.y - rhs.y)) < th2;
        });
    
         // You can save all points in the same class in a vector (one for each class), just like findContours
        vector<vector<Point>> contours(n_labels);
        for (int i = 0; i < pts.size(); ++i){
            contours[labels[i]].push_back(pts[i]);
        }
    
         // Get bounding boxes
        vector<Rect> boxes;
        for (int i = 0; i < contours.size(); ++i)
        {
            Rect box = boundingRect(contours[i]);
            if(contours[i].size()>500){//prima era 1000
                boxes.push_back(box);
    
                Rect enlarged_box = box + Size(100,100);
                enlarged_box -= Point(30,30);
    
                if(enlarged_box.x<0){
                    enlarged_box.x = 0;
                }
                if(enlarged_box.y<0){
                    enlarged_box.y = 0;
                }
                if(enlarged_box.height + enlarged_box.y > res.rows){
                    enlarged_box.height = res.rows - enlarged_box.y;
                }
                if(enlarged_box.width + enlarged_box.x > res.cols){
                    enlarged_box.width = res.cols - enlarged_box.x;
                }
    
                Mat img = res(Rect(enlarged_box));
                result.push_back(enlarged_box);
            }
         }
         Rect largest_box = *max_element(boxes.begin(), boxes.end(), [](const Rect& lhs, const Rect& rhs) {
             return lhs.area() < rhs.area();
         });
    
        //draw the rects in case you want to see them
         for(int j=0;j<=boxes.size();j++){
             if(boxes[j].area() > largest_box.area()/3){
                 rectangle(res, boxes[j], Scalar(0, 0, 255));
    
                 Rect enlarged_box = boxes[j] + Size(20,20);
                 enlarged_box -= Point(10,10);
    
                 rectangle(res, enlarged_box, Scalar(0, 255, 0));
             }
         }
    
         rectangle(res, largest_box, Scalar(0, 0, 255));
    
         Rect enlarged_box = largest_box + Size(20,20);
         enlarged_box -= Point(10,10);
    
         rectangle(res, enlarged_box, Scalar(0, 255, 0));
    
         return result;
    }
    
    /*
     *  code for detect the speed limit sign , it draws a circle around the speed limit signs
     */
    vector<Mat> detectAndDisplaySpeedLimit( Mat frame )
    {
        std::vector<Rect> signs;
        vector<Mat> result;
        Mat frame_gray;
    
        cvtColor( frame, frame_gray, CV_BGR2GRAY );
        //normalizes the brightness and increases the contrast of the image
        equalizeHist( frame_gray, frame_gray );
    
        //-- Detect signs
        speed_limit_cascade.detectMultiScale( frame_gray, signs, 1.1, 3, 0|CV_HAAR_SCALE_IMAGE, Size(30, 30) );
        cout << speed_limit_cascade.getFeatureType();
    
        for( size_t i = 0; i < signs.size(); i++ )
        {
            Point center( signs[i].x + signs[i].width*0.5, signs[i].y + signs[i].height*0.5 );
            ellipse( frame, center, Size( signs[i].width*0.5, signs[i].height*0.5), 0, 0, 360, Scalar( 255, 0, 255 ), 4, 8, 0 );
    
    
            Mat resultImage = frame(Rect(center.x - signs[i].width*0.5,center.y - signs[i].height*0.5,signs[i].width,signs[i].height));
            result.push_back(resultImage);
        }
        return result;
    }
    
    /*
     *  code for detect the warning sign , it draws a circle around the warning signs
     */
    vector<Mat> detectAndDisplayWarning( Mat frame )
    {
        std::vector<Rect> signs;
        vector<Mat> result;
        Mat frame_gray;
    
        cvtColor( frame, frame_gray, CV_BGR2GRAY );
        equalizeHist( frame_gray, frame_gray );
    
        //-- Detect signs
        warning_cascade.detectMultiScale( frame_gray, signs, 1.1, 3, 0|CV_HAAR_SCALE_IMAGE, Size(30, 30) );
        cout << warning_cascade.getFeatureType();
        Rect previus;
    
    
        for( size_t i = 0; i < signs.size(); i++ )
        {
            Point center( signs[i].x + signs[i].width*0.5, signs[i].y + signs[i].height*0.5 );
            Rect newRect = Rect(center.x - signs[i].width*0.5,center.y - signs[i].height*0.5,signs[i].width,signs[i].height);
            if((previus & newRect).area()>0){
                previus = newRect;
            }else{
                ellipse( frame, center, Size( signs[i].width*0.5, signs[i].height*0.5), 0, 0, 360, Scalar( 0, 0, 255 ), 4, 8, 0 );
                Mat resultImage = frame(newRect);
                result.push_back(resultImage);
                previus = newRect;
            }
        }
        return result;
    }
    
    /*
     *  code for detect the no parking sign , it draws a circle around the no parking signs
     */
    vector<Mat> detectAndDisplayNoParking( Mat frame )
    {
        std::vector<Rect> signs;
        vector<Mat> result;
        Mat frame_gray;
    
        cvtColor( frame, frame_gray, CV_BGR2GRAY );
        equalizeHist( frame_gray, frame_gray );
    
        //-- Detect signs
        no_parking_cascade.detectMultiScale( frame_gray, signs, 1.1, 3, 0|CV_HAAR_SCALE_IMAGE, Size(30, 30) );
        cout << no_parking_cascade.getFeatureType();
        Rect previus;
    
        for( size_t i = 0; i < signs.size(); i++ )
        {
            Point center( signs[i].x + signs[i].width*0.5, signs[i].y + signs[i].height*0.5 );
            Rect newRect = Rect(center.x - signs[i].width*0.5,center.y - signs[i].height*0.5,signs[i].width,signs[i].height);
            if((previus & newRect).area()>0){
                previus = newRect;
            }else{
                ellipse( frame, center, Size( signs[i].width*0.5, signs[i].height*0.5), 0, 0, 360, Scalar( 255, 0, 0 ), 4, 8, 0 );
                Mat resultImage = frame(newRect);
                result.push_back(resultImage);
                previus = newRect;
            }
        }
        return result;
    }
    
    /*
     *  train the classifier for digit recognition, this could be done only one time, this method save the result in a file and
     *  it can be used in the next executions
     *  in order to train user must enter manually the corrisponding digit that the program shows, press space if the red box is just a point (false positive)
     */
    void trainDigitClassifier(){
        Mat thr,gray,con;
        Mat src=imread("/Users/giuliopettenuzzo/Desktop/all_numbers.png",1);
        cvtColor(src,gray,CV_BGR2GRAY);
        threshold(gray,thr,125,255,THRESH_BINARY_INV); //Threshold to find contour
        imshow("ci",thr);
        waitKey(0);
        thr.copyTo(con);
    
        // Create sample and label data
        vector< vector <Point> > contours; // Vector for storing contour
        vector< Vec4i > hierarchy;
        Mat sample;
        Mat response_array;
        findContours( con, contours, hierarchy,CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE ); //Find contour
    
        for( int i = 0; i< contours.size(); i=hierarchy[i][0] ) // iterate through first hierarchy level contours
        {
            Rect r= boundingRect(contours[i]); //Find bounding rect for each contour
            rectangle(src,Point(r.x,r.y), Point(r.x+r.width,r.y+r.height), Scalar(0,0,255),2,8,0);
            Mat ROI = thr(r); //Crop the image
            Mat tmp1, tmp2;
            resize(ROI,tmp1, Size(10,10), 0,0,INTER_LINEAR ); //resize to 10X10
            tmp1.convertTo(tmp2,CV_32FC1); //convert to float
    
            imshow("src",src);
    
            int c=waitKey(0); // Read corresponding label for contour from keyoard
            c-=0x30;     // Convert ascii to intiger value
            response_array.push_back(c); // Store label to a mat
            rectangle(src,Point(r.x,r.y), Point(r.x+r.width,r.y+r.height), Scalar(0,255,0),2,8,0);
            sample.push_back(tmp2.reshape(1,1)); // Store  sample data
        }
    
        // Store the data to file
        Mat response,tmp;
        tmp=response_array.reshape(1,1); //make continuous
        tmp.convertTo(response,CV_32FC1); // Convert  to float
    
        FileStorage Data("TrainingData.yml",FileStorage::WRITE); // Store the sample data in a file
        Data << "data" << sample;
        Data.release();
    
        FileStorage Label("LabelData.yml",FileStorage::WRITE); // Store the label data in a file
        Label << "label" << response;
        Label.release();
        cout<<"Training and Label data created successfully....!! "<<endl;
    
        imshow("src",src);
        waitKey(0);
    
    
    }
    
    /*
     *  get digit from the image given in param, using the classifier trained before
     */
    string getDigits(Mat image)
    {
        Mat thr1,gray1,con1;
        Mat src1 = image.clone();
        cvtColor(src1,gray1,CV_BGR2GRAY);
        threshold(gray1,thr1,125,255,THRESH_BINARY_INV); // Threshold to create input
        thr1.copyTo(con1);
    
    
        // Read stored sample and label for training
        Mat sample1;
        Mat response1,tmp1;
        FileStorage Data1("TrainingData.yml",FileStorage::READ); // Read traing data to a Mat
        Data1["data"] >> sample1;
        Data1.release();
    
        FileStorage Label1("LabelData.yml",FileStorage::READ); // Read label data to a Mat
        Label1["label"] >> response1;
        Label1.release();
    
    
        Ptr<ml::KNearest>  knn(ml::KNearest::create());
    
        knn->train(sample1, ml::ROW_SAMPLE,response1); // Train with sample and responses
        cout<<"Training compleated.....!!"<<endl;
    
        vector< vector <Point> > contours1; // Vector for storing contour
        vector< Vec4i > hierarchy1;
    
        //Create input sample by contour finding and cropping
        findContours( con1, contours1, hierarchy1,CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE );
        Mat dst1(src1.rows,src1.cols,CV_8UC3,Scalar::all(0));
        string result;
    
        for( int i = 0; i< contours1.size(); i=hierarchy1[i][0] ) // iterate through each contour for first hierarchy level .
        {
            Rect r= boundingRect(contours1[i]);
            Mat ROI = thr1(r);
            Mat tmp1, tmp2;
            resize(ROI,tmp1, Size(10,10), 0,0,INTER_LINEAR );
            tmp1.convertTo(tmp2,CV_32FC1);
            Mat bestLabels;
            float p=knn -> findNearest(tmp2.reshape(1,1),4, bestLabels);
            char name[4];
            sprintf(name,"%d",(int)p);
            cout << "num = " << (int)p;
            result = result + to_string((int)p);
    
            putText( dst1,name,Point(r.x,r.y+r.height) ,0,1, Scalar(0, 255, 0), 2, 8 );
        }
    
        imwrite("dest.jpg",dst1);
        return  result ;
    }
    /*
     *  from the digits detected, it returns a speed limit if it is detected correctly, -1 otherwise
     */
    int getSpeedLimit(string numbers){
        if ((numbers.find("30") != std::string::npos) || (numbers.find("03") != std::string::npos)) {
            return 30;
        }
        if ((numbers.find("50") != std::string::npos) || (numbers.find("05") != std::string::npos)) {
            return 50;
        }
        if ((numbers.find("80") != std::string::npos) || (numbers.find("08") != std::string::npos)) {
            return 80;
        }
        if ((numbers.find("70") != std::string::npos) || (numbers.find("07") != std::string::npos)) {
            return 70;
        }
        if ((numbers.find("90") != std::string::npos) || (numbers.find("09") != std::string::npos)) {
            return 90;
        }
        if ((numbers.find("100") != std::string::npos) || (numbers.find("001") != std::string::npos)) {
            return 100;
        }
        if ((numbers.find("130") != std::string::npos) || (numbers.find("031") != std::string::npos)) {
            return 130;
        }
        return -1;
    }
    
    /*
     *  load all the image in the file with the path hard coded below
     */
    vector<Mat> loadAllImage(){
        vector<cv::String> fn;
        glob("/Users/giuliopettenuzzo/Desktop/T1/dataset/*.jpg", fn, false);
    
        vector<Mat> images;
        size_t count = fn.size(); //number of png files in images folder
        for (size_t i=0; i<count; i++)
            images.push_back(imread(fn[i]));
        return images;
    }