50、C++项目实战:车牌识别系统
车牌识别,听起来是不是挺高大上的?其实说白了,就是让计算机从一张图片里找到车牌,再把上面的字符读出来。我最早接触这个项目是在一个停车场管理系统里,当时甲方要求识别率必须达到95%以上,嗯,那段时间可没少加班。
今天我就带你手把手拆解这个项目。别怕,核心逻辑并不复杂,我们一步步来。
一、系统整体架构
一个完整的车牌识别系统,通常包含三个核心模块:图像预处理、车牌定位、字符识别。我个人习惯把流程画成图,这样思路更清晰。
你看,整个流程就像一条流水线。图片从左边进来,经过一道道工序,最后右边输出车牌号码。我在实际项目中踩过最大的坑就是——预处理没做好,后面全白搭。
二、图像预处理模块
预处理的目的就一个:让车牌区域更明显。你想想看,如果原图光线不好,或者车牌上有污渍,直接做定位肯定不准。
2.1 灰度化与去噪
彩色图像信息量太大,我们先转成灰度图。然后我会用高斯滤波去噪,这一步能平滑掉一些细小的干扰点。
// 灰度化 + 高斯滤波
cv::Mat preprocessImage(const cv::Mat& src) {
cv::Mat gray, blurred;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);
return blurred;
}
嗯,这里要注意:高斯核大小选5x5比较稳妥。我曾经试过3x3,结果噪点没滤干净,后面定位出了好多假车牌。
2.2 边缘检测
车牌区域通常有丰富的边缘信息——字符和背景的对比度很高。我习惯用Sobel算子做水平边缘检测,因为车牌字符大多是水平排列的。
// Sobel水平边缘检测
cv::Mat detectEdges(const cv::Mat& gray) {
cv::Mat grad_x;
cv::Sobel(gray, grad_x, CV_16S, 1, 0, 3);
cv::Mat abs_grad_x;
cv::convertScaleAbs(grad_x, abs_grad_x);
return abs_grad_x;
}
三、车牌定位模块
定位是整个系统的核心。说白了,就是从一堆边缘信息里,找到那个最像车牌的区域。
3.1 形态学操作
边缘检测后的图像有很多断点,我们需要用闭运算(先膨胀再腐蚀)把字符连成一片。这样车牌区域就会变成一个白色的矩形块。
// 形态学闭运算
cv::Mat morphClose(const cv::Mat& binary) {
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(17, 5));
cv::Mat closed;
cv::morphologyEx(binary, closed, cv::MORPH_CLOSE, kernel);
return closed;
}
为什么核的宽度比高度大?因为车牌是宽扁的矩形。这个尺寸我调了很久才找到最优解,你直接拿去用就行。
3.2 轮廓筛选
找到所有轮廓后,我们根据几个特征来筛选:
- 宽高比:标准车牌宽高比约3.14:1,允许一定误差
- 面积:太小或太大的都排除
- 矩形度:轮廓面积与最小外接矩形面积的比值
std::vector<cv::Rect> filterPlates(const std::vector<std::vector<cv::Point>>& contours) {
std::vector<cv::Rect> candidates;
for (const auto& contour : contours) {
cv::Rect rect = cv::boundingRect(contour);
double aspect = (double)rect.width / rect.height;
double area = rect.area();
// 宽高比在2.5~4.0之间,面积在合理范围
if (aspect > 2.5 && aspect < 4.0 && area > 1000 && area < 50000) {
candidates.push_back(rect);
}
}
return candidates;
}
四、字符分割与识别
定位到车牌后,我们需要把每个字符单独切出来,然后逐个识别。
4.1 字符分割
我用的方法是垂直投影法。把车牌图像垂直方向上的像素值累加,字符之间的间隙会出现波谷,从波谷位置切开就行。
std::vector<cv::Mat> splitChars(const cv::Mat& plate) {
cv::Mat gray, binary;
cv::cvtColor(plate, gray, cv::COLOR_BGR2GRAY);
cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
std::vector<cv::Mat> chars;
// 垂直投影,找波谷位置
std::vector<int> v_proj(plate.cols, 0);
for (int x = 0; x < plate.cols; ++x) {
for (int y = 0; y < plate.rows; ++y) {
if (binary.at<uchar>(y, x) == 0) v_proj[x]++;
}
}
// 根据投影值分割...(省略具体实现)
return chars;
}
4.2 OCR识别
字符识别我推荐用Tesseract OCR库,它支持中文车牌字符。不过要注意,Tesseract默认配置对车牌识别效果一般,需要做一些定制:
- 设置白名单:只识别数字和大写字母(中文省份简称单独处理)
- 调整页面分割模式(PSM)为单字符模式
- 对每个字符图像做归一化处理(统一大小、去边框)
// 使用Tesseract识别单个字符
std::string recognizeChar(const cv::Mat& charImg) {
tesseract::TessBaseAPI tess;
tess.Init(NULL, "eng", tesseract::OEM_LSTM_ONLY);
tess.SetVariable("tessedit_char_whitelist", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
tess.SetPageSegMode(tesseract::PSM_SINGLE_CHAR);
tess.SetImage(charImg.data, charImg.cols, charImg.rows, 1, charImg.step);
char* out = tess.GetUTF8Text();
std::string result(out);
delete[] out;
return result;
}
🔑 核心要点:识别率上不去的时候,别急着调OCR参数。先回头看看预处理和定位有没有做好。我80%的识别问题,最后都发现是前面步骤的锅。
五、性能优化与工程实践
项目做完了,但离上线还有距离。这里分享几个我在工程化过程中积累的经验:
| 问题 | 解决方案 | 效果 |
|---|---|---|
| 处理速度慢 | 缩小输入图像尺寸(保持宽高比) | 速度提升3倍,识别率几乎不变 |
| 夜间识别率低 | 加入自适应直方图均衡化(CLAHE) | 夜间识别率从60%提升到85% |
| 倾斜车牌 | 用Hough变换检测倾斜角,做仿射变换校正 | 倾斜车牌识别率提升至90% |
| 多线程处理 | 用OpenMP并行处理多路视频流 | 4路视频实时处理无压力 |
我记得有一次上线前测试,发现系统对蓝色车牌的识别率明显低于黄色车牌。查了半天,原来是预处理时蓝色通道的权重没调好。你看,细节决定成败。
六、完整代码框架
最后,给你一个完整的类框架,把上面所有模块串起来:
class LicensePlateRecognizer {
public:
LicensePlateRecognizer() {
// 初始化Tesseract
tess_.Init(NULL, "eng", tesseract::OEM_LSTM_ONLY);
tess_.SetVariable("tessedit_char_whitelist",
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
std::string recognize(const cv::Mat& src) {
// 1. 预处理
cv::Mat processed = preprocessImage(src);
// 2. 边缘检测
cv::Mat edges = detectEdges(processed);
// 3. 形态学操作
cv::Mat closed = morphClose(edges);
// 4. 找轮廓并筛选
std::vector<cv::Rect> plates = findPlates(closed);
if (plates.empty()) return "";
// 5. 取最大候选区域
cv::Rect plateRect = plates[0];
cv::Mat plateImg = src(plateRect);
// 6. 字符分割
std::vector<cv::Mat> chars = splitChars(plateImg);
// 7. 逐个识别
std::string result;
for (auto& c : chars) {
result += recognizeChar(c);
}
return result;
}
private:
tesseract::TessBaseAPI tess_;
// ... 各模块实现
};
这个框架可以直接拿去用。你只需要把各模块的函数实现补全,再调调参数,一个能跑的车牌识别系统就出来了。
做这个项目,我最大的体会是:计算机视觉项目,80%的时间花在图像预处理和参数调优上。别指望一个算法搞定所有问题,多试试不同的组合,找到最适合你场景的那一套。
好了,今天的分享就到这里。如果你在实现过程中遇到什么问题,欢迎交流。
公众号:蓝海资料掘金营,微信deep3321