OpenCV C++ 实现简单的车牌识别

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

测试车牌图像如下:

licensePlate1.png


开发环境:

  • 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 为进一步闭运算、开运算和膨胀的图像)

预处理过程中的各车牌图像如下所示:

OpenCV_APP_2.png


二、查找轮廓和筛选轮廓

这个时候,形态学操作后的图像(图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;
}

颜色检测后的图像如下所示:

OpenCV_APP_3.png


四、合并边缘和颜色的检测接管,检测出车牌区域

然后合并合并两种方法的检测结果,去除重复的检测框,检测出车牌区域矩形:

// 方法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;

在原图上绘制检测框,以确保我们已正确检测到车牌,效果图如下所示:

OpenCV_APP_4.png


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

OpenCV_APP_5.png


六、全部代码

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

posted @ 2025-10-16 09:24  fengMisaka  阅读(48)  评论(0)    收藏  举报