模板匹配是一种基于图像处理的技术,用于在目标图像中寻找与给定模板图像最相似的部分。通过设定的模板,将目标图像与模板图像比较,计算其相似度,实现对目标图像的判断。
目录
一.手写数字识别
重要函数:
1.cv::glob
2. cv::matchTemplate
实现流程:
总结:
二.车牌识别
1.提取车牌
1.Sobel算子
cv::Sobel:
cv::convertScaleAbs
2.两次滤波方法的选择
3.开运算和闭运算核的大小选择
4.cv:: boundingRect
5.实现代码
2.分割车牌
1.重点步骤
2.实现代码
3.车牌识别
模板匹配实现数字识别流程图
功能:根据指定的模式匹配获取文件路径列表。
函数语法:
void cv::glob(const String &pattern, std::vector<String> &result, bool recursive);
pattern | 匹配文件的模式字符串。 例如 "/*.*" 表示该路径下的所有文件。 |
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());
}
功能:用于在一幅图像中搜索和匹配另一个图像(模板)。该函数通过滑动模板图像,并在每个位置计算匹配值,最终找出最佳匹配位置。
函数语法:
void cv::matchTemplate(
InputArray image,
InputArray templ,
OutputArray result,
int method);
image | 输入的源图像 |
templ | 用于匹配的模板图像 |
result | 输出的结果图像,其每个位置包含对应位置的匹配度。 |
method | 匹配方法,可以是以下之一:
|
使用示例:
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);
}
实现代码:
#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)识别字符:识别车牌上提取的字符。
提取车牌流程图
重要问题和函数:
用于边缘检测,重点提取车牌及其中字符的边缘。计算图像在X方向上的梯度能够突出垂直方向上的边缘。这对于检测图像中的物体边界、线条和其他显著的特征非常有用。
功能:使用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);
功能:将输入图像按比例缩放,并将其转换为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]内
高斯滤波前置:在边缘检测前使用高斯滤波,可以减少高频噪声对边缘检测的干扰,使边缘检测结果更准确。
中值滤波后置:在形态学处理后使用中值滤波,可以去除形态学操作可能引入的或未能去除的噪声,尤其是椒盐噪声,同时保持图像的边缘细节。
这种滤波顺序的选择是为了在每个处理阶段有效去除不同类型的噪声,提高图像处理效果,从而更好地实现车牌的定位和提取。
核大小的选择原因:
闭运算核(17, 5):用宽的水平核来连接水平分布的车牌字符。
开运算核(1, 19):用高的垂直核来消除垂直方向上的噪声而不影响水平的车牌字符。
选择这些核大小是基于车牌字符的典型排列方式(水平分布)以及背景噪声的形状特征。根据实际情况和图像特点,这些值可能需要进行调整以获得更好的效果。
功能:用于计算能够完全包含指定点集或轮廓的最小矩形边框。这个函数有两个常用的重载版本,一个用于处理点集(std::vector<cv::Point>
),另一个用于处理轮廓(std::vector<std::vector<cv::Point>>
)。这里用到的是处理轮廓。
函数语法:
cv::Rect cv::boundingRect(const std::vector<std::vector<cv::Point>>& contours)
使用示例:
cv::Rect rect = boundingRect(contours[i]);
int x = rect.x;
int y = rect.y;
int weight = rect.width;
int height = rect.height;
#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;
}
里面注释了每一步操作,在后续完整实现中需要将其封装成函数。
分割车牌是指将车牌中的各字符提取出来,以便进行后续识别。通常情况下,需要先对图像进行预处理(主要是进行去噪、二值化、膨胀等操作)以便提取每个字符的轮廓。接下来,寻找车牌内的所有轮廓,将其中高宽比符合字符特征的轮廓判定为字符。
车牌分割流程图
膨胀F4: 通常情况下,字符的各个笔画之间是分离的,通过膨胀操作可以让各字符形成一个整体。
轮廓F5: 该操作用来查找图像内的所有轮廓,可以使用函数findcontours完成。此时找到的轮廓非常多,既包含每个字符的轮廓,又包含噪声的轮廓。下一步工作是将字符的轮廓筛选出来。
包围框F6: 该操作让每个轮廓都被包围框包围,可以通过函数boundingRect完成。使用包围框替代轮廓的目的是,通过包围框的高宽比及宽度值,可以很方便地判定一个包围框包含的是噪声还是字符。
分割F7: 逐个遍历包围框,将其中宽高比在指定范围内、宽度大于特定值的包围框判定为字符。该操作可通过循环语句内置判断条件实现。
#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;
}
后续需要同提取车牌一样封装成函数。
由于每个字符的模板数量未必是一致的,即有的字符有较多的模板,有的字符有较少的模板,不同的模板数量为计算带来了不便,因此采用分层的方式实现模板匹配。先针对模板内的每个字符计算出一个与待识别字符最匹配的模板;然后在逐字符匹配结果中找出最佳匹配模板,从而确定最终识别结果。
完成程序:
#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等)、深度学习等方式来实现车牌识别,更准确。