基于模板匹配的车牌识别系统(UI界面+模板匹配C++实现)_车牌字符模板匹配-程序员宅基地

技术标签: # 图像分类  人工智能  

有问题欢迎联系作者WX Qwe1398276934

摘要: 车牌识别系统用于识别车牌,从图像、视频、摄像设备等图像中分析,对小区门口的车辆进行自动化识别检测。在介绍算法原理的同时,给出C++的模板匹配实现代码,QT的UI界面以及模板图像。车牌常用于小区门口、停车场等车流量较为密集的场所,利用传统图像模板匹配算法,自动对图片进行识别、分析。博文提供了完整的C++代码和使用教程,适合入门的朋友参考,完整的代码资源文件请转到文末的下载链接。

visusal studio完整工程项目下载,评论区留言或私信作者

前言

车牌识别技术是一项基于计算机视觉和图像处理的先进技术,它的出现改变了交通管理和安防监控的方式。随着城市交通的快速增长和车辆数量的不断增加,传统手动检查车牌的方法已经无法满足日益增长的需求。而车牌识别技术的出现,为自动化、高效的车辆识别提供了可行的解决方案。通过使用高分辨率摄像头和复杂的图像处理算法,车牌识别技术能够准确地捕捉车辆车牌的信息,并将其转化为数字或文字形式,实现自动化的车辆识别和数据管理。这项技术不仅提高了交通管理和安防监控的效率,还为城市交通的智能化发展奠定了坚实的基础。

这里给出博主设计的软件界面,一贯的简单作风哈哈,功能也可以满足图片初始界面如下图:

选择图片之后,灰度图效果如下图所示

选择图片之后,边缘检测结果图片如下图所示 

获取车牌结果如下图所示: 

识别图片结果,下图所示,这里没有显示中文和特殊符号 

模板匹配的原理基于假设:车牌在图像中的出现形式与模板图像非常相似。通过计算模板与图像区域的相似度,可以找到最佳匹配区域,从而定位到车牌的位置。

需要注意的是,模板匹配方法对光照、尺度变化和视角变化等因素较为敏感。在实际应用中,可能需要使用其他预处理步骤、特征提取算法和机器学习方法来提高车牌识别系统的准确性和鲁棒性。模板匹配通常作为车牌识别系统中的一部分,并结合其他技术共同完成车牌字符的分割和识别任务。

  1. 车牌识别的模板匹配是一种基于相似性度量的图像匹配方法。原理基于以下步骤:

  2. 载入车牌图像和模板图像:首先,需要准备一张包含车牌的图像作为输入图像,以及一个包含车牌字符样式的模板图像。

  3. 转换为灰度图像:为了简化处理过程,将输入图像和模板图像转换为灰度图像。这可以通过使用灰度转换函数(如OpenCV中的cv2.cvtColor()函数)来实现。

  4. 获取模板图像的尺寸:在模板匹配过程中,需要知道模板图像的宽度和高度。这将用于定位匹配区域。

  5. 应用模板匹配算法:使用模板匹配算法(如OpenCV中的cv2.matchTemplate()函数),在灰度图像中搜索与模板最相似的区域。该算法通过在输入图像中滑动模板图像,计算模板与图像区域的相似度。常用的匹配方法包括平方差匹配、相关系数匹配和归一化相关系数匹配等。

  6. 获取最佳匹配结果:根据模板匹配算法的结果,可以通过函数(如OpenCV中的cv2.minMaxLoc()函数)找到匹配结果中的最大值和对应的位置。最大值表示最相似的匹配程度,位置表示匹配区域的左上角坐标。

  7. 绘制匹配结果:使用左上角坐标和模板图像的尺寸,可以在原始图像中绘制车牌区域的矩形框。这样可以直观地显示出车牌的位置。

进行图像预处理操作

#include"source.h"

using namespace std;
Mat Image_Preprocessing(Mat temp)//图像预处理
{
    Mat kernel = getStructuringElement(MORPH_RECT, Size(25, 25), Point(-1, -1));
    Mat open_gray_blur_Image;
    morphologyEx(temp, open_gray_blur_Image, MORPH_OPEN, kernel);
    Mat rst;
    subtract(temp, open_gray_blur_Image, rst, Mat());
    //imshow("rst", rst);
    Mat Canny_Image;
    Canny(rst, Canny_Image, 400, 200, 3);
    return Canny_Image;
}


进行图像形态学处理
 

Mat Morphological_Processing(Mat temp)//形态学处理
{
    //图片膨胀处理
    Mat dilate_image, erode_image;
    //自定义核:进行 x 方向的膨胀腐蚀
    Mat elementX = getStructuringElement(MORPH_RECT, Size(25, 1));
    Mat elementY = getStructuringElement(MORPH_RECT, Size(1, 19));
    Point point(-1, -1);
    dilate(temp, dilate_image, elementX, point, 2);
    erode(dilate_image, erode_image, elementX, point, 4);
    dilate(erode_image, dilate_image, elementX, point, 2);
    //自定义核:进行 Y 方向的膨胀腐蚀
    erode(dilate_image, erode_image, elementY, point, 1);
    dilate(erode_image, dilate_image, elementY, point, 2);
    //平滑处理 中值滤波
    Mat median_Image;
    medianBlur(dilate_image, median_Image, 15);
    medianBlur(median_Image, median_Image, 15);
    //imshow("中值滤波", median_Image);
    return median_Image;
}

对车牌进行定位

Mat Locate_License_Plate(Mat temp, Mat src, Mat gray_src)//车牌定位
{
    vector<vector<Point>> contours;
    findContours(temp, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    //画出轮廓
    drawContours(temp, contours, -1, Scalar(255), 1);
    //轮廓表示为一个矩形
    Mat Roi;
    for (int i = 0; i < contours.size(); i++)
    {
        RotatedRect rect = minAreaRect(Mat(contours[i]));
        Point2f p[4];
        rect.points(p);
        double axisLongTemp = 0.0, axisShortTemp = 0.0;//矩形的长边和短边
        axisLongTemp = sqrt(pow(p[1].x - p[0].x, 2) + pow(p[1].y - p[0].y, 2));  //计算长轴(勾股定理)
        axisShortTemp = sqrt(pow(p[2].x - p[1].x, 2) + pow(p[2].y - p[1].y, 2)); //计算短轴(勾股定理)
        double LengthTemp;     //中间变量
        if (axisShortTemp > axisLongTemp)   //短轴大于长轴,交换数据
        {
            LengthTemp = axisLongTemp;
            axisLongTemp = axisShortTemp;
            axisShortTemp = LengthTemp;
        }
        double rectArea = axisLongTemp * axisShortTemp;//计算矩形面积
        double Area = contourArea(Mat(contours[i]));//轮廓面积
        double rectDegree = Area / rectArea;//计算矩形度
        //这部分的条件判断可视实际情况做调整
        if (axisLongTemp / axisShortTemp >= 2.2 && axisLongTemp / axisShortTemp <= 5.1 && rectDegree > 0.63 && rectDegree < 1.37 && rectArea>2000 && rectArea < 50000)
        {
            for (int i = 0; i < 4; i++)       //划线框出车牌区域
                line(src, p[i], p[((i + 1) % 4) ? (i + 1) : 0], Scalar(0, 0, 255), 2, 8, 0);

            float width_height = (float)rect.size.width / (float)rect.size.height;
            float angle = rect.angle;
            if (width_height < 1)//处理图像中旋转角度大于90度的车牌
                angle = angle + 270;
            Mat rotMat = getRotationMatrix2D(rect.center, angle, 1);//获得矩形的旋转矩阵
            Mat warpImg;
            warpAffine(gray_src, warpImg, rotMat, src.size(), INTER_CUBIC);
            //imshow("仿射变换", warpImg);
            //图像切割
            Size minRectSize = rect.size;
            if (width_height < 1)
                swap(minRectSize.width, minRectSize.height);
            getRectSubPix(warpImg, minRectSize, rect.center, Roi);
        }
    }
    //imshow("test", src);
    //imshow("车牌提取结果", Roi);
    return Roi;
}


进行仿射变换,校正图片

Mat Affine_Transform(Mat temp)
{
    Mat warp_dstImage = Mat::zeros(100, 500, temp.type());
    Point2f srcTri[3];
    Point2f dstTri[3];
    //设置三个点来计算仿射变换
    srcTri[0] = Point2f(0, 0);
    srcTri[1] = Point2f(temp.cols, 0);
    srcTri[2] = Point2f(0, temp.rows);

    dstTri[0] = Point2f(0, 0);
    dstTri[1] = Point2f(500, 0);
    dstTri[2] = Point2f(0, 100);
    //计算仿射变换矩阵
    Mat warp_mat(2, 3, CV_32FC1);
    warp_mat = getAffineTransform(srcTri, dstTri);
    //对加载图形进行仿射变换操作
    warpAffine(temp, warp_dstImage, warp_mat, warp_dstImage.size());
    return warp_dstImage;
}

切割垂直边界

Mat Remove_Vertial_Border(Mat temp)
{
    Mat vline = getStructuringElement(MORPH_RECT, Size(1, temp.rows), Point(-1, -1));
    Mat dst1, temp1;
    erode(temp, temp1, vline);
    dilate(temp1, dst1, vline);
    //namedWindow("提取垂直线", WINDOW_AUTOSIZE);
    //imshow("提取垂直线", dst1);
    subtract(temp, dst1, temp, Mat());
    //imshow("切割车牌垂直边框结果", temp);
    return temp;
}

切割水平边界

Mat Remove_Horizon_Border(Mat temp)
{
    Mat hline = getStructuringElement(MORPH_RECT, Size(60, 1), Point(-1, -1));//矩形形状为:1*src.cols
    Mat dst1, temp1;
    erode(temp, temp1, hline);
    dilate(temp1, dst1, hline);
    //namedWindow("提取水平线", WINDOW_AUTOSIZE);
    //imshow("提取水平线", dst1);
    subtract(temp, dst1, temp, Mat());
    //imshow("切割车牌水平边框结果", temp);
    return temp;
}
Mat Horizon_Cut(Mat temp)
{
    int* counter_y = new int[temp.rows];
    for (int i = 0; i < temp.rows; i++)
        counter_y[i] = 0;
    for (int row = 0; row < temp.rows; row++)
    {
        int count = 0;
        for (int col = 0; col < temp.cols; col++)
        {
            if (temp.at<uchar>(row, col) == 255)
            {
                count++;
            }
        }
        if (count > 50)
        {
            counter_y[row] = 1;
        }
    }
    int count_temp = 0;
    int* record = new int[temp.rows];
    for (int i = 0; i < temp.rows; i++)
        record[i] = 0;
    for (int i = 0; i < temp.rows; i++)
    {
        if (counter_y[i] == 1)
        {
            count_temp++;
            record[i] = count_temp;
        }
        else
            count_temp = 0;
    }
    int max = record[0];
    int index = 0;
    for (int i = 1; i < temp.rows; i++)
    {
        if (max < record[i])
        {
            max = record[i];
            index = i;
        }
    }
    int index_row_begin = index - max + 1;
    int index_row_end = index;
    int height = index_row_end - index_row_begin;
    Mat image_preprocess = Mat::zeros(height, temp.cols, CV_8UC1);
    for (int row = 0; row < image_preprocess.rows; row++)
    {
        for (int col = 0; col < image_preprocess.cols; col++)
        {
            image_preprocess.at<uchar>(row, col) = temp.at<uchar>(row + index_row_begin, col);
        }
    }
    //imshow("image_preprocess", image_preprocess);
    return image_preprocess;
}

定位字符

void Locate_String(int* x_begin, int* x_end, Mat temp)
{
    int* counter_x = new int[temp.cols];//记录每一列的白像素个数
    for (int i = 0; i < temp.cols; i++)
        counter_x[i] = 0;
    for (int col = 0; col < temp.cols; col++)
    {
        int count = 0;
        for (int row = 0; row < temp.rows; row++)
        {
            if (temp.at<uchar>(row, col) == 255)
            {
                count++;
            }
        }
        counter_x[col] = count;
    }
    int index_col = 0;
    int number_width = 0;//记录字符宽度
    for (int i = 0; i < temp.cols - 1; i++)
    {
        if (counter_x[i] >= 4)//当每一列的白像素个数大于某一阈值,字符宽度开始累加
        {
            number_width++;
            if (number_width > 8)//10 当白像素个数大于某一阈值的连续列的个数大于10,开始更新字符位置信息
            {
                x_end[index_col] = i;
                x_begin[index_col] = i - number_width + 1;
                if (counter_x[i + 1] < 4)
                {
                    number_width = 0;
                    index_col++;
                }
            }
        }
        else
        {
            number_width = 0;
        }
        if (index_col >= 8)
            break;
    }
}

绘制结果

void Draw_Result(int* x_begin, int* x_end, Mat temp)
{
    int x, y;
    int width;
    int length;
    Mat Result = temp.clone();
    for (int i = 0; i < 8; i++)
    {
        x = x_begin[i];
        y = 0;
        width = x_end[i] - x_begin[i];
        length = temp.cols;
        Rect rect(x, y, width, length);
        Scalar color(255, 255, 255);
        rectangle(Result, rect, color, 2, LINE_AA);
    }
    //imshow("车牌号码分割结果", Result);
}

识别车牌

string Recognize_Lisence(int* x_begin, int* x_end, Mat temp)
{
    string res = "";
    int cycle_index = 0;
    for (int i = 0; i < 8; i++)
    {
        if (x_end[i] > 0)
            cycle_index++;
    }
    for (int i = 0; i < cycle_index; i++)
    {
        float error[28] = { 0 };
        //    //picture1是二值图像
        Mat picture1 = Mat::zeros(temp.rows, x_end[i] - x_begin[i], temp.type());
        for (int row = 0; row < picture1.rows; row++)
        {
            for (int col = 0; col < picture1.cols; col++)
            {
                picture1.at<uchar>(row, col) = temp.at<uchar>(row, col + x_begin[i]);
            }
        }
        Mat NUM[28];//字符匹配模板
        for (int i = 0; i < 28; i++)
        {
            stringstream stream;
            stream << "pictures/num_";
            stream << i;
            stream << ".bmp";
            String name = stream.str();
            NUM[i] = imread(name);
            if (NUM[i].empty())
            {
                cout << "未能读取" << name << endl;
            }
            cvtColor(NUM[i], NUM[i], COLOR_BGR2GRAY);
            threshold(NUM[i], NUM[i], 0, 255, THRESH_BINARY);

            Point2f srcTri[3];
            Point2f dstTri[3];
            Mat warp_mat(2, 3, CV_32FC1);
            //创建仿射变换目标图像与原图像尺寸类型相同
            Mat result = Mat::zeros(picture1.rows, picture1.cols, picture1.type());
            //设置三个点来计算仿射变换
            srcTri[0] = Point2f(0, 0);
            srcTri[1] = Point2f(NUM[i].cols, 0);
            srcTri[2] = Point2f(0, NUM[i].rows);
            dstTri[0] = Point2f(0, 0);
            dstTri[1] = Point2f(picture1.cols, 0);
            dstTri[2] = Point2f(0, picture1.rows);
            //计算仿射变换矩阵
            warp_mat = getAffineTransform(srcTri, dstTri);
            //对加载图形进行仿射变换操作
            warpAffine(NUM[i], result, warp_mat, picture1.size());
            threshold(result, result, 0, 255, THRESH_BINARY_INV);
            float error_sum = 0;
            float error_temp = 0;
            for (int row = 0; row < result.rows; row++)
            {
                for (int col = 0; col < result.cols; col++)
                {
                    error_temp = picture1.at<uchar>(row, col) - result.at<uchar>(row, col);
                    error_sum = error_sum + pow(error_temp, 2);
                }
            }
            error[i] = error_sum / (picture1.rows * picture1.cols * 255);
        }
        float min_error = error[0];
        int Index = 0;
        for (int i = 1; i < 28; i++)
        {
            if (min_error > error[i])
            {
                min_error = error[i];
                Index = i;
            }
        }
        if (Index == 10)
        {
            res += "E";
            cout << "E" << '\t';
        }
        else if (Index == 11) {
            res += "V";
            cout << "V" << '\t';
        }
        else if (Index == 12) {
            res += "苏";
            cout << "苏" << '\t';
        }
        else if (Index == 13)
        {
            res += "沪";
            cout << "沪" << '\t';
        }
        else if (Index == 14)
        {
            res += "B";
            cout << "B" << '\t';
        }
        else if (Index == 15)
        {
            res += "S";
            cout << "S" << '\t';
        }
        else if (Index == 16)
        {
            res += "京";
            cout << "京" << '\t';
        }
        else if (Index == 17)
        {
            res += "N";
            cout << "N" << '\t';
        }
        else if (Index == 18)
        {
            res += "J";
            cout << "J" << '\t';
        }
        else if (Index == 19)
        {
            res += "P";
            cout << "P" << '\t';
        }
        else if (Index == 20)
        {    
            res += "A";
            cout << "A" << '\t';
        }
        else if (Index == 21)
        {
            res += "浙";
            cout << "浙" << '\t';
        }
        else if (Index == 22)
        {
            res += "G";
            cout << "G" << '\t';
        }
        else if (Index == 23)
        {
            res += "U";
            cout << "U" << '\t';
        }
        else if (Index == 24)

        {
            res += "豫";
            cout << "豫" << '\t';
        }
        else if (Index == 25)
        {
            res += "K";
            cout << "K" << '\t';
        }
        else if (Index == 26)
        {
            res += "陕";
            cout << "陕" << '\t';
        }
        else if (Index == 27)
        {
            res += "·";
            cout << "·" << '\t';
        }
        else if (Index >= 0 && Index <= 9)
        {
            res += to_string(Index);
            cout << Index << '\t';
        }
    }
    cout << endl;
    return res;
}

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_46644680/article/details/130099890

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签