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

news2024/12/28 5:57:20

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

目录

一.手写数字识别

重要函数:

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等)、深度学习等方式来实现车牌识别,更准确。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mac平台虚拟机 Parallels Desktop v19.4.1,支持M1/M2/M3芯片组

Parallels Desktop for Mac是功能强大灵活度高的虚拟化方案&#xff0c;无需重启即可在同一台电脑上随时访问Windows和Mac两个系统上的众多应用程序。从仅限于PC的游戏到生产力软件&#xff0c;Parallels Desktop都能帮您实现便捷使用。Parallels Desktop 是一款专业的Mac虚拟机…

虚拟机因断电进入./#状态解决办法

现象&#xff1a; 解决&#xff1a;先查看错误日志&#xff1a;journalctl -p err -b查看自己虚拟机中标黄部分的名字 之后运行&#xff1a;xfs_repair -v -L /dev/sda #这里sda用你自己标黄的 最后重启 reboot 即可。

ArcGIS的智慧与情怀

初识ArcGIS 在这个信息化的时代&#xff0c;ArcGIS如同一位智者&#xff0c;静静地伫立在地理信息系统的巅峰。初识它时&#xff0c;我仿佛走进了一片未知的领域&#xff0c;心中充满了好奇与期待。ArcGIS&#xff0c;这款专业的地理信息系统软件&#xff0c;凭借其强大的功能…

【k8s中安装rabbitmq】k8s中安装rabbitmq并搭建镜像集群-hostpath版

文章目录 简介一.条件及环境说明二.需求说明三.实现原理及说明四.详细步骤4.1.规划节点标签4.2.创建configmap配置4.3.创建三个statefulset和service headless配置4.4.创建service配置 五.安装完后的配置六.安装说明 简介 k8s集群中搭建rabbitmq集群服务一般都会用到pvc&#x…

传知代码-图神经网络长对话理解(论文复现)

代码以及视频讲解 本文所涉及所有资源均在传知代码平台可获取 概述 情感识别是人类对话理解的关键任务。随着多模态数据的概念&#xff0c;如语言、声音和面部表情&#xff0c;任务变得更加具有挑战性。作为典型解决方案&#xff0c;利用全局和局部上下文信息来预测对话中每…

2024世界人工智能大会:AI产品技术与未来趋势的深度解析

随着2024年世界人工智能大会&#xff08;WAIC 2024&#xff09;在上海的圆满落幕&#xff0c;我们见证了人工智能技术的又一次飞跃。本次大会以“以共商促共享&#xff0c;以善治促善智”为主题&#xff0c;汇聚了全球顶尖的智慧&#xff0c;共同探讨了AI技术的未来趋势和应用前…

妙笔生词智能写歌词软件:创新助力还是艺术之殇?

在音乐创作日益普及和多样化的当下&#xff0c;各种辅助工具层出不穷&#xff0c;妙笔生词智能写歌词软件便是其中之一。那么&#xff0c;它到底表现如何呢&#xff1f; 妙笔生词智能写歌词软件&#xff08;veve522&#xff09;的突出优点在于其便捷性和高效性。对于那些灵感稍…

JVM内存泄露的ThreadLocal详解

目录 一、为什么要有ThreadLocal 二、ThreadLocal的使用 三、实现解析 实现分析 具体实现 Hash冲突的解决 开放定址法 链地址法 再哈希法 建立公共溢出区 四、引发的内存泄漏分析 内存泄漏的现象 分析 总结 错误使用ThreadLocal导致线程不安全 一、为什么要有Thr…

Test-Time Adaptation via Conjugate Pseudo-labels--论文笔记

论文笔记 资料 1.代码地址 https://github.com/locuslab/tta_conjugate 2.论文地址 https://arxiv.org/abs/2207.09640 3.数据集地址 论文摘要的翻译 测试时间适应(TTA)指的是使神经网络适应分布变化&#xff0c;在测试时间仅访问来自新领域的未标记测试样本。以前的TT…

【pytorch24】Visdom可视化

TensorboardX pytorch有一个工具借鉴了tensorboard pip install tensorboardX 有查看变量的数值、监听曲线等功能 如何使用 新建SummaryWriter()实例 要把监听的数据&#xff0c;比如说要监听dummy_s1[0]&#xff08;y 坐标&#xff09;存放到data/scalar1中&#xff0c;…

普中51单片机:中断系统与寄存器解析(六)

文章目录 引言中断流程图中断优先级下降沿中断结构图中断相关寄存器IE中断允许寄存器&#xff08;可位寻址&#xff09;XICON辅助中断控制寄存器&#xff08;可位寻址&#xff09;TCON标志控制寄存器SCON串行口控制寄存器 中断号中断响应条件中断函数代码模板电路图开发板IO连接…

洁净车间的压缩空气质量如何检测(露点、水油、粒子、浮游菌)

通常一个空压机站的设备即为一个狭义的压缩空气系统&#xff0c;下图为一个典型的压缩空气系统流程图&#xff1a; 气源设备&#xff08;空气压缩机&#xff09;吸入大气&#xff0c;将自然状态下的空气压缩成为具有较高压力的压缩空气&#xff0c;经过净化设备除去压缩空气中的…

新手如何正确学习Python?分享我是如何2个月熟练掌握Python的!学习大纲+学习方式+学习资料 汇总!

前言 一直以来都有很多想学习Python的朋友们问我&#xff0c;学Python怎么学&#xff1f;爬虫和数据分析怎么学&#xff1f;web开发的学习路线能教教我吗&#xff1f; 我先告诉大家一个点&#xff0c;不管你是报了什么培训班&#xff0c;还是自己在通过各种渠道自学&#xff…

[C++][ProtoBuf][Proto3语法][三]详细讲解

目录 1.默认值2.更新消息1.更新规则2.保留字段reserved 3.未知字段1.是什么&#xff1f;2.未知字段从哪获取 4.前后兼容性5.选项option1.选项分类2.常用选项列举3.设置自定义选项 1.默认值 反序列化消息时&#xff0c;如果被反序列化的⼆进制序列中不包含某个字段&#xff0c;…

elasticsearch集群模式部署

系统版本&#xff1a;CentOS Linux release 7.9.2009 (Core) es版本&#xff1a; elasticsearch-7.6.2 本次搭建es集群为三个节点 添加启动用户 确保elasticsearch的启动用户为普通用户&#xff0c;这里我创建了es用户用于启动elasticsearch 执行命令为es用户添加sudo权限 v…

数学建模及国赛

认识数学建模及国赛 认识数学建模 环境类&#xff1a;预测一下明天的气温 实证类&#xff1a; 评价一下政策的优缺点 农业类&#xff1a; 预测一下小麦的产量 财经类&#xff1a; 分析一下理财产品的最优组合 规划类&#xff1a; 土地利用情况进行 合理的划分 力学类&#xf…

如何在 CentOS 中配置 Linux 命名空间(ip netns)

引言 Linux 命名空间是一项强大的技术&#xff0c;允许在同一系统上创建多个独立的虚拟化实例&#xff0c;每个实例可以拥有自己的网络栈、路由表、IP 地址等网络资源&#xff0c;实现资源的隔离和管理。本文将深入探讨如何在 CentOS 中配置和使用 ip netns 命名空间&#xff0…

网络安全合规建设

网络安全合规建设 一、法律安全需求基本合规&#xff08;1&#xff09;《网络安全法》重要节点等级保护政策核心变化 二、安全需求 业务刚需&#xff08;1&#xff09;内忧&#xff08;2&#xff09;外患 三、解决方法&#xff08;1&#xff09;总安全战略目标图&#xff08;2&…

PaddleVideo:Squeeze Time算法移植

参考PaddleVideo/docs/zh-CN/contribute/add_new_algorithm.md at develop PaddlePaddle/PaddleVideo GitHubAwesome video understanding toolkits based on PaddlePaddle. It supports video data annotation tools, lightweight RGB and skeleton based action recognitio…

Xilinx FPGA UltraScale SelectIO 接口逻辑资源

目录 1. 简介 2. Bank Overview 2.1 Diagram 2.2 IOB 2.3 Slice 2.4 Byte Group 2.5 I/O bank 示例 2.6 Pin Definition 2.7 数字控制阻抗(DCI) 2.8 SelectIO 管脚供电电压 2.8.1 VCCO 2.8.2 VREF 2.8.3 VCCAUX 2.8.4 VCCAUX_IO 2.8.5 VCCINT_IO 3. 总结 1. 简介…