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位有符号整数))。
dx X方向上的差分阶数(例如,1)。
dy Y方向上的差分阶数(例如,0)。
ksize Sobel算子的核大小(默认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等)、深度学习等方式来实现车牌识别,更准确。

相关推荐

  1. C#实现字符串模糊匹配

    2024-07-10 17:46:01       6 阅读
  2. 【Rust】第节:枚举与模式匹配

    2024-07-10 17:46:01       48 阅读
  3. 跟我学C++中级篇——函数模板匹配

    2024-07-10 17:46:01       37 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-10 17:46:01       5 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-10 17:46:01       5 阅读
  3. 在Django里面运行非项目文件

    2024-07-10 17:46:01       4 阅读
  4. Python语言-面向对象

    2024-07-10 17:46:01       6 阅读

热门阅读

  1. 获取和设置Spring Cookie

    2024-07-10 17:46:01       11 阅读
  2. Spring——配置说明

    2024-07-10 17:46:01       9 阅读
  3. springboot中在filter中用threadlocal存放用户身份信息

    2024-07-10 17:46:01       16 阅读
  4. LDAP技术解析:打造安全、高效的企业数据架构

    2024-07-10 17:46:01       12 阅读
  5. android 替换设置-安全里面的指纹背景图片

    2024-07-10 17:46:01       14 阅读
  6. Node.js的应用场景

    2024-07-10 17:46:01       12 阅读
  7. 并发请求的艺术:Postman中实现高效API测试

    2024-07-10 17:46:01       13 阅读