C++视觉开发 七.模板匹配

模板匹配是一种基于图像处理的技术,用于在目标图像中寻找与给定模板图像最相似的部分。通过设定的模板,将目标图像与模板图像比较,计算其相似度,实现对目标图像的判断。

目录

一.手写数字识别

重要函数:

1.cv::glob

2. cv::matchTemplate

 实现流程:

总结:

二.车牌识别

1.提取车牌

1.Sobel算子

cv::Sobel:

cv::convertScaleAbs

2.两次滤波方法的选择

3.开运算和闭运算核的大小选择

4.cv:: boundingRect

5.实现代码

2.分割车牌

1.重点步骤 

2.实现代码

3.车牌识别


一.手写数字识别

模板匹配实现数字识别流程图

重要函数:

1.cv::glob

功能:根据指定的模式匹配获取文件路径列表。

函数语法:

void cv::glob(const String &pattern, std::vector<String> &result, bool recursive);
参数含义
pattern

匹配文件的模式字符串。

例如 "image/*.jpg" 表示匹配所有 .jpg 文件。

"/*.*" 表示该路径下的所有文件。

result存储匹配结果的字符串向量。
recursive是否递归搜索子目录,默认值为 false

使用示例:

    // 准备模板图像
    vector<String> images;
    for (int i = 0; i < 10; i++) {
        vector<String> temp;
        glob("image/" + to_string(i) + "/*.*", temp, false); //"/*.*"表示该路径下的所有文件。
        images.insert(images.end(), temp.begin(), temp.end());
    }

2. cv::matchTemplate

功能:用于在一幅图像中搜索和匹配另一个图像(模板)。该函数通过滑动模板图像,并在每个位置计算匹配值,最终找出最佳匹配位置。

函数语法:

void cv::matchTemplate(
    InputArray image, 
    InputArray templ, 
    OutputArray result, 
    int method);
参数含义
image输入的源图像
templ用于匹配的模板图像
result输出的结果图像,其每个位置包含对应位置的匹配度。
method

匹配方法,可以是以下之一:

CV_TM_SQDIFF:平方差匹配法,结果越小表示越匹配。

CV_TM_SQDIFF_NORMED:归一化平方差匹配法,结果越小表示越匹配。

CV_TM_CCORR:相关匹配法,结果越大表示越匹配。

CV_TM_CCORR_NORMED:归一化相关匹配法,结果越大表示越匹配。

CV_TM_CCOEFF:相关系数匹配法,结果越大表示越匹配。

CV_TM_CCOEFF_NORMED:归一化相关系数匹配法,结果越大表示越匹配。

使用示例:

double getMatchValue(const string& templatePath, const Mat& image) {
    // 读取模板图像
    Mat templateImage = imread(templatePath);
    // 模板图像色彩空间转换,BGR-->灰度
    cvtColor(templateImage, templateImage, COLOR_BGR2GRAY);
    // 模板图像阈值处理,灰度-->二值
    threshold(templateImage, templateImage, 0, 255, THRESH_OTSU);
    // 获取待识别图像的尺寸
    int height = image.rows;
    int width = image.cols;
    // 将模板图像调整为与待识别图像尺寸一致
    resize(templateImage, templateImage, Size(width, height));
    // 计算模板图像、待识别图像的模板匹配值
    Mat result;
    matchTemplate(image, templateImage, result, TM_CCOEFF);
    // 返回计算结果
    return result.at<float>(0, 0);
}

 实现流程:

1.数据准备

2.计算匹配值

3.获取最佳匹配值对应模板

4.将最佳匹配模板对应的数字作为识别结果

实现代码:

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <string>

using namespace cv;
using namespace std;

// 准备数据
//Mat o = imread("image/test2/6.bmp", IMREAD_GRAYSCALE);

// 函数:获取匹配值
double getMatchValue(const string& templatePath, const Mat& image) {
    // 读取模板图像
    Mat templateImage = imread(templatePath);
    // 模板图像色彩空间转换,BGR-->灰度
    cvtColor(templateImage, templateImage, COLOR_BGR2GRAY);
    // 模板图像阈值处理,灰度-->二值
    threshold(templateImage, templateImage, 0, 255, THRESH_OTSU);
    // 获取待识别图像的尺寸
    int height = image.rows;
    int width = image.cols;
    // 将模板图像调整为与待识别图像尺寸一致
    resize(templateImage, templateImage, Size(width, height));
    // 计算模板图像、待识别图像的模板匹配值
    Mat result;
    matchTemplate(image, templateImage, result, TM_CCOEFF);
    // 返回计算结果
    return result.at<float>(0, 0);
}

int main() {
    // 准备数据
    Mat o = imread("image/test2/6.bmp", IMREAD_GRAYSCALE);
    // 准备模板图像
    vector<String> images;
    for (int i = 0; i < 10; i++) {
        vector<String> temp;
        glob("image/" + to_string(i) + "/*.*", temp, false); //"/*.*"表示该路径下的所有文件。
        images.insert(images.end(), temp.begin(), temp.end());
    }

    // 计算最佳匹配值及模板序号
    vector<double> matchValue;
    for (const auto& xi : images) {
        double d = getMatchValue(xi, o);
        matchValue.push_back(d);
    }

    // 获取最佳匹配值
    double bestValue = *max_element(matchValue.begin(), matchValue.end());
    // 获取最佳匹配值对应模板编号
    int i = distance(matchValue.begin(), find(matchValue.begin(), matchValue.end(), bestValue));

    // 计算识别结果
    int number = i / 10;

    // 显示识别结果
    cout << "识别结果: 数字 " << number << endl;

    return 0;
}

总结:

这是传统图像处理方法进行的手写数字识别。实践中,为了更加高效,我们通常可以采取以下的改进方法:

1.基于机器学习(KNN)的K邻近算法。

2.基于个性化特征的手写识别。实践中可以先分别提取每个数字的个性化特征,然后将数字依次与各个数字的个性化特征进行比对。符合哪个特征,就将其识别为哪个特征对应的数字。例如选用方向梯度直方图(Histogram of 0riented Gradient,H0G)对图像进行量化作为SVM分类的数据指标。

3.基于深度学习可以更高效地实现手写数字识别。例如,通过调用TensorFlow可以非常方便地实现高效的手写数字识别的方法。

二.车牌识别

使用模板匹配的方法实现车牌识别。在采用模板匹配的方法识别时,车牌识别与手写数字识别的基本原理是一致的。但是在车牌识别中要解决的问题更多。本章的待识别的手写数字是单独的一个数字,每个待识别数字的模板数量都是固定的,这个前提条件让识别变得很容易。而在车牌识别中,首先要解决的是车牌的定位,然后要将车牌分割为一个一个待识别字符。如果每个字符的模板数量不一致,那么在识别时就不能通过简单的对应关系实现模板和对应字符的匹配,需要考虑新的匹配方式。可以理解为对手写数字识别的改进或优化。

车牌识别流程图 

车牌识别流程:

(1)提取车牌:将车牌从复杂的背景中提取出来。

(2)拆分字符:将车牌拆分成一个个独立的字符。

(3)识别字符:识别车牌上提取的字符。

1.提取车牌

提取车牌流程图 

重要问题和函数:

1.Sobel算子

用于边缘检测,重点提取车牌及其中字符的边缘。计算图像在X方向上的梯度能够突出垂直方向上的边缘。这对于检测图像中的物体边界、线条和其他显著的特征非常有用。

cv::Sobel:

功能:使用Sobel算子计算图像的梯度。

函数语法:

void Sobel(
    InputArray src, 
    OutputArray dst, 
    int ddepth, 
    int dx, 
    int dy, 
    int ksize = 3, 
    double scale = 1, 
    double delta = 0, 
    int borderType = BORDER_DEFAULT);
参数含义
src输入图像。
dst输出图像(梯度)。
ddepth输出图像的深度(例如,CV_16S(16位有符号整数))。
dxX方向上的差分阶数(例如,1)。
dyY方向上的差分阶数(例如,0)。
ksizeSobel算子的核大小(默认3)。
scale可选的缩放系数(默认值为1)。
delta可选的偏移量(默认值为0)。
borderType边界类型(默认值为BORDER_DEFAULT)。

使用示例:

    cv::Mat SobelX;
    cv::Sobel(image, SobelX, CV_16S, 1, 0);
cv::convertScaleAbs

功能:将输入图像按比例缩放,并将其转换为8位无符号图像(即将像素值映射到[0, 255])。

函数语法:

void convertScaleAbs(InputArray src, OutputArray dst, double alpha = 1, double beta = 0);
参数含义
src输入图像(梯度图像)
dst输出图像(缩放后的图像)。
alpha缩放系数(默认值为1)。
beta可选的偏移量(默认值为0)。

使用示例:

    Mat absX;
    convertScaleAbs(SobelX, absX);  // 映射到[0, 255]内

2.两次滤波方法的选择

(1)高斯滤波

目的:高斯滤波是一种线性平滑滤波器,主要用于去除高频噪声,同时保持图像的大致结构。它通过高斯核对图像进行卷积操作,平滑图像,减少噪声。

特点:高斯滤波对每个像素进行加权平均,权重根据高斯分布确定。它对高斯噪声(例如,图像传感器噪声)特别有效。

原因:在边缘检测之前应用高斯滤波,可以使图像变得更平滑,减少边缘检测中的噪声干扰,提高边缘检测的准确性。

(2)中值滤波

目的:中值滤波是一种非线性滤波器,主要用于去除椒盐噪声(salt-and-pepper noise)。它通过将像素邻域内的所有像素值排序,并用中值替换中心像素值来达到去噪效果。

特点:中值滤波对椒盐噪声特别有效,因为它能够保留边缘细节,而不像线性滤波器那样模糊边缘。

原因:在形态学处理(开闭运算)之后,应用中值滤波可以进一步去除图像中可能残留的噪声,特别是形态学处理未能完全去除的椒盐噪声

高斯滤波前置:在边缘检测前使用高斯滤波,可以减少高频噪声对边缘检测的干扰,使边缘检测结果更准确。

中值滤波后置:在形态学处理后使用中值滤波,可以去除形态学操作可能引入的或未能去除的噪声,尤其是椒盐噪声,同时保持图像的边缘细节。

这种滤波顺序的选择是为了在每个处理阶段有效去除不同类型的噪声,提高图像处理效果,从而更好地实现车牌的定位和提取。

3.开运算和闭运算核的大小选择

(1)闭运算

目的:闭运算是先膨胀后腐蚀,主要用于填补前景物体中的小洞,连接近邻的前景物体。

核的选择:选择(17, 5)这样的核尺寸,意味着在水平方向(17个像素)上进行更多的扩展和收缩,而在垂直方向(5个像素)上进行较少的扩展和收缩。这适用于将车牌字符连接成一个整体,因为车牌字符通常是水平排列的。

(2)开运算

目的:开运算是先腐蚀后膨胀,主要用于去除图像中的小噪声点。

核的选择:选择(1, 19)这样的核尺寸,意味着在垂直方向(19个像素)上进行更多的收缩和扩展,而在水平方向(1个像素)上进行较少的收缩和扩展。这有助于去除与车牌字符排列无关的垂直噪声,因为车牌字符在垂直方向上通常是独立的。

核大小的选择原因:

闭运算核(17, 5):用宽的水平核来连接水平分布的车牌字符

开运算核(1, 19):用高的垂直核来消除垂直方向上的噪声而不影响水平的车牌字符

选择这些核大小是基于车牌字符的典型排列方式(水平分布)以及背景噪声的形状特征。根据实际情况和图像特点,这些值可能需要进行调整以获得更好的效果。

4.cv:: boundingRect

功能:用于计算能够完全包含指定点集或轮廓的最小矩形边框。这个函数有两个常用的重载版本,一个用于处理点集(std::vector<cv::Point>),另一个用于处理轮廓(std::vector<std::vector<cv::Point>>)。这里用到的是处理轮廓。

函数语法:

cv::Rect cv::boundingRect(const std::vector<std::vector<cv::Point>>& contours)

 返回值:

x:矩形左上角的 x 坐标。

y:矩形左上角的 y 坐标。

w:矩形的宽度(沿 x 轴的长度)。

h:矩形的高度(沿 y 轴的长度)

使用示例:

        cv::Rect rect = boundingRect(contours[i]);
        int x = rect.x;
        int y = rect.y;
        int weight = rect.width;
        int height = rect.height;

5.实现代码

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main() {
    // ====================读取原始图像======================
    Mat image = imread("gua.jpg");  // 读取原始图像
    if (image.empty()) {
        cout << "Could not open or find the image!" << endl;
        return -1;
    }
    Mat rawImage = image.clone();  // 复制原始图像
    imshow("original", image);  // 测试语句,观察原始图像

    // ===========滤波处理O1(去噪)=====================
    GaussianBlur(image, image, Size(3, 3), 0);
    imshow("GaussianBlur", image);  // 测试语句,查看滤波结果(去噪)

    // ==========灰度变换O2(色彩空间转换BGR-->GRAY)===========
    cvtColor(image, image, COLOR_BGR2GRAY);
    imshow("gray", image);  // 测试语句,查看灰度图像

    // ==============边缘检测O3(Sobel算子、X方向边缘梯度)===============
    Mat SobelX;
    Sobel(image, SobelX, CV_16S, 1, 0);
    Mat absX;
    convertScaleAbs(SobelX, absX);  // 映射到[0, 255]内
    image = absX;
    imshow("soblex", image);  // 测试语句,图像边缘

    // ===============二值化O4(阈值处理)==========================
    threshold(image, image, 0, 255, THRESH_OTSU);
    imshow("imageThreshold", image);  // 测试语句,查看处理结果

    // ===========闭运算O5:先膨胀后腐蚀,车牌各个字符是分散的,让车牌构成一体=======
    Mat kernelX = getStructuringElement(MORPH_RECT, Size(17, 5));
    morphologyEx(image, image, MORPH_CLOSE, kernelX);
    imshow("imageCLOSE", image);  // 测试语句,查看处理结果

    // =============开运算O6:先腐蚀后膨胀,去除噪声==============
    Mat kernelY = getStructuringElement(MORPH_RECT, Size(1, 19));
    morphologyEx(image, image, MORPH_OPEN, kernelY);
    imshow("imageOPEN", image);

    // ================滤波O7:中值滤波,去除噪声=======================
    medianBlur(image, image, 15);
    imshow("imagemedianBlur", image);  // 测试语句,查看处理结果

    // =================查找轮廓O8==================
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(image, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);

    // 测试语句,查看轮廓
    Mat contourImage = rawImage.clone();
    drawContours(contourImage, contours, -1, Scalar(0, 0, 255), 3);
    imshow("imagecc", contourImage);

    // ============定位车牌O9:逐个遍历轮廓,将宽度>3倍高度的轮廓确定为车牌============
    Mat plate;
    for (size_t i = 0; i < contours.size(); i++) {
        Rect rect = boundingRect(contours[i]);
        int x = rect.x;
        int y = rect.y;
        int weight = rect.width;
        int height = rect.height;
        if (weight > (height * 3)) {
            plate = rawImage(Rect(x, y, weight, height)).clone();
        }
    }

    // ================显示提取车牌============================        
    if (!plate.empty()) {
        imshow("plate", plate);  // 测试语句:查看提取车牌
    }
    else {
        cout << "No plate detected!" << endl;
    }

    waitKey(0);
    destroyAllWindows();

    return 0;
}

里面注释了每一步操作,在后续完整实现中需要将其封装成函数。 

2.分割车牌

分割车牌是指将车牌中的各字符提取出来,以便进行后续识别。通常情况下,需要先对图像进行预处理(主要是进行去噪、二值化、膨胀等操作)以便提取每个字符的轮廓。接下来,寻找车牌内的所有轮廓,将其中高宽比符合字符特征的轮廓判定为字符。

车牌分割流程图

1.重点步骤 

膨胀F4: 通常情况下,字符的各个笔画之间是分离的,通过膨胀操作可以让各字符形成一个整体。
轮廓F5: 该操作用来查找图像内的所有轮廓,可以使用函数findcontours完成。此时找到的轮廓非常多,既包含每个字符的轮廓,又包含噪声的轮廓。下一步工作是将字符的轮廓筛选出来。
包围框F6: 该操作让每个轮廓都被包围框包围,可以通过函数boundingRect完成。使用包围框替代轮廓的目的是,通过包围框的高宽比及宽度值,可以很方便地判定一个包围框包含的是噪声还是字符。
分割F7: 逐个遍历包围框,将其中宽高比在指定范围内、宽度大于特定值的包围框判定为字符。该操作可通过循环语句内置判断条件实现。

2.实现代码

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <algorithm>

using namespace cv;
using namespace std;

int main() {
    // 读取车牌图像
    Mat image = imread("gg.bmp");
    if (image.empty()) {
        cout << "Could not open or find the image!" << endl;
        return -1;
    }
    Mat o = image.clone();  // 复制原始图像,用于绘制轮廓用
    imshow("original", image);

    // 图像预处理
    // 图像去噪灰度处理F1
    GaussianBlur(image, image, Size(3, 3), 0);
    imshow("GaussianBlur", image);

    // 色彩空间转换F2
    Mat grayImage;
    cvtColor(image, grayImage, COLOR_BGR2GRAY);
    imshow("gray", grayImage);

    // 阈值处理(二值化)F3
    Mat binaryImage;
    threshold(grayImage, binaryImage, 0, 255, THRESH_OTSU);
    imshow("threshold", binaryImage);

    // 膨胀处理F4,让一个字构成一个整体
    Mat dilatedImage;
    Mat kernel = getStructuringElement(MORPH_RECT, Size(2, 2));
    dilate(binaryImage, dilatedImage, kernel);
    imshow("dilate", dilatedImage);

    // 查找轮廓F5,各个字符的轮廓及噪声点轮廓
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(dilatedImage, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    Mat contourImage = o.clone();
    drawContours(contourImage, contours, -1, Scalar(0, 0, 255), 1);
    imshow("contours", contourImage);
    cout << "共找到轮廓个数:" << contours.size() << endl;  // 测试语句:看看找到多少个轮廓

    // 遍历所有轮廓, 寻找最小包围框F6
    vector<Rect> chars;
    for (size_t i = 0; i < contours.size(); i++) {
        Rect rect = boundingRect(contours[i]);
        chars.push_back(rect);
        //绘制矩形框
        rectangle(o, rect, Scalar(0, 0, 255), 1);
    }
    imshow("contours2", o);

    // 将包围框按照x轴坐标值排序(自左向右排序)
    sort(chars.begin(), chars.end(), [](const Rect& a, const Rect& b) { return a.x < b.x; });

    // 将字符的轮廓筛选出来F7
    vector<Mat> plateChars;
    for (const Rect& word : chars) {
        if ((word.height > (word.width * 1.5)) && (word.height < (word.width * 8)) && (word.width > 3)) {
            Mat plateChar = binaryImage(word);
            plateChars.push_back(plateChar);
        }
    }

    // 测试语句:查看各个字符
    for (size_t i = 0; i < plateChars.size(); i++) {
        string windowName = "char" + to_string(i);
        imshow(windowName, plateChars[i]);
    }

    waitKey(0);
    destroyAllWindows();

    return 0;
}

后续需要同提取车牌一样封装成函数。

3.车牌识别

由于每个字符的模板数量未必是一致的,即有的字符有较多的模板,有的字符有较少的模板,不同的模板数量为计算带来了不便,因此采用分层的方式实现模板匹配。先针对模板内的每个字符计算出一个与待识别字符最匹配的模板;然后在逐字符匹配结果中找出最佳匹配模板,从而确定最终识别结果。

具体来说,需要使用3层循环关系
最外层循环:逐个遍历提取的各个字符。
中间层循环:遍历所有特征字符(字符集中的每个字符)
最内层循环:遍历每一个特征字符的所有模板。

完成程序:

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <map>

using namespace cv;
using namespace std;

// ==========================提取车牌函数==============================
Mat getPlate(Mat image) {
    Mat rawImage = image.clone();
    // 去噪处理
    GaussianBlur(image, image, Size(3, 3), 0);
    // 色彩空间转换(RGB-->GRAY)
    cvtColor(image, image, COLOR_BGR2GRAY);
    // Sobel算子(X方向边缘梯度)
    Mat Sobel_x;
    Sobel(image, Sobel_x, CV_16S, 1, 0);
    Mat absX;
    convertScaleAbs(Sobel_x, absX);
    image = absX;
    // 阈值处理
    threshold(image, image, 0, 255, THRESH_OTSU);
    // 闭运算:先膨胀后腐蚀,车牌各个字符是分散的,让车牌构成一体
    Mat kernelX = getStructuringElement(MORPH_RECT, Size(17, 5));
    morphologyEx(image, image, MORPH_CLOSE, kernelX);
    // 开运算:先腐蚀后膨胀,去除噪声
    Mat kernelY = getStructuringElement(MORPH_RECT, Size(1, 19));
    morphologyEx(image, image, MORPH_OPEN, kernelY);
    // 中值滤波:去除噪声
    medianBlur(image, image, 15);
    // 查找轮廓
    vector<vector<Point>> contours;
    findContours(image, contours, RETR_TREE, CHAIN_APPROX_SIMPLE);
    // 测试语句,查看处理结果
    // drawContours(rawImage.clone(), contours, -1, Scalar(0, 0, 255), 3);
    // 遍历轮廓,将宽度 > 3 倍高度的轮廓确定为车牌
    Mat plate;
    for (const auto& item : contours) {
        Rect rect = boundingRect(item);
        if (rect.width > (rect.height * 3)) {
            plate = rawImage(Rect(rect.x, rect.y, rect.width, rect.height)).clone();
        }
    }
    return plate;
}

// ==================预处理函数,图像去噪等处理=================
Mat preprocessor(Mat image) {
    // 图像去噪灰度处理
    GaussianBlur(image, image, Size(3, 3), 0);
    // 色彩空间转换
    Mat grayImage;
    cvtColor(image, grayImage, COLOR_BGR2GRAY);
    // 阈值处理(二值化)
    threshold(grayImage, image, 0, 255, THRESH_OTSU);
    // 膨胀处理,让一个字构成一个整体(大多数字不是一体的,是分散的)
    Mat kernel = getStructuringElement(MORPH_RECT, Size(2, 2));
    dilate(image, image, kernel);
    return image;
}

// ===========拆分车牌函数,将车牌内各个字符分离==================
vector<Mat> splitPlate(Mat image) {
    // 查找轮廓,各个字符的轮廓
    vector<vector<Point>> contours;
    findContours(image, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    vector<Rect> words;
    // 遍历所有轮廓
    for (const auto& item : contours) {
        words.push_back(boundingRect(item));
    }
    // 按照x轴坐标值排序(自左向右排序)
    sort(words.begin(), words.end(), [](const Rect& a, const Rect& b) { return a.x < b.x; });
    // 筛选字符的轮廓(高宽比在1.5-8之间,宽度大于3)
    vector<Mat> plateChars;
    for (const auto& word : words) {
        if ((word.height > (word.width * 1.5)) && (word.height < (word.width * 8)) && (word.width > 3)) {
            plateChars.push_back(image(Rect(word.x, word.y, word.width, word.height)).clone());
        }
    }
    return plateChars;
}

// ==================模板,部分省份,使用字典表示==============================
map<int, string> templateDict = {
    {0, "0"}, {1, "1"}, {2, "2"}, {3, "3"}, {4, "4"}, {5, "5"},
    {6, "6"}, {7, "7"}, {8, "8"}, {9, "9"}, {10, "A"}, {11, "B"},
    {12, "C"}, {13, "D"}, {14, "E"}, {15, "F"}, {16, "G"}, {17, "H"},
    {18, "J"}, {19, "K"}, {20, "L"}, {21, "M"}, {22, "N"}, {23, "P"},
    {24, "Q"}, {25, "R"}, {26, "S"}, {27, "T"}, {28, "U"}, {29, "V"},
    {30, "W"}, {31, "X"}, {32, "Y"}, {33, "Z"}, {34, "京"}, {35, "津"},
    {36, "冀"}, {37, "晋"}, {38, "蒙"}, {39, "辽"}, {40, "吉"}, {41, "黑"},
    {42, "沪"}, {43, "苏"}, {44, "浙"}, {45, "皖"}, {46, "闽"}, {47, "赣"},
    {48, "鲁"}, {49, "豫"}, {50, "鄂"}, {51, "湘"}, {52, "粤"}, {53, "桂"},
    {54, "琼"}, {55, "渝"}, {56, "川"}, {57, "贵"}, {58, "云"}, {59, "藏"},
    {60, "陕"}, {61, "甘"}, {62, "青"}, {63, "宁"}, {64, "新"}, {65, "港"},
    {66, "澳"}, {67, "台"}
};

// ==================获取所有字符的路径信息===================
vector<vector<string>> getCharacters() {
    vector<vector<string>> c;
    for (int i = 0; i <= 67; i++) {
        vector<string> words;
        string pattern = "template/" + templateDict[i] + "/*.*";
        vector<String> filenames;
        glob(pattern, filenames);
        for (const auto& f : filenames) {
            words.push_back(f);
        }
        c.push_back(words);
    }
    return c;
}

// =============计算匹配值函数=====================
double getMatchValue(string templatePath, Mat image) {
    // 读取模板图像
    Mat templateImage = imread(templatePath, IMREAD_GRAYSCALE);
    // 模板图像阈值处理, 灰度-->二值
    threshold(templateImage, templateImage, 0, 255, THRESH_OTSU);
    // 获取待识别图像的尺寸
    int height = image.rows;
    int width = image.cols;
    // 将模板图像调整为与待识别图像尺寸一致
    resize(templateImage, templateImage, Size(width, height));
    // 计算模板图像、待识别图像的模板匹配值
    Mat result;
    matchTemplate(image, templateImage, result, TM_CCOEFF);
    // 将计算结果返回
    double minVal, maxVal;
    cv::Point minLoc, maxLoc;
    minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc);
    return maxVal;
}

// ===========对车牌内字符进行识别====================
string matchChars(const vector<Mat>& plates, const vector<vector<string>>& chars) {
    string results;
    // 遍历要识别的字符
    for (const auto& plateChar : plates) {
        vector<double> bestMatch;
        // 遍历模板内的字符
        for (const auto& words : chars) {
            vector<double> match;
            // 遍历单个字符的所有模板
            for (const auto& word : words) {
                double result = getMatchValue(word, plateChar);
                match.push_back(result);
            }
            bestMatch.push_back(*max_element(match.begin(), match.end()));
        }
        int i = distance(bestMatch.begin(), max_element(bestMatch.begin(), bestMatch.end()));
        results += templateDict[i];
    }
    return results;
}

// ================主程序=============
int main() {
    // 读取原始图像
    Mat image = imread("gua.jpg");
    if (image.empty()) {
        cout << "Could not open or find the image!" << endl;
        return -1;
    }
    imshow("original", image);
    // 获取车牌
    image = getPlate(image);
    imshow("plate", image);
    // 预处理
    image = preprocessor(image);
    // 分割车牌,将每个字符独立出来
    vector<Mat> plateChars = splitPlate(image);
    for (size_t i = 0; i < plateChars.size(); ++i) {
        imshow("plateChars" + to_string(i), plateChars[i]);
    }
    // 获取所有模板文件(文件名)
    vector<vector<string>> chars = getCharacters();
    // 使用模板chars逐个识别字符集plates
    string results = matchChars(plateChars, chars);
    // 输出识别结果
    cout << "识别结果为:" << results << endl;
    waitKey(0);
    destroyAllWindows();
    return 0;
}

里面包含含所有步骤的注释。

本章在进行字符识别时,将每一个待识别字符与整个字符集进行了匹配值计算。实际上,在车牌中第一个字符是省份简称,只需要与汉字集进行匹配值计算即可;第二个字符是字母,只需要与字母集进行匹配值计算即可。因此,在具体实现时,可以对识别进行优化,以降低运算量,提高识别率。
除模板匹配以外,还可以尝试使用第三方包(如tesseract-ocr等)、深度学习等方式来实现车牌识别,更准确。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/786822.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【已解决】腾讯云安装了redis,但是本地访问不到,连接不上

汇总了我踩过的所有问题。 查看配置文件redis.conf 1、把bind 127.0.0.1给注释掉&#xff08;前面加个#就是&#xff09;或者改成bind 0.0.0.0&#xff0c;因为刚下载时它是默认只让本地访问。&#xff08;linux查找文档里的内容可以输入/后面加需要匹配的内容&#xff0c;然后…

FAO(脂肪酸β-氧化,Fatty acid beta-oxidation)应用实例

一、FAOBlue及其香豆素衍生物的吸收光谱和荧光光谱 在PBS缓冲液&#xff08;pH 7.4&#xff09;中&#xff0c;FAO代谢后释放的FAOBlue和香豆素衍生物的吸收光谱&#xff08;左&#xff09;、荧光光谱&#xff08;右&#xff09;。 FAOBlue经过FAO转化为香豆素衍生物后&#…

同步时钟系统支持多种校时方式

在当今数字化、信息化高速发展的时代&#xff0c;时间的准确性和同步性变得至关重要。无论是金融交易、通信网络、交通运输&#xff0c;还是工业生产、科学研究等领域&#xff0c;都离不开一个精确且同步的时钟系统。而同步时钟系统之所以能够在众多领域发挥关键作用&#xff0…

使用Python绘制箱线图并分析数据

使用Python绘制箱线图并分析数据 在这篇博客中&#xff0c;我们将探讨如何使用Python中的pandas库和matplotlib库来绘制箱线图&#xff0c;并分析数据文件中的内容。箱线图是一种常用的图表类型&#xff0c;用于展示数据的分布情况及其统计特性&#xff0c;如中位数、四分位数…

程序员日志之DNF手游强化20攻略

目录 传送门正文日志1、概要2、炭的获取3、强化 传送门 SpringMVC的源码解析&#xff08;精品&#xff09; Spring6的源码解析&#xff08;精品&#xff09; SpringBoot3框架&#xff08;精品&#xff09; MyBatis框架&#xff08;精品&#xff09; MyBatis-Plus SpringDataJP…

全能型CAE/CFD建模工具SimLab 详解Part1: Geomtry,轻松集成力学、电磁学、疲劳优化等功能

SimLab的建模功能 SimLab集成了结构力学&#xff0c;流体力学&#xff0c;电磁学&#xff0c;疲劳和优化等功能&#xff0c;是全能型的CAE / CFD建模工具。 具有强大的几何、网格编辑功能&#xff0c;能够快速的清理复杂模型&#xff0c;减少手动修复的工作量&#xff0c;提高…

websocket推送消息,模拟推送

上一篇文章&#xff1a;什么是webSocket&#xff1f;以及它的一些相关理论知识 背景&#xff1a; MQTT 的发布/订阅模式与 WebSocket 的双向通信特性相结合。 通过将 MQTT 与 WebSocket 结合使用&#xff0c;可以在 Web 应用中实现高效、实时的消息传输&#xff0c;特别适用于…

C# 下sendmessage和postmessage的区别详解与示例

文章目录 1、SendMessage2、PostMessage3、两者的区别&#xff1a; 总结 在C#中&#xff0c;SendMessage和PostMessage是两个用于Windows编程的API&#xff0c;它们用于向窗口发送消息。这两个方法都位于System.Windows.Forms命名空间中&#xff0c;通常用于自动化Windows应用程…

AI应用观:从“卷模型”到“卷应用”的时代跨越

在2024年世界人工智能大会的舞台上&#xff0c;百度创始人李彦宏的发言如同一股清流&#xff0c;为当前如火如荼的人工智能领域注入了深刻的思考。他提出的“大家不要卷模型&#xff0c;要卷应用”的观点&#xff0c;不仅是对当前AI技术发展趋势的精准洞察&#xff0c;更是对未…

帮企建站包响应式建站源码系统 带完整的安装代码包以及搭建部署教程

系统概述 帮企建站包响应式建站源码系统是一款为企业和个人提供便捷、高效建站解决方案的工具。它融合了先进的技术和设计理念&#xff0c;旨在帮助用户轻松构建具有专业水准的网站&#xff0c;无论在桌面端还是移动端都能呈现出完美的展示效果。 该系统基于响应式设计原则&a…

C++ 信号量和锁的区别

网上关于信号量和锁的区别&#xff0c;写的比较官方晦涩难懂&#xff0c;对于这个知识点吸收难&#xff0c;通过示例&#xff0c;我们看到信号量&#xff0c;可以控制同一时刻的线程数量&#xff0c;就算同时开启很多线程&#xff0c;依然可以的达到线程数可控 #include <i…

【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第一篇 嵌入式Linux入门篇-第十六章 Linux 第一个程序 HelloWorld

i.MX8MM处理器采用了先进的14LPCFinFET工艺&#xff0c;提供更快的速度和更高的电源效率;四核Cortex-A53&#xff0c;单核Cortex-M4&#xff0c;多达五个内核 &#xff0c;主频高达1.8GHz&#xff0c;2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT…

mysql 5.7.44 32位 zip安装

前言 因为研究别人代码&#xff0c;他使用了5.7的 32位 mysql &#xff0c;同时最新的 8.4 64位 mysql 不能用官方lib连接。所以安装这个版本使用&#xff0c;期间有些坑&#xff0c;在这里记录一下。 下载路径 mysql官方路径&#xff1a;https://downloads.mysql.com/archi…

【c语言】轻松拿捏自定义类型

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C语言 目录 前言 一、结构体 1.结构体类型的定义和使用 1.1 结构体类型声明 1.2 结构体变量的创建和初始化 1.3 结构体变量成员的访问 1.4 结构体的特殊声…

AI赋能OFFICE 智能化办公利器!

ONLYOFFICE在线编辑器的最新版本8.1已经发布&#xff0c;整个套件带来了30多个新功能和432个bug修复。这个文档编辑器无疑成为了办公软件中的翘楚。它不仅支持处理文本文档、电子表格、演示文稿、可填写的表单和PDF&#xff0c;还允许多人在线协作&#xff0c;并支持AI集成&…

linux 基础命令、gcc的基础用法

1、ls——>列出目录下的内容 语法&#xff1a;ls [-a -l -h] [Linux路径] &#xff08;1&#xff09;-a -l -h 是可选的选项 &#xff08;2&#xff09;Linux路径是此命令的可选参数 ①当不使用选项和参数&#xff0c;直接使用 ls 命令本体&#xff0c;表示&#xff1a;…

以终为始,胜意费控云「包干管控」助力精细管控与体验提升

在全球宏观经济环境的波动和内在经济逻辑的推动下&#xff0c;我国经济正经历着关键的结构调整期。如何稳健穿越周期&#xff0c;是企业必须直面的课题。与此同时&#xff0c;企业成本管控也面临着更为精细和严格的挑战。 企业需要一种更为灵活合理的费用管控策略。胜意费控云升…

3d模型墙模糊怎么回事?---模大狮模型网

在展览3D模型设计行业中&#xff0c;技术细节常常是设计师们需要面对和解决的关键问题之一。其中&#xff0c;3D模型墙模糊的现象可能会影响整个展览的视觉效果和观众的体验。本文将深入探讨这一问题的起因及解决方法&#xff0c;帮助设计师们更好地处理类似挑战。 一、问题的起…

Windows Server 2012 R2查看IIS版本

文章目录 一、方法一1.win R 键打开运行窗口 → 输入 "regedit" → 点击【确定】2.HKEY_LOCAL_MACHINE → SOFTWARE → Microsoft → InetStp 二、方法二1.win R 键打开运行窗口 → 输入 "inetmgr" → 点击【确定】2.点击 【帮助】 → 选择【关于 Intern…

【CVPR 2024】GART: Gaussian Articulated Template Models

【CVPR 2024】GART: Gaussian Articulated Template Models 一、前言Abstract1. Introduction2. Related Work3. Method3.1. Template Prior3.2. Shape Appearance Representation with GMM3.3. Motion Representation with Forward Skinning3.4. Reconstruct GART from Monocu…