OpenCV车牌识别(C++)
1 车牌定位与字符分割
1.1 图片预处理
在进行车牌定位之前,首先需要对原始图像进行一系列预处理操作,以增强车牌区域的特征,去除背景干扰。
1.1.1 缩放图像
将原图缩放至限定大小(如1024×720以内),保持宽高比,避免处理大图导致的运算开销过大,避免处理小图导致无法定位。
double scale = std::min(static_cast<double>(maxWidth) / origin.cols, static_cast<double>(maxHeight) / origin.rows);
cv::resize(origin, resizedImg, cv::Size(), scale, scale, cv::INTER_LINEAR);
1.1.2 灰度转换与高斯模糊
灰度化简化图像处理,去除颜色干扰,高斯模糊降低噪声,避免边缘检测时产生伪边缘。
cv::cvtColor(resizedImg, grayImg, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(grayImg, blurImg, blurKernel, 0);
1.1.3 Gamma 校正(灰度拉伸)
通过幂次变换(Gamma Correction)对图像灰度进行非线性映射,使车牌区域更加突出。
blurImg.convertTo(normImg, CV_32F, 1.0 / 255.0);
cv::pow(normImg, gamma, gammaImg);
gammaImg.convertTo(stretchGrayImg, CV_8U, 255.0);
1.1.4 顶帽变换与差分增强
利用原图与开运算图的差分提取图像中的亮区域,车牌往往比周围区域更亮,这样可增强其特征。
cv::morphologyEx(stretchGrayImg, openImg, cv::MORPH_OPEN, rectKernel);
cv::absdiff(stretchGrayImg, openImg, diffImg);
1.1.5 二值化与边缘检测
利用Otsu自适应阈值算法将图像二值化,提取潜在车牌区域;使用Canny边缘检测提取轮廓,有助于后续形状筛选与定位。
cv::threshold(diffImg, binaryImg, 0, 255, cv::THRESH_BINARY + cv::THRESH_OTSU);
cv::Canny(binaryImg, edgeImg, canny1, canny2);
1.1.6 形态学闭运算 + 开运算
闭运算可以填补断裂的车牌轮廓;开运算可以去除多余的小连通区域;多次反复操作增强车牌轮廓连通性,利于轮廓检测。
cv::morphologyEx(edgeImg, closeImg1, cv::MORPH_CLOSE, kernel1);
cv::morphologyEx(closeImg1, openImg1, cv::MORPH_OPEN, kernel2);
cv::morphologyEx(openImg1, closeImg2, cv::MORPH_CLOSE, kernel1);
cv::morphologyEx(closeImg2, openImg2, cv::MORPH_OPEN, kernel2);
1.1.7 预处理函数代码
void PlateLocator::preprocess(const cv::Mat& origin, cv::Mat& resized, cv::Mat& preprocessed) const {
cv::Mat resizedImg, grayImg, blurImg, stretchGrayImg, openImg, diffImg, binaryImg, edgeImg;
cv::Mat closeImg1, closeImg2, openImg1, openImg2;
double scale = std::min(static_cast<double>(maxWidth) / origin.cols, static_cast<double>(maxHeight) / origin.rows);
cv::resize(origin, resizedImg, cv::Size(), scale, scale, cv::INTER_LINEAR);
resized = resizedImg.clone();
cv::cvtColor(resizedImg, grayImg, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(grayImg, blurImg, blurKernel, 0);
cv::Mat normImg, gammaImg;
blurImg.convertTo(normImg, CV_32F, 1.0 / 255.0);
cv::pow(normImg, gamma, gammaImg);
gammaImg.convertTo(stretchGrayImg, CV_8U, 255.0);
cv::Mat rectKernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(static_cast<int>(3.14 * radius), radius));
cv::morphologyEx(stretchGrayImg, openImg, cv::MORPH_OPEN, rectKernel);
cv::absdiff(stretchGrayImg, openImg, diffImg);
cv::threshold(diffImg, binaryImg, 0, 255, cv::THRESH_BINARY + cv::THRESH_OTSU);
cv::Canny(binaryImg, edgeImg, canny1, canny2);
cv::Mat kernel1 = cv::getStructuringElement(cv::MORPH_RECT, kernel1Size);
cv::Mat kernel2 = cv::getStructuringElement(cv::MORPH_RECT, kernel2Size);
cv::morphologyEx(edgeImg, closeImg1, cv::MORPH_CLOSE, kernel1);
cv::morphologyEx(closeImg1, openImg1, cv::MORPH_OPEN, kernel2);
cv::morphologyEx(openImg1, closeImg2, cv::MORPH_CLOSE, kernel1);
cv::morphologyEx(closeImg2, openImg2, cv::MORPH_OPEN, kernel2);
preprocessed = openImg2.clone();
}
1.2 车牌定位
在预处理后的图像中,我们需要从众多轮廓中筛选出可能为车牌的矩形区域。
1.2.1 轮廓提取
cv::findContours(preprocessedImg, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
使用 RETR_EXTERNAL 提取最外层轮廓,忽略嵌套结构,CHAIN_APPROX_SIMPLE 轮廓点压缩表示,提高效率。
1.2.2 筛选条件设计(形状+面积+填充)
为了准确定位车牌,必须对每个候选矩形进行以下多维度筛选:
形状判断:宽高比
float aspectRatio = w / h;
if (aspectRatio < minAspectRatio || aspectRatio > maxAspectRatio) continue;
车牌通常是宽 > 高的长方形,理论宽高比为3.14,若矩形太窄或太高,视为非车牌区域。
尺寸判断:面积占比
float areaRatio = (w * h) / totalArea;
if (areaRatio < minRectAreaRatio || areaRatio > maxRectAreaRatio) continue;
过滤过小/过大的矩形,避免误识别图像边缘或大型非车牌区域。
纹理判断:区域填充度
cv::Mat roi = preprocessedImg(rect);
float fillRatio = static_cast<float>(cv::countNonZero(roi)) / (w * h);
if (fillRatio < minFillRatio) continue;
填充度 = 白色像素数 / 矩形面积,白色像素代表边缘或高响应区域,车牌区域通常有较多边缘特征,填充度过低说明该区域可能是空洞或纯背景。
1.2.3 候选车牌排序
std::sort(plateRects.begin(), plateRects.end(),
[targetAspectRatio](const cv::Rect& a, const cv::Rect& b) {
...
});
if (plateRects.size() > remain) plateRects.resize(remain);
按照宽高比与目标比值的接近程度排序,并·控制返回结果数量,便于后续多车牌识别。
1.2.4 车牌定位函数代码
std::vector<cv::Rect> PlateLocator::locatePlates(
const cv::Mat& preprocessedImg,
float minAspectRatio,
float maxAspectRatio,
float targetAspectRatio,
float minRectAreaRatio,
float maxRectAreaRatio,
float minFillRatio,
int remain
) const {
std::vector<std::vector<cv::Point>> contours;
cv::findContours(preprocessedImg, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
std::vector<cv::Rect> plateRects;
float totalArea = static_cast<float>(preprocessedImg.cols * preprocessedImg.rows);
for (const auto& contour : contours) {
cv::Rect rect = cv::boundingRect(contour);
float w = rect.width, h = rect.height;
if (w == 0 || h == 0) continue;
float aspectRatio = w / h;
float areaRatio = (w * h) / totalArea;
if (aspectRatio < minAspectRatio || aspectRatio > maxAspectRatio) continue;
if (areaRatio < minRectAreaRatio || areaRatio > maxRectAreaRatio) continue;
cv::Mat roi = preprocessedImg(rect);
float fillRatio = static_cast<float>(cv::countNonZero(roi)) / (w * h);
if (fillRatio < minFillRatio) continue;
plateRects.push_back(rect);
}
std::sort(plateRects.begin(), plateRects.end(),
[targetAspectRatio](const cv::Rect& a, const cv::Rect& b) {
float aspectA = static_cast<float>(a.width) / a.height;
float aspectB = static_cast<float>(b.width) / b.height;
return std::abs(aspectA - targetAspectRatio) < std::abs(aspectB - targetAspectRatio);
});
if (plateRects.size() > remain) plateRects.resize(remain);
return plateRects;
}
1.3 字符分割
成功定位到车牌后,接下来需要将整块车牌图像中的字符逐个提取出来,为后续识别模型做准备。
1.3.1 图像标准化:统一尺寸
cv::Mat resized = resizeToMinWidth(plateImg, 100);
将车牌图像宽度统一缩放至最小值,保持处理图片大小一致,有利于后续阈值判断、轮廓分析。
1.3.2 灰度化与自适应二值化
cv::cvtColor(resized, gray, cv::COLOR_BGR2GRAY);
cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY + cv::THRESH_OTSU);
if (cv::mean(binary)[0] > 128) cv::bitwise_not(binary, binary);
灰度化简化图像内容,THRESH_OTSU 自动选择阈值进行二值化,若背景为白、字符为黑(如反色车牌),则自动取反,确保“字符为白、背景为黑”的统一格式。
1.3.3 闭运算修复字符断裂
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 10));
cv::morphologyEx(binary, morph, cv::MORPH_CLOSE, kernel);
字符经过边缘处理可能出现断裂,闭运算可连接笔画、填补字符间小缝隙,使字符更完整便于轮廓检测。
1.3.4 查找字符候选轮廓
cv::findContours(morph, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
使用外轮廓提取字符边界,每个闭合区域即为潜在字符。
1.3.5 候选字符筛选条件
float aspectRatio = width / height;
float areaRatio = rectArea / totalPlateArea;
float fillRatio = nonZero / rectArea;
字符判别标准(尝试多次后最优阈值):
| 条件 | 含义 | 典型范围 |
|---|---|---|
areaRatio > 0.01 | 候选字符区域不能太小 | > 1% |
0.4 < aspectRatio < 1.0 | 字符形状近似正方形或竖条 | 0.4 ~ 1.0 |
fillRatio > 0.2 | 白色像素占比必须合理(即字符必须有足够笔画) | > 20% |
注意:这些参数对可能需微调。
1.3.6 从左到右排序字符
std::sort(candidateRects.begin(), candidateRects.end(), [](a, b) { return a.x < b.x; });
按字符在图像中的 x 坐标排序,确保识别顺序正确,如“鲁A12345”。
1.3.7 提取并保存字符图像
for (...) {
characters.push_back(binary(rect).clone());
}
从原始二值图中按位置提取字符图像,作为输入送入字符识别模型。
1.3.8 字符分割代码
std::vector<cv::Mat> PlateLocator::segmentCharacters(const cv::Mat& plateImg) const {
std::vector<cv::Mat> characters;
if (plateImg.empty()) return characters;
cv::Mat resized = resizeToMinWidth(plateImg, 100);
cv::Mat gray;
cv::cvtColor(resized, gray, cv::COLOR_BGR2GRAY);
cv::Mat binary;
cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY + cv::THRESH_OTSU);
double meanVal = cv::mean(binary)[0];
if (cv::mean(binary)[0] > 128) cv::bitwise_not(binary, binary);
cv::Mat morph;
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 10));
cv::morphologyEx(binary, morph, cv::MORPH_CLOSE, kernel);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(morph, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
std::vector<cv::Rect> candidateRects;
for (const auto& contour : contours) {
cv::Rect rect = cv::boundingRect(contour);
float aspectRatio = static_cast<float>(rect.width) / rect.height;
int plateArea = morph.cols * morph.rows;
int rectArea = rect.width * rect.height;
float areaRatio = static_cast<float>(rectArea) / plateArea;
cv::Mat roi = morph(rect);
float fillRatio = static_cast<float>(cv::countNonZero(roi)) / (rect.width * rect.height);
if (areaRatio > 0.01f && aspectRatio > 0.4f && aspectRatio < 1.0f && fillRatio > 0.2f) {
candidateRects.push_back(rect);
}
}
std::sort(candidateRects.begin(), candidateRects.end(),
[](const cv::Rect& a, const cv::Rect& b) {
return a.x < b.x;
});
for (const auto& rect : candidateRects) {
cv::Mat charBin = binary(rect);
characters.push_back(charBin.clone());
}
return characters;
}
1.4 类头文件
#ifndef PLATE_LOCATOR_H
#define PLATE_LOCATOR_H
#include <opencv2/opencv.hpp>
#include <vector>
class PlateLocator {
public:
PlateLocator(
int targetMaxWidth = 1024,
int targetMaxHeight = 720,
cv::Size blurKernelSize = cv::Size(5, 5),
double gammaValue = 0.2,
int radius = 15,
int cannyThreshold1 = 100,
int cannyThreshold2 = 200,
cv::Size morphKernel1Size = cv::Size(44, 14),
cv::Size morphKernel2Size = cv::Size(9, 4)
);
void preprocess(
const cv::Mat& origin,
cv::Mat& resized,
cv::Mat& preprocessed
) const;
std::vector<cv::Rect> locatePlates(
const cv::Mat& preprocessedImg,
float minAspectRatio = 2.1f,
float maxAspectRatio = 4.2f,
float targetAspectRatio = 3.14f,
float minRectAreaRatio = 0.005f,
float maxRectAreaRatio = 0.5f,
float minFillRatio = 0.5f,
int remain = 3
) const;
std::vector<cv::Mat> segmentCharacters(
const cv::Mat& plateImg
) const;
private:
int maxWidth, maxHeight;
cv::Size blurKernel;
double gamma;
int radius;
int canny1, canny2;
cv::Size kernel1Size, kernel2Size;
};
#endif // PLATE_LOCATOR_H
2 字符识别
2.1 数据集
使用的训练数据来源于 EasyPR 项目 提供的数据集。
cv::Mat charImgProcess(cv::Mat charImg, int imgeSize) {
cv::Mat resizedChar = resizeToMaxWidth(charImg, imgeSize);
cv::Mat paddedChar = padToSquareAvgMin(resizedChar, imgeSize);
cv::Mat stretched = stretchGrayPercentile(paddedChar, 0.05, 0.95);
cv::Mat binaryChar = binarizeByOtsu(stretched, 10);
cv::Mat cleaned = removeSmallComponents(binaryChar, 3);
return cleaned;
}
void processAndSave(const std::string& dataDir, const std::string& outDir, int imgeSize) {
for (const auto& classDir : std::filesystem::directory_iterator(dataDir)) {
if (!classDir.is_directory()) continue;
auto outClassDir = outDir + "/" + classDir.path().filename().string();
std::filesystem::create_directories(outClassDir);
for (const auto& imgPath : std::filesystem::directory_iterator(classDir)) {
cv::Mat img = cv::imread(imgPath.path().string(), cv::IMREAD_GRAYSCALE);
if (img.empty()) continue;
cv::Mat processedImg = charImgProcess(img, imgeSize);
cv::imwrite(outClassDir + "/" + imgPath.path().filename().string(), processedImg);
}
}
}
经过处理后形成二值化的分类数据集。
2.2 PCA + SVM分类
bool PcaSvmClassifier::train(const cv::Mat& samples, const cv::Mat& labels) {
cv::minMaxLoc(samples, &minVal, &maxVal);
if (maxVal - minVal < 1e-6) return false;
cv::Mat samplesNorm;
samples.convertTo(samplesNorm, CV_32F, 1.0 / (maxVal - minVal), -minVal / (maxVal - minVal));
pca = cv::PCA(samplesNorm, cv::Mat(), cv::PCA::DATA_AS_ROW, numComponents);
cv::Mat samplesPCA;
pca.project(samplesNorm, samplesPCA);
svm = cv::ml::SVM::create();
svm->setType(cv::ml::SVM::C_SVC);
svm->setKernel(cv::ml::SVM::RBF);
svm->setGamma(svmGamma);
svm->setC(svmC);
svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, epochs, 1e-6));
return svm->train(samplesPCA, cv::ml::ROW_SAMPLE, labels);
}
使用PCA将图像降低维度,否则维度太大SVM无法收敛,再使用SVM进行训练。
3 功能代码
图像识别、视频识别、摄像头识别本质上是对图片/帧进行处理。
3.1 帧处理
cv::Mat processFrame(const cv::Mat& src, int imgSize, PcaSvmClassifier& classifier) {
PlateLocator locator;
cv::Mat resized, preprocessed;
locator.preprocess(src, resized, preprocessed);
cv::Mat drawImg = resized.clone();
std::vector<cv::Rect> plates = locator.locatePlates(preprocessed);
if (plates.empty()) {
std::cout << "处理失败或未检测到车牌" << std::endl;
return drawImg;
}
cv::Mat plateImg = resized(plates[0]);
std::vector<cv::Mat> chars = locator.segmentCharacters(plateImg);
std::cout << "分割出字符数量:" << chars.size() << std::endl;
std::string plateText;
for (size_t i = 0; i < chars.size(); ++i) {
cv::Mat processedImg = charImgProcess(chars[i], imgSize);
int pred = classifier.predict(processedImg);
std::string label = classifier.idToLabel(pred);
plateText += label;
}
std::cout << "车牌号: " + plateText << std::endl;
cv::Rect plateRect = plates[0];
cv::rectangle(drawImg, plateRect, cv::Scalar(0, 255, 0), 2);
// 在车牌框上方标注识别出的车牌号
int baseline = 0;
int font = cv::FONT_HERSHEY_SIMPLEX;
double fontScale = 0.8;
int thickness = 2;
cv::Size textSize = cv::getTextSize(plateText, font, fontScale, thickness, &baseline);
cv::Point textOrg(plateRect.x, plateRect.y - 5); // 文字位置:车牌框上方
// 防止文字越界到图像外
if (textOrg.y < textSize.height) {
textOrg.y = plateRect.y + textSize.height + 5;
}
cv::putText(drawImg, plateText, textOrg, font, fontScale, cv::Scalar(0, 0, 255), thickness);
return drawImg;
}
3.2 功能函数
void recognizeImage(const std::string& imagePath, int imgSize, PcaSvmClassifier& classifier) {
cv::Mat img = cv::imread(imagePath);
if (img.empty()) {
std::cerr << "图像加载失败: " << imagePath << std::endl;
return;
}
cv::Mat drawFrame = processFrame(img, imgSize, classifier);
if (drawFrame.empty()) {
std::cout << "处理失败或未检测到车牌" << std::endl;
} else {
cv::imshow("车牌识别结果", drawFrame);
cv::waitKey(0);
}
}
void recognizeVideo(const std::string& videoPath, int imgSize, PcaSvmClassifier& classifier) {
cv::VideoCapture cap(videoPath);
if (!cap.isOpened()) {
std::cerr << "无法打开视频: " << videoPath << std::endl;
return;
}
cv::Mat frame;
while (cap.read(frame)) {
cv::Mat drawImg = processFrame(frame, imgSize, classifier);
cv::imshow("Video Frame", drawImg);
if (cv::waitKey(30) == 27) break;
}
}
void recognizeCamera(int cameraId, int imgSize, PcaSvmClassifier& classifier) {
cv::VideoCapture cap(cameraId);
if (!cap.isOpened()) {
std::cerr << "无法打开摄像头: " << cameraId << std::endl;
return;
}
cv::Mat frame;
while (cap.read(frame)) {
cv::Mat drawImg = processFrame(frame, imgSize, classifier);
cv::imshow("Camera", drawImg);
if (cv::waitKey(30) == 27) break;
}
}
&spm=1001.2101.3001.5002&articleId=148008349&d=1&t=3&u=38235c71ab894353b92b5c4ffd5a92da)
1135

被折叠的 条评论
为什么被折叠?



