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

OpenCV and C++ - Shape and road signs detection

我必须编写一个程序来检测 3 种道路标志(限速、禁止停车和警告)。我知道如何使用 HoughCircles 检测圆,但我有几张图像,并且 HoughCircles 的参数对于每张图像都不同。有一种通用的方法可以在不更改每个图像的参数的情况下检测圆圈吗?

此外,我需要检测三角形(警告标志),因此我正在寻找通用形状检测器。你有什么 suggestions/code 可以帮助我完成这项任务吗?

最后为了检测限速标志上的数字,我想使用 SIFT 并将图像与一些模板进行比较,以便识别标志上的数字。这会是个好方法吗?

谢谢您的回答!

也许你应该尝试实现 ransac 算法,如果你使用的是彩色图像,那么只获得红色通道是个好主意(如果你在欧洲),因为速度限制被红色 cricle 包围(或者我也认为是薄白色。

为此,您需要过滤图像以获得边缘(canny 过滤器)。

这里有一些有用的链接:

OpenCV detect partial circle with noise

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

我认为终于可以检测数字了。其他方法是使用像 Viola-Jones 算法这样的东西来检测信号,使用预先训练的现有模型......这取决于你!

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

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

限速标志上方绿色写着限速值

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

正如你所看到的,程序运行良好,它能够检测和区分三种标志,并在限速标志的情况下识别限速值。一切都是在不计算太多误报的情况下完成的,例如,当图像中有一些不属于这三个类别之一的标志时。 为了达到这个结果,软件分三个主要步骤计算检测。 第一步涉及基于颜色的方法,其中检测图像中的红色对象并提取其区域进行分析。此步骤对于防止误报检测特别有用,因为只处理了图像的一小部分。 第二步使用机器学习算法:特别是我们使用级联分类器来计算检测。此操作首先需要训练 classifier,然后在稍后阶段使用它们来检测标志。 在最后一步中,读取限速标志内的限速值,在本例中也是通过机器学习算法读取的,但使用的是 k 最近邻算法。 现在我们将详细了解每个步骤。

基于颜色的步骤

由于路牌总是被红框圈起来,我们可以只取出检测到红色物体的区域进行分析。 为了select红色物体,我们考虑了红色的所有范围:即使这可能会产生一些误报,但在接下来的步骤中它们很容易被丢弃。

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

在下图中,我们可以看到使用此方法检测到的红色物体的示例。

找到红色像素后,我们可以收集它们以使用聚类算法找到区域,我使用方法

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

执行此方法后,我们可以将同一簇中的所有点保存在一个向量中(每个簇一个)并提取代表 下一步要分析的区域。

用于标志检测的 HAAR 级联分类器

这是检测路牌的真正检测步骤。为了执行级联 classifier,第一步包括构建正片和负片图像的数据集。现在我将解释我是如何构建自己的图像数据集的。 首先要注意的是,我们需要训练三个不同的 Haar 级联,以便区分我们要检测的三种标志,因此我们必须对三种标志中的每一种重复以下步骤。

我们需要两个数据集:一个用于正样本(必须是一组包含我们要检测的路标的图像),另一个用于负样本,可以是任何一种没有图像的图像路牌。 在两个不同的文件夹中收集了一组 100 张正样本图像和一组 200 张负样本图像后,我们需要编写两个文本文件:

  1. Signs.info 其中包含如下所示的文件名列表, positive 文件夹中每个正样本一个。

    pos/image_name.png 1 0 0 50 45
    

    这里名字后面的数字分别代表编号 图像中的路标,左上角的坐标 路牌一角,他的高和他的宽。

  2. Bg.txt 其中包含一个文件名列表,如下所示,一个 对于否定文件夹中的每个符号。

    neg/street15.png
    

我们使用下面的命令行生成 .vect 文件,其中包含软件从正样本中检索到的所有信息。

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

之后我们使用以下命令训练级联 classifier:

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

其中阶段数表示 class 将生成以构建级联的生成器的数量。 在此过程结束时,我们将获得一个文件 cascade.xml,CascadeClassifier 程序将使用该文件来检测图像中的对象。 现在我们已经训练了我们的算法,我们可以为每种路牌声明一个 CascadeClassifier,然后我们通过

检测图像中的标志
detectMultiScale(<#InputArray image#>, <#std::vector<Rect> &objects#>)

此方法在已检测到的每个对象周围创建一个 Rect。 重要的是要注意,就像每个机器学习算法一样,为了表现良好,我们需要数据集中的大量样本。我建立的数据集不是很大,因此在某些情况下它无法检测到所有迹象。这主要发生在图像中看不到路标的一小部分时,如下面的警告标志所示:

我已经扩展了我的数据集,直到我获得了一个相当准确的结果而没有 错误太多。

限速值检测

就像这里的路标检测一样,我使用了机器学习算法,但采用了不同的方法。经过一些工作,我意识到 OCR (tesseract) 解决方案性能不佳,因此我决定构建自己的 ocr 软件。

对于机器学习算法,我将下图作为训练数据,其中包含一些速度限制值:

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

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

按照手动数字 class化,训练数据 (train.png) 中的所有数字都被手动标记,图像将如下图所示。

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

对于训练,我们按如下方式进行:

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

现在检测:

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

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

代码

下面我 post 我所有的 openCv c++ 代码在一个 class 中,按照我的说明你应该能够实现我的结果。

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