用opencv识别路标(不准转弯)

  1. 目标
    在自然场景下识别下面这个路标
    No uturn
    准备一些场景测试图片,一行会用。用opencv识别路标(不准转弯)_第1张图片

用opencv识别路标(不准转弯)_第2张图片
2. 思路
路标外围的红色还是挺有区分度的,所以可以利用hsv先将场景中的红色区域提取出来,然后通过中值滤波和形态学处理去除噪音,接着就可以把每个红色区域的轮廓提取出来了,由于路标是椭圆形(视角原因,本来是圆形),所以利用每个轮廓去拟合一个椭圆并计算面积,同样的,轮廓也可以计算面积。如果当前的轮廓是路标的话,这两个面积比应该接近于1.具体的细节请看代码和注释。
3. 代码

#include
#include
#define PI 3.1415926

using namespace std;
using namespace cv;
//将RGB色域转换为HSV色域,可以直接网上搜公式
void RGB2HSV(double red, double green, double blue, double& hue, double& saturation, double& intensity)
{
    double r, g, b;
    double h, s, i;

    double sum;
    double minRGB, maxRGB;
    double theta;

    r = red / 255.0;
    g = green / 255.0;
    b = blue / 255.0;

    minRGB = ((r < g) ? (r) : (g));
    minRGB = (minRGB < b) ? (minRGB) : (b);

    maxRGB = ((r > g) ? (r) : (g));
    maxRGB = (maxRGB > b) ? (maxRGB) : (b);

    sum = r + g + b;
    i = sum / 3.0;

    if (i < 0.001 || maxRGB - minRGB < 0.001)
    {

        h = 0.0;
        s = 0.0;
    }
    else
    {
        s = 1.0 - 3.0*minRGB / sum;
        theta = sqrt((r - g)*(r - g) + (r - b)*(g - b));
        theta = acos((r - g + r - b)*0.5 / theta);
        if (b <= g)
            h = theta;
        else
            h = 2 * PI - theta;
        if (s <= 0.01)
            h = 0;
    }

    hue = (int)(h * 180 / PI);
    saturation = (int)(s * 100);
    intensity = (int)(i * 100);
}
//由于路标里面有空洞,先使用floodFill算法将红色区域的外围覆盖成255,然后通过最后一行代码取出红色区域,此时路标里的空洞以被填充。
//这里的srcBw是一通道的mask,原图像中红色的区域为255,其他为0,dstBw 为填充后的mask
void fillHole(const Mat &srcBw, Mat &dstBw)
{
    Size m_Size = srcBw.size();
    Mat Temp = Mat::zeros(m_Size.height + 2, m_Size.width + 2, srcBw.type());
    srcBw.copyTo(Temp(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1)));

    cv::floodFill(Temp, Point(0, 0), Scalar(255));

    Mat cutImg;
    Temp(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1)).copyTo(cutImg);

    dstBw = srcBw | (~cutImg);
}
/*
 *image: 原图像,三通道
 *contours: 从原图像提取出的轮廓
*/
void getContours(const Mat & image,vector<vector<Point>> &contours)
{
    int width = image.cols;//图像宽度
    int height = image.rows;//图像高度
    double B = 0.0, G = 0.0, R = 0.0, H = 0.0, S = 0.0, V = 0.0;
    Mat matRgb = Mat::zeros(image.size(), CV_8UC1);
    int x, y; //循环,根据hsv值挑选出红色的区域,红色区域会在matRgb中相应位置取值255
    for (y = 0; y < height; y++)
    {
        for (x = 0; x < width; x++)
        {
            // 获取BGR值
            B = image.at<Vec3b>(y, x)[0];
            G = image.at<Vec3b>(y, x)[1];
            R = image.at<Vec3b>(y, x)[2];
            RGB2HSV(R, G, B, H, S, V);
            //红色范围
            if ((H >= 330 && H <= 360 || H >= 0 && H <= 10) && S >= 11 && S <= 100 && V > 10 && V < 99) //H不能低于10,H不能大于344,S不能高于21,V不能变
            {
                matRgb.at<uchar>(y, x) = 255;
            }
        }
    }
    medianBlur(matRgb, matRgb, 3); // 中值滤波,去除噪声,下面的形态学处理也是

    Mat element = getStructuringElement(MORPH_ELLIPSE, Size(5, 5), Point(0, 0));
    morphologyEx(matRgb,matRgb,MORPH_CLOSE,element,Point(-1,-1),1);
    fillHole(matRgb,matRgb);// 填充路标的空洞,看fillHole的函数注释
    vector<Vec4i> hierarchy;
    findContours(matRgb, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0)); // opencv 函数,计算轮廓
}
// 根据一个轮廓,把这个轮廓里的图像(in)扣出来
void Segment(const Mat &in,const vector<Point> &contour,Mat &out,Rect &box)
{
    Mat temp(in.size(),in.type());
    Mat img;
    in.copyTo(img);
    temp.setTo(Scalar(0,0,0));
    RotatedRect ell = fitEllipse(contour); //轮廓拟合椭圆
    box = boundingRect(contour);  //轮廓的最小矩形
    cout<<box<<endl;
    ellipse(temp,ell,Scalar(255,255,255),-1); //将椭圆填充到temp,成为一个mask
    bitwise_and(in,temp,img); //根据椭圆切割图像
    img(box).copyTo(out);
}
// 简单计算两个图像的相似度,直接求差值,一个为路标template,另一个为自然场景中扣出来的图片,这里是很简单的计算方法,完全可以改进
// 函数返回值越小2,说明两张图片越相似

double score(const Mat &queryImg,const Mat &tempImg)
{
    Mat tmp1,tmp2;
    queryImg.copyTo(tmp1);
    tempImg.copyTo(tmp2);
    Scalar mean1 = cv::mean(tmp1);
    Scalar mean2 = cv::mean(tmp2);
    tmp1 -= mean1;
    tmp2 -= mean2;
    return cv::norm(tmp1,tmp2);
}
int main(int argc,char *argv[])
{
    /*
     * idlefish-msg-1640693329982.jpg
     * idlefish-msg-1640693332987.jpg
     * idlefish-msg-1640693317275.jpg
     * idlefish-msg-1640693319435.jpg
     * */
    if(argc!=3)
    {
        cerr << "Usage: "<<argv[0] <<" image_path template_image_path"<<endl;
    }
    Mat srcImg = imread(argv[1]);
    Mat template_Img = imread(argv[2]);
    Mat srcImgCopy;
    srcImg.copyTo(srcImgCopy);
    vector<vector<Point>> srcImg_contours,templateImg_contours;
    getContours(srcImg,srcImg_contours); //先计算template和image的轮廓
    getContours(template_Img,templateImg_contours);
    Rect template_box;
    Segment(template_Img,templateImg_contours[0],template_Img,template_box);// 对template的图片也切割一下,把不是红色轮廓里面的背景置0
    double best_score = 0;
    Rect best_rect;
    for(auto & contour : srcImg_contours)
    {
        double ConArea = contourArea(contour);// 场景image中的某个轮廓
        if(ConArea < 500) // 这个轮廓面积太小,就跳过
            continue;
        RotatedRect ell = fitEllipse(contour); //这个轮廓的拟合椭圆
        double EllArea = ell.size.area() * PI/4; // 拟合椭圆的面积
        if(ConArea/EllArea > 1.1 || ConArea/EllArea < 0.9) // 如果这个两个面积比相差太大,则跳过
            continue;
        Mat queryImg;
        Rect queryBox;
        Segment(srcImgCopy,contour,queryImg,queryBox); // 将这个轮廓对应的图片扣出来与template做对比,首先要resize到和template 一样的大小才行。
        resize(queryImg,queryImg,template_Img.size());
        Mat result;
        double s = score(queryImg,template_Img); //计算相似性
        if(best_score == 0) { // 如果是第一个轮廓,就把best_score赋值
            best_score = s;
            best_rect = queryBox;
        }
        if(s<best_score) // 如果目前的score比以前的best_score小,说明当前扣出来的图片与template的相似度高一点,则替换以前的值
        {
            best_score = s;
            best_rect = queryBox;
        }
        std::cout<<"ConArea: "<<ConArea<<" EllArea "<<EllArea<<endl;
    }
    cv::rectangle(srcImgCopy,best_rect,Scalar(255,0,0),2);
    imshow("srcImgCopy",srcImgCopy);
    imwrite("result.png",srcImgCopy);
    waitKey(0);
    return 0;
}
  1. 结果

    用opencv识别路标(不准转弯)_第3张图片

你可能感兴趣的:(opencv,算法,图像处理,opencv,计算机视觉)