OpenCV车牌识别(C++)

该文章已生成可运行项目,

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;
    }
}

4 项目地址

[OpenCV License Plate Recognition]

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值