OpenCV C++ 实现简单的车牌识别
下面使用 C++ 实现简单的 ROI 截取车牌区域图片,暂时还未实现使用 OCR 识别出车牌字符,后面考虑实现。
测试车牌图像如下:

开发环境:
- Qt :Qt5.12.8
- 编译器:MingW 64-bit(debug)
车牌识别的过程可分为四步:
- (1)车牌图像预处理;
- (2)查找轮廓和筛选轮廓;
- (3)使用颜色检测辅助定位车牌;
- (4)合并边缘和颜色的检测接管,检测出车牌区域;
- (5)ROI截取车牌区域图像;
一、车牌图像预处理
// 图像预处理 - 针对车牌检测优化
void preprocessImage(const Mat& input, Mat& gray, Mat& blurred, Mat& edges, Mat& morph) {
// 转换为灰度图
cvtColor(input, gray, COLOR_BGR2GRAY);
// 直方图均衡化增强对比度
equalizeHist(gray, gray);
// 高斯模糊去噪
GaussianBlur(gray, blurred, Size(5, 5), 0);
// 使用Canny边缘检测
Canny(blurred, edges, 50, 150, 3);
// 形态学操作 - 连接断开的边缘
Mat kernel = getStructuringElement(MORPH_RECT, Size(15, 3));
morphologyEx(edges, morph, MORPH_CLOSE, kernel);
// 开运算去除小噪点
Mat kernel2 = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(morph, morph, MORPH_OPEN, kernel2);
// 膨胀操作,连接车牌字符
Mat kernel3 = getStructuringElement(MORPH_RECT, Size(3, 1));
dilate(morph, morph, kernel3, Point(-1, -1), 2);
}
- 灰度处理将原图转化为灰度图像,可去除多通道产生的外界噪声;(下面图 1 为灰度处理后的灰度图像)
- 每一副图像都包含某种程度的噪声,这里采用高斯平滑对灰度图像进行降噪处理;(下面图 2 为进一步高斯模糊去糊后的图像)
- 完成了高斯去噪以后,我们要对图像进行边缘检测,我们这里采用的是 Canny 边缘检测;(下面图 3 为进一步边缘检测后的图像)
- 为了后面更加准确的提取车牌的轮廓,我们需要对图像进行形态学处理,分别使用闭运算连接断开的边缘、开运算去除小噪点、膨胀操作连接车牌字符;(下面图 4 为进一步闭运算、开运算和膨胀的图像)
预处理过程中的各车牌图像如下所示:

二、查找轮廓和筛选轮廓
这个时候,形态学操作后的图像(图4),可以看到车牌的轮廓已经初步被选出来了,只是还有一些白色块在干扰。所有我们先需要查找出所有轮廓:
// 2. 查找轮廓
vector<vector<Point>> contours;
findContours(morph, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 输出轮廓结构描述
for (size_t i = 0; i < contours.size(); i++) {
cout << contours[i] << endl;
}
查找的所有轮廓点值如下:
[258, 125;
256, 127;
255, 127;
255, 129;
261, 129;
261, 125]
// ...
102, 5;
115, 5;
115, 3;
113, 3;
112, 2;
111, 2;
110, 1;
110, 0]
现在我们已经查找出了所有轮廓,但是我们需要筛选出车牌所在的那个轮廓,由于车牌宽和高的比例是固定的,依据这个几何特征,我们进行筛选。
// 车牌检测参数
const double MIN_AREA = 2000; // 最小面积
const double MAX_AREA = 100000; // 最大面积
const double MIN_ASPECT_RATIO = 2.5; // 最小长宽比
const double MAX_ASPECT_RATIO = 6.0; // 最大长宽比
// 筛选轮廓 - 更严格的筛选条件
vector<Rect> filterContours(const vector<vector<Point>>& contours) {
vector<Rect> candidates;
for (const auto& contour : contours) {
// 计算轮廓面积
double area = contourArea(contour);
if (area < MIN_AREA || area > MAX_AREA) {
continue;
}
// 获取边界矩形
Rect boundingRect = cv::boundingRect(contour);
// 计算长宽比
double aspectRatio = (double)boundingRect.width / boundingRect.height;
if (aspectRatio < MIN_ASPECT_RATIO || aspectRatio > MAX_ASPECT_RATIO) {
continue;
}
// 计算轮廓的凸包
vector<Point> hull;
convexHull(contour, hull);
// 计算凸包面积与轮廓面积的比值
double hullArea = contourArea(hull);
double solidity = area / hullArea;
// 筛选较为规则的形状
if (solidity > 0.6) {
// 额外的筛选:检查矩形的填充率
double fillRatio = area / (boundingRect.width * boundingRect.height);
if (fillRatio > 0.3) {
candidates.push_back(boundingRect);
}
}
}
return candidates;
}
三、使用颜色检测辅助定位车牌
// 使用颜色检测辅助定位车牌
vector<Rect> detectByColor(const Mat& inputImage) {
vector<Rect> colorCandidates;
// 转换到HSV颜色空间
Mat hsv;
cvtColor(inputImage, hsv, COLOR_BGR2HSV);
// 检测蓝色车牌(中国车牌)
Mat blueMask;
inRange(hsv, Scalar(100, 50, 50), Scalar(130, 255, 255), blueMask);
// 检测绿色车牌
Mat greenMask;
inRange(hsv, Scalar(35, 50, 50), Scalar(85, 255, 255), greenMask);
// 检测黄色车牌
Mat yellowMask;
inRange(hsv, Scalar(20, 50, 50), Scalar(35, 255, 255), yellowMask);
// 合并所有颜色掩码
Mat colorMask;
add(blueMask, greenMask, colorMask);
add(colorMask, yellowMask, colorMask);
// 形态学操作
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5));
morphologyEx(colorMask, colorMask, MORPH_CLOSE, kernel);
morphologyEx(colorMask, colorMask, MORPH_OPEN, kernel);
// 查找轮廓
vector<vector<Point>> contours;
findContours(colorMask, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 筛选颜色检测的候选区域
for (const auto& contour : contours) {
double area = contourArea(contour);
if (area > 1000) {
Rect boundingRect = cv::boundingRect(contour);
double aspectRatio = (double)boundingRect.width / boundingRect.height;
if (aspectRatio > 2.0 && aspectRatio < 6.0) {
colorCandidates.push_back(boundingRect);
}
}
}
return colorCandidates;
}
颜色检测后的图像如下所示:

四、合并边缘和颜色的检测接管,检测出车牌区域
然后合并合并两种方法的检测结果,去除重复的检测框,检测出车牌区域矩形:
// 方法1:基于边缘检测的车牌检测
vector<Rect> edgeCandidates = detector.detectLicensePlates(inputImage);
// 方法2:基于颜色检测的车牌检测
vector<Rect> colorCandidates = detector.detectByColor(inputImage);
// 合并两种方法的检测结果
vector<Rect> allCandidates;
allCandidates.insert(allCandidates.end(), edgeCandidates.begin(), edgeCandidates.end());
allCandidates.insert(allCandidates.end(), colorCandidates.begin(), colorCandidates.end());
// 去除重复的检测框
vector<Rect> finalCandidates;
for (const auto& rect : allCandidates) {
bool isDuplicate = false;
for (const auto& existing : finalCandidates) {
// 检查重叠度
Rect intersection = rect & existing;
double overlapRatio = (double)intersection.area() / min(rect.area(), existing.area());
if (overlapRatio > 0.5) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
finalCandidates.push_back(rect);
}
}
if (finalCandidates.empty()) {
cout << "未检测到车牌区域" << endl;
imshow("Original Image", inputImage);
waitKey(0);
return 0;
}
cout << "检测到 " << finalCandidates.size() << " 个候选车牌区域" << endl;
五、ROI截取车牌区域图像
// 提取ROI区域
Mat extractROI(const Mat& inputImage, const Rect& roi) {
// 确保ROI在图像范围内
Rect validROI = roi & Rect(0, 0, inputImage.cols, inputImage.rows);
if (validROI.width <= 0 || validROI.height <= 0) {
return Mat();
}
// 提取ROI并调整大小
Mat extractedROI = inputImage(validROI);
// 调整ROI大小到标准尺寸
Mat resizedROI;
resize(extractedROI, resizedROI, Size(300, 100));
return resizedROI;
}
// 保存ROI图像
bool saveROI(const Mat& roi, const string& filename) {
if (roi.empty()) {
cout << "ROI图像为空,无法保存" << endl;
return false;
}
return imwrite(filename, roi);
}
// 显示结果
void displayResults(const Mat& inputImage, const vector<Rect>& candidates, const vector<Mat>& rois) {
Mat result = inputImage.clone();
// 在原图上绘制检测框
for (size_t i = 0; i < candidates.size(); i++) {
rectangle(result, candidates[i], Scalar(0, 255, 0), 2);
// 添加标签
string label = "Plate " + to_string(i + 1);
putText(result, label, Point(candidates[i].x, candidates[i].y - 10),
FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
}
// 显示原图和结果
imshow("Original Image", inputImage);
imshow("Detection Result", result);
// 显示ROI图像
for (size_t i = 0; i < rois.size(); i++) {
if (!rois[i].empty()) {
string windowName = "License Plate ROI " + to_string(i + 1);
imshow(windowName, rois[i]);
}
}
waitKey(0);
}
最后调用这些函数:
// 提取ROI区域
vector<Mat> rois;
for (size_t i = 0; i < finalCandidates.size(); i++) {
Mat roi = detector.extractROI(inputImage, finalCandidates[i]);
if (!roi.empty()) {
rois.push_back(roi);
// 保存ROI图像
string filename = "license_plate_roi_" + to_string(i + 1) + ".jpg";
if (detector.saveROI(roi, filename)) {
cout << "已保存ROI图像: " << filename << endl;
}
}
}
// 显示结果
detector.displayResults(inputImage, finalCandidates, rois);
cout << "检测完成!按任意键退出..." << endl;
在原图上绘制检测框,以确保我们已正确检测到车牌,效果图如下所示:

ROI 截取车牌区域图像后,提取 ROI 区域并显示,ROI 图像如下所示:

六、全部代码
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <vector>
#include <string>
using namespace cv;
using namespace std;
class LicensePlateDetector {
private:
// 车牌检测参数
const double MIN_AREA = 2000; // 最小面积
const double MAX_AREA = 100000; // 最大面积
const double MIN_ASPECT_RATIO = 2.5; // 最小长宽比
const double MAX_ASPECT_RATIO = 6.0; // 最大长宽比
public:
// 主检测函数
vector<Rect> detectLicensePlates(const Mat& inputImage) {
Mat gray, blurred, edges, morph;
vector<Rect> candidates;
// 1. 图像预处理
preprocessImage(inputImage, gray, blurred, edges, morph);
// 2. 查找轮廓
vector<vector<Point>> contours;
findContours(morph, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 输出轮廓结构描述
for (size_t i = 0; i < contours.size(); i++) {
cout << contours[i] << endl;
}
// 3. 筛选候选区域
candidates = filterContours(contours);
return candidates;
}
// 图像预处理 - 针对车牌检测优化
void preprocessImage(const Mat& input, Mat& gray, Mat& blurred, Mat& edges, Mat& morph) {
// 转换为灰度图
cvtColor(input, gray, COLOR_BGR2GRAY);
// 直方图均衡化增强对比度
equalizeHist(gray, gray);
// 高斯模糊去噪
GaussianBlur(gray, blurred, Size(5, 5), 0);
// 使用Canny边缘检测
Canny(blurred, edges, 50, 150, 3);
// 形态学操作 - 连接断开的边缘
Mat kernel = getStructuringElement(MORPH_RECT, Size(15, 3));
morphologyEx(edges, morph, MORPH_CLOSE, kernel);
// 开运算去除小噪点
Mat kernel2 = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(morph, morph, MORPH_OPEN, kernel2);
// 膨胀操作,连接车牌字符
Mat kernel3 = getStructuringElement(MORPH_RECT, Size(3, 1));
dilate(morph, morph, kernel3, Point(-1, -1), 2);
}
// 筛选轮廓 - 更严格的筛选条件
vector<Rect> filterContours(const vector<vector<Point>>& contours) {
vector<Rect> candidates;
for (const auto& contour : contours) {
// 计算轮廓面积
double area = contourArea(contour);
if (area < MIN_AREA || area > MAX_AREA) {
continue;
}
// 获取边界矩形
Rect boundingRect = cv::boundingRect(contour);
// 计算长宽比
double aspectRatio = (double)boundingRect.width / boundingRect.height;
if (aspectRatio < MIN_ASPECT_RATIO || aspectRatio > MAX_ASPECT_RATIO) {
continue;
}
// 计算轮廓的凸包
vector<Point> hull;
convexHull(contour, hull);
// 计算凸包面积与轮廓面积的比值
double hullArea = contourArea(hull);
double solidity = area / hullArea;
// 筛选较为规则的形状
if (solidity > 0.6) {
// 额外的筛选:检查矩形的填充率
double fillRatio = area / (boundingRect.width * boundingRect.height);
if (fillRatio > 0.3) {
candidates.push_back(boundingRect);
}
}
}
return candidates;
}
// 使用颜色检测辅助定位车牌
vector<Rect> detectByColor(const Mat& inputImage) {
vector<Rect> colorCandidates;
// 转换到HSV颜色空间
Mat hsv;
cvtColor(inputImage, hsv, COLOR_BGR2HSV);
// 检测蓝色车牌(中国车牌)
Mat blueMask;
inRange(hsv, Scalar(100, 50, 50), Scalar(130, 255, 255), blueMask);
// 检测绿色车牌
Mat greenMask;
inRange(hsv, Scalar(35, 50, 50), Scalar(85, 255, 255), greenMask);
// 检测黄色车牌
Mat yellowMask;
inRange(hsv, Scalar(20, 50, 50), Scalar(35, 255, 255), yellowMask);
// 合并所有颜色掩码
Mat colorMask;
add(blueMask, greenMask, colorMask);
add(colorMask, yellowMask, colorMask);
// 形态学操作
Mat kernel = getStructuringElement(MORPH_RECT, Size(5, 5));
morphologyEx(colorMask, colorMask, MORPH_CLOSE, kernel);
morphologyEx(colorMask, colorMask, MORPH_OPEN, kernel);
// 查找轮廓
vector<vector<Point>> contours;
findContours(colorMask, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 筛选颜色检测的候选区域
for (const auto& contour : contours) {
double area = contourArea(contour);
if (area > 1000) {
Rect boundingRect = cv::boundingRect(contour);
double aspectRatio = (double)boundingRect.width / boundingRect.height;
if (aspectRatio > 2.0 && aspectRatio < 6.0) {
colorCandidates.push_back(boundingRect);
}
}
}
return colorCandidates;
}
// 提取ROI区域
Mat extractROI(const Mat& inputImage, const Rect& roi) {
// 确保ROI在图像范围内
Rect validROI = roi & Rect(0, 0, inputImage.cols, inputImage.rows);
if (validROI.width <= 0 || validROI.height <= 0) {
return Mat();
}
// 提取ROI并调整大小
Mat extractedROI = inputImage(validROI);
// 调整ROI大小到标准尺寸
Mat resizedROI;
resize(extractedROI, resizedROI, Size(300, 100));
return resizedROI;
}
// 保存ROI图像
bool saveROI(const Mat& roi, const string& filename) {
if (roi.empty()) {
cout << "ROI图像为空,无法保存" << endl;
return false;
}
return imwrite(filename, roi);
}
// 显示结果
void displayResults(const Mat& inputImage, const vector<Rect>& candidates, const vector<Mat>& rois) {
Mat result = inputImage.clone();
// 在原图上绘制检测框
for (size_t i = 0; i < candidates.size(); i++) {
rectangle(result, candidates[i], Scalar(0, 255, 0), 2);
// 添加标签
string label = "Plate " + to_string(i + 1);
putText(result, label, Point(candidates[i].x, candidates[i].y - 10),
FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
}
// 显示原图和结果
imshow("Original Image", inputImage);
imshow("Detection Result", result);
// 显示ROI图像
for (size_t i = 0; i < rois.size(); i++) {
if (!rois[i].empty()) {
string windowName = "License Plate ROI " + to_string(i + 1);
imshow(windowName, rois[i]);
}
}
waitKey(0);
}
// 显示处理过程
void showProcessingSteps(const Mat& inputImage) {
Mat gray, blurred, edges, morph;
// 预处理
preprocessImage(inputImage, gray, blurred, edges, morph);
// 显示处理步骤
imshow("1. Grayscale", gray);
imshow("2. Gaussian Blur", blurred);
imshow("3. Edge Detection", edges);
imshow("4. Morphological Processing", morph);
// 颜色检测
Mat hsv;
cvtColor(inputImage, hsv, COLOR_BGR2HSV);
Mat blueMask, greenMask, yellowMask;
inRange(hsv, Scalar(100, 50, 50), Scalar(130, 255, 255), blueMask);
inRange(hsv, Scalar(35, 50, 50), Scalar(85, 255, 255), greenMask);
inRange(hsv, Scalar(20, 50, 50), Scalar(35, 255, 255), yellowMask);
Mat colorMask;
add(blueMask, greenMask, colorMask);
add(colorMask, yellowMask, colorMask);
imshow("5. Color Detection", colorMask);
waitKey(0);
}
};
int main() {
// 创建检测器实例
LicensePlateDetector detector;
// 读取图像
string imagePath = "licensePlate1.png"; // 请替换为您的图像路径
Mat inputImage = imread(imagePath);
if (inputImage.empty()) {
cout << "无法读取图像文件: " << imagePath << endl;
cout << "请确保图像文件存在,或者修改imagePath变量指向正确的图像文件" << endl;
return -1;
}
cout << "开始车牌检测..." << endl;
// 显示处理步骤(可选)
detector.showProcessingSteps(inputImage);
// 方法1:基于边缘检测的车牌检测
vector<Rect> edgeCandidates = detector.detectLicensePlates(inputImage);
// 方法2:基于颜色检测的车牌检测
vector<Rect> colorCandidates = detector.detectByColor(inputImage);
// 合并两种方法的检测结果
vector<Rect> allCandidates;
allCandidates.insert(allCandidates.end(), edgeCandidates.begin(), edgeCandidates.end());
allCandidates.insert(allCandidates.end(), colorCandidates.begin(), colorCandidates.end());
// 去除重复的检测框
vector<Rect> finalCandidates;
for (const auto& rect : allCandidates) {
bool isDuplicate = false;
for (const auto& existing : finalCandidates) {
// 检查重叠度
Rect intersection = rect & existing;
double overlapRatio = (double)intersection.area() / min(rect.area(), existing.area());
if (overlapRatio > 0.5) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
finalCandidates.push_back(rect);
}
}
if (finalCandidates.empty()) {
cout << "未检测到车牌区域" << endl;
imshow("Original Image", inputImage);
waitKey(0);
return 0;
}
cout << "检测到 " << finalCandidates.size() << " 个候选车牌区域" << endl;
// 提取ROI区域
vector<Mat> rois;
for (size_t i = 0; i < finalCandidates.size(); i++) {
Mat roi = detector.extractROI(inputImage, finalCandidates[i]);
if (!roi.empty()) {
rois.push_back(roi);
// 保存ROI图像
string filename = "license_plate_roi_" + to_string(i + 1) + ".jpg";
if (detector.saveROI(roi, filename)) {
cout << "已保存ROI图像: " << filename << endl;
}
}
}
// 显示结果
detector.displayResults(inputImage, finalCandidates, rois);
cout << "检测完成!按任意键退出..." << endl;
return 0;
}

浙公网安备 33010602011771号