OpenCV和C++ - 形状和道路标志检测

3
我需要编写一个程序来检测三种道路标志(限速、禁止停车和警示)。我知道如何使用HoughCircles检测圆形,但是我有多张图片,每张图片的HoughCircles参数不同。有没有一种普遍的方法可以检测圆形,而无需为每张图片更改参数?
此外,我需要检测三角形(警示标志),因此我正在寻找一种通用形状检测器。您有任何建议/代码可以帮助我完成这个任务吗?
最后,为了检测限速标志上的数字,我想使用SIFT,并将图像与一些模板进行比较,以识别标志上的数字。这是一个好的方法吗?
感谢您的回答!
2个回答

4

我知道这是一个相当古老的问题,但我曾经遇到过同样的问题,现在让我向您展示我是如何解决它的。 下面的图片显示了opencv程序显示的一些最准确的结果。 在下面的图片中,检测到的街道标志用三种不同的颜色圈出,以区分三种类型的街道标志(警告、禁止停车、限速)。

  • 红色表示警告标志
  • 蓝色表示禁止停车标志
  • 品红色表示限速标志

速度限制值以绿色字体写在限速标志上方。

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

正如您所看到的,该程序表现良好,能够检测和区分三种标志,并在限速标志情况下识别速度限制值。在图像中存在一些不属于其中一类的标志时,所有操作都是在不计算太多误报的情况下完成的。为了实现这一结果,软件分三个主要步骤进行检测。
第一个步骤涉及基于颜色的方法,其中检测图像中的红色物体并提取其区域以进行分析。此步骤特别有用,以防止检测到误报,因为仅处理图像的一小部分。
第二步采用机器学习算法:具体来说,我们使用级联分类器来计算检测。这一操作首先需要训练分类器,然后在后期使用它们来检测标志。
在最后一步中,通过使用k最近邻算法的机器学习算法读取限速标志内部的速度限制值。现在我们将详细介绍每个步骤。
基于颜色的步骤: 由于街道标志始终被红色边框环绕,因此我们可以只取出和分析检测到红色物体的区域。为了选择红色物体,我们考虑红色颜色的所有范围:即使这可能会产生一些误报,它们将在下一步轻松丢弃。
inRange(image, Scalar(0, 70, 50), Scalar(10, 255, 255), mask1);
inRange(image, Scalar(170, 70, 50), Scalar(180, 255, 255), mask2);

在下面的图像中,我们可以看到使用此方法检测到的红色物体的示例。 enter image description here 在找到红色像素后,我们可以使用聚类算法将它们聚集在一起以查找区域,我使用的是这种方法。
partition(<#_ForwardIterator __first#>, _ForwardIterator __last, <#_Predicate __pred#>)

执行此方法后,我们可以将同一簇中的所有点保存在一个向量中(每个簇一个向量),并提取代表下一步要分析的区域的边界框。
HAAR级联分类器用于标志检测
这是真正的检测步骤,其中检测到街道标志。为了执行级联分类器,第一步是构建正面和负面图像数据集。现在我将解释如何构建自己的图像数据集。 首先要注意的是,我们需要训练三种不同的Haar级联分类器,以区分我们必须检测的三种类型的标志,因此我们必须针对每种标志重复以下步骤。
我们需要两个数据集:一个用于正样本(必须是包含我们要检测的路标的图像集),另一个用于负样本,可以是任何没有街道标志的图像。 在收集了100张正样本和200张负样本的图像后,我们需要编写两个文本文件:
  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
    
使用下面的命令行,我们生成包含所有软件从正样本中检索到的信息的.vect文件。
opencv_createsamples -info sign.info -num 100 -w 50 -h 50 -vec signs.vec

接下来,我们使用以下命令训练级联分类器:

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

级数的数量表示将生成多少个分类器以构建级联。 在此过程结束时,我们获得一个名为cascade.xml的文件,该文件将被CascadeClassifier程序用于检测图像中的对象。 现在,我们已经训练好了算法,并且我们可以为每种类型的街道标志声明一个CascadeClassifier,然后通过它来检测图像中的标志。

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

这种方法会为每个被检测到的对象创建一个矩形。需要注意的是,与所有机器学习算法一样,为了表现良好,我们需要在数据集中拥有大量样本。我构建的数据集并不是非常大,因此在某些情况下无法检测到所有标志。这通常发生在图像中小部分交通标志不可见的情况下,例如下面的警告标志:

enter image description here

我扩展了我的数据集,使得在没有太多错误的情况下获得了相当准确的结果。

限速值检测

与交通标志检测一样,我也使用了机器学习算法,但采用了不同的方法。经过一些尝试,我意识到OCR(tesseract)解决方案表现不佳,因此决定自己构建OCR软件。

对于机器学习算法,我使用以下包含一些限速值的图像作为训练数据:

enter image description here

训练数据的数量很少。但是,由于在限速标志中所有字母都具有相同的字体,因此这不是一个巨大的问题。为了准备训练数据,我用OpenCV编写了一小段代码。它执行以下操作:

  1. 加载左侧的图像;
  2. 选择数字(显然通过轮廓查找并对字母的面积和高度应用约束以避免误检测)。
  3. 在一个字母周围绘制边界矩形,并等待手动按键。此时用户自行按下与框中字母相对应的数字键。
  4. 一旦按下相应的数字键,它会将100个像素值保存在一个数组中,并将手动输入的数字保存在另一个数组中。
  5. 最终,它将两个数组分别保存在单独的txt文件中。

在手动数字分类后,训练数据(train.png)中的所有数字都被手动标记,该图像将如下所示。 enter image description here

现在我们进入训练和测试部分。

对于训练,我们执行以下操作:

  1. 加载我们之前保存的txt文件
  2. 创建我们将要使用的分类器实例(KNearest)
  3. 然后使用KNearest.train函数来训练数据

现在进行检测:

  1. 我们加载检测到的限速标志图像。
  2. 像之前一样处理图像,并使用轮廓方法提取每个数字。
  3. 为其绘制边界框,然后调整大小为10x10,并将其像素值存储在一个数组中,就像之前做的那样。
  4. 然后我们使用KNearest.find_nearest()函数查找与我们给出的最接近的项。
    并且它能够识别正确的数字。

我在许多图像上测试了这个小型OCR,仅使用这个小数据集,我获得了约90%的准确度。

代码

以下是我的所有openCv c ++代码,按照我的指示,您应该能够达到我的结果。

#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;
}

你能告诉我如何创建级联XML文件吗? - user7697718
在“HAAR级联分类器用于标志检测”部分中有解释!有什么不清楚的吗? - Giulio Pettenuzzo
我看到了,但是我找不到opencv_createsamples.exe和opencv_traincascade.exe。这个可执行文件是你创建的吗? - user7697718
我已经成功训练了自己的级联模型,但是在你的代码中我看不到你在加载模型后如何使用它。你能给我一些提示吗? - user7697718

0

也许你应该尝试实现RANSAC算法,如果你正在使用彩色图像,获取红色通道可能是个好主意(如果你在欧洲),因为速度限制标志被一个红色圆圈(或者我认为是一个细白色的圆圈)所包围。

为此,你需要对图像进行滤波以获取边缘(Canny滤波器)。

以下是一些有用的链接:

OpenCV detect partial circle with noise

https://hal.archives-ouvertes.fr/hal-00982526/document

最后,对于数字检测,我认为它还不错。另一种方法是使用类似Viola-Jones算法的东西来检测信号,使用预训练的现有模型...这取决于你!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接