车牌识别大体上需要经历过 Sobel 定位, 颜色定位, SVM 对定位来的候选车牌进行评测, 给出评分, 最后通过提取 HOG 特征按照训练模型进入 ANN 识别.
这一章节介绍 定位相关的逻辑代码, 其中定位用到 Sobel 定位(边缘检测定位), 颜色定位: 对应代码里的 CarSobelPlateLocation,CarColorPlateLocation; 两者定位后得到一些候选的图片, 把这些图片送去 SVM 进行评测, SVM 基于 HOG 提取边缘信息特征, HOG 类同之前处理纹理特征的 LBP, 项目代码在 Clion 上开发的, 源码地址前往车牌定位 https://github.com/yinxiucheng/OpencvCarRecgnize
Sobel 定位
CarSobelPlateLocation, 通过以下的一些步骤进行降噪:
高斯模糊
灰度化
边缘化
二值化
闭操作
高斯模糊
- // 预处理 : 去噪 让车牌区域更加突出
- Mat blur;
- //1, 高斯模糊(平滑) (1, 为了后续操作 2, 降噪 )
- GaussianBlur(src, blur, Size(5, 5), 0);
- //imshow("高斯模糊",blur);
灰度化
- Mat gray;
- //2, 灰度化 去掉颜色 因为它对于我们这里没用 降噪
- cvtColor(blur, gray, COLOR_BGR2GRAY);
- imshow("灰度", gray);
边缘化
- Mat sobel_16;
- //3, 边缘检测 让车牌更加突出 在调用时需要以 16 位来保存数据 在后续操作 以及显示的时候需要转回 8 位
- Sobel(gray, sobel_16, CV_16S, 1, 0);
- // 转为 8 位
- Mat sobel;
- convertScaleAbs(sobel_16, sobel);
- imshow("Sobel", sobel);
二值化
- //4. 二值化 黑白
- Mat shold;
- // 大律法 最大类间算法
- threshold(sobel, shold, 0, 255, THRESH_OTSU + THRESH_BINARY);
- imshow("二值", shold);
闭操作
- //5, 闭操作
- // 将相邻的白色区域扩大 连接成一个整体
- Mat close;
- Mat element = getStructuringElement(MORPH_RECT, Size(17, 3));
- morphologyEx(shold, close, MORPH_CLOSE, element);
- imshow("闭操作", close);
以上的操作是在处理降噪, 第六步初步赛选.
第六步: 最大面积, 最小面积. 宽高逼.
- //6, 查找轮廓
- // 获得初步筛选车牌轮廓 ================================================================
- // 轮廓检测
- vector<vector<Point>> contours;
- // 查找轮廓 提取最外层的轮廓 将结果变成点序列放入 集合
- findContours(close, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
- // 遍历
- vector<RotatedRect> vec_sobel_roi;
- for(vector<Point> point:contours){
- RotatedRect rotatedRect= minAreaRect(point);
- //rectangle(src, rotatedRect.boundingRect(), Scalar(255, 0, 255));
- // 进行初步的筛选 把完全不符合的轮廓给排除掉 ( 比如: 1x1,5x1000 )
- if (verifySizes(rotatedRect)) {
- vec_sobel_roi.push_back(rotatedRect);
- }
- }
初步赛选: 宽高比 float aspec, 把不符合的删除掉(1 * 1 的, 5* 1000 的等候选矩形)
- int CarPlateLocation::verifySizes(RotatedRect rotated_rect) {
- // 容错率
- float error = 0.75f;
- // 训练时候模型的宽高 136 * 32
- // 获得宽高比
- float aspect = float(136) / float(32);
- // 最小 最大面积 不符合的丢弃
- // 给个大概就行 随时调整
- // 尽量给大一些没关系, 这还是初步筛选.
- int min = 20 * aspect * 20;
- int max = 180 * aspect * 180;
- // 比例浮动 error 认为也满足
- // 最小宽, 高比
- float rmin = aspect - aspect * error;
- // 最大的宽高比
- float rmax = aspect + aspect * error;
- // 矩形的面积
- float area = rotated_rect.size.height * rotated_rect.size.width;
- // 矩形的比例
- float r = (float) rotated_rect.size.width / (float) rotated_rect.size.height;
- if ((area <min || area> max) || (r <rmin || r> rmax))
- return 0;
- return 1;
- }
把斜的图片转正: 仿射变换
- //1, 矫正前 2, 矫正后 3, 矩形的大小 4, 矩形中心点坐标 5, 角度
- void CarPlateLocation::rotation(Mat src, Mat &dst, Size rect_size,
- Point2f center, double angle) {
- // 获得旋转矩阵
- Mat rot_mat = getRotationMatrix2D(center, angle, 1);
- // 运用仿射变换
- Mat mat_rotated;
- // 矫正后 大小会不一样, 但是对角线肯定能容纳
- int max = sqrt(pow(src.rows, 2) + pow(src.cols, 2));
- // 仿射变换
- warpAffine(src, mat_rotated, rot_mat, Size(max, max),
- CV_INTER_CUBIC);
- imshow("旋转前", src);
- imshow("旋转", mat_rotated);
- // 截取 尽量把车牌多余的区域截取掉
- getRectSubPix(mat_rotated, Size(rect_size.width, rect_size.height), center, dst);
- imshow("截取", dst);
- mat_rotated.release();
- rot_mat.release();
- }
颜色定位
HSV 颜色模型
色调(H), 饱和度(S), 明度(V);
BGR 转成 HSV
cvtColor(src,hsv,COLOR_BGR2HSV);
色调 H
用角度度量, 取值范围为 0°~360°, 从红色开始按逆时针方向计算, 红色为 0°, 绿色为 120°, 蓝色为 240°. 它们的补色是: 黄色为 60°, 青色为 180°, 品红为 300°;
饱和度 S
饱和度 S 表示颜色接近光谱色的程度. 一种颜色, 可以看成是某种光谱色与白色混合的结果. 其中光谱色所占的比例愈大, 颜色接近光谱色的程度就愈高, 颜色的饱和度也就愈高. 饱和度高, 颜色则深而艳. 光谱色的白光成分为 0, 饱和度达到最高. 通常取值范围为 0%~100%, 值越大, 颜色越饱和.
明度 V
明度表示颜色明亮的程度, 对于光源色, 明度值与发光体的光亮度有关; 对于物体色, 此值和物体的透射比或反射比有关. 通常取值范围为 0%(黑)到 100%(白)
在 OpenCV 中 hsv 数据为 8UC 则取值分别为 0-180 0-255 0-255 , 即蓝色应该是 120
按照上面的表格找到蓝色区域 (100 ~ 124), 然后将 HSV 中的 H,S 转为 0, V 变为 255. 其它区域的 HSV 赋值为 0.
- //3 通道
- int chanles = hsv.channels();
- // 高
- int h = hsv.rows;
- // 宽数据长度
- int w = hsv.cols * 3;
- // 判断数据是否为一行存储的
- // 内存足够的话 mat 的数据是一块连续的内存进行存储
- if (hsv.isContinuous()) {
- w *= h;
- h = 1;
- }
- for (size_t i = 0; i <h; ++i) {
- // 第 i 行的数据 hsv 的数据 uchar = java byte
- uchar *p = hsv.ptr<uchar>(i);
- for (size_t j = 0; j <w; j += 3) {
- int h = int(p[j]);
- int s = int(p[j + 1]);
- int v = int(p[j + 2]);
- bool blue = false;
- // 蓝色
- if (h>= 100 && h <= 124 && s>= 43 && s <= 255 && v>= 46 && v <= 255) {
- blue = true;
- }
- if (blue){
- p[j] = 0;
- p[j + 1]=0;
- p[j + 2]=255;
- }else {
- //hsv 模型 h:0 红色 亮度和饱和度都是 0 , 也就变成了黑色
- p[j] = 0;
- p[j + 1] = 0;
- p[j + 2] = 0;
- }
- }
- }
得到下面的图:
接下来抽取亮度:
- // 把亮度数据抽出来
- // 把 h,s,v 分离出来
- vector<Mat> hsv_split;
- split(hsv, hsv_split);
然后跟 sobel 一样通过二值化, 大律法等操作
- // 整个图片 + 经过初步赛选的车牌 + 得到的候选车牌
- tortuosity(src, vec_sobel_roi, dst);
- for (Mat s: dst) {
- imshow("候选", s);
- waitKey();
- }
筛选出来一个集合:
把两个结合结合起来, 然后通过 SVM 进行评测, 因为不像人脸识别是没有现成的模型.
- vector<Mat> sobel_plates;
- //sobel 定位
- plateLocation->location(src, sobel_plates);
- // 颜色定位
- vector<Mat> color_plates;
- plateColorLocation->location(src, color_plates);
- vector<Mat> plates;
- // 把 sobel_plates 的内容 全部加入 plates 向量
- plates.insert(plates.end(),sobel_plates.begin(), sobel_plates.end());
- plates.insert(plates.end(), color_plates.begin(), color_plates.end());
SVM
简单来说, SVM 就是用于区分不同的类型(车牌, 非车牌).SVM 的训练数据既有特征又有标签, 通过训练, 让机器可以自己找到特征和标签之间的联系, 在面对只有特征没有标签的数据时, 可以判断出标签. 属于机器学习中的监督学习. 线性可分, 线性不可分, 不可分的时候用核函数来区分:
核函数: 用于将不同类型进行提维
人脸识别用的 LBP 提取特征, 这里采取 HOG 来提取特征.
SVM load 模型, 模型是同样是 xml 文件
- svm = SVM::load(svm_model);
- CarPlateRecgnize p("/Users/xiuchengyin/Documents/Tina-NDK/OpencvCarRecgnize/resource/HOG_SVM_DATA2.xml");
HOG 特征
局部归一化的梯度方向直方图, 是一种对图像局部重叠区域的密集型描述符, 它通过计算局部区域的梯度方向直方图来构成特征.
参数 1(检测窗口)的宽 - 参数 2(块大小)的宽 结果与参数 3(块滑动增量)的余数要为 0 高也一样
参数 4 是胞元大小, 参数 5 是梯度方向
HOGDescriptor hog(Size(128, 64), Size(16, 16), Size(8, 8), Size(8, 8), 3);
初始化 HOG 变量
- // 参数 1 的宽 - 参数 2 的宽 结果与参数 3 的余数为 0 高也一样
- svmHog = new HOGDescriptor(Size(128,64),Size(16,16),Size(8,8),Size(8,8),3);
检测窗口被分为:((128-16)/8+1)*((64-16)/8+1)=105 个块(Block);
一个 Block 有 4 个胞元(Cell);
一个 Cell 的 Hog 描述子向量的长度是 9;
统计梯度直方图特征, 就是将梯度方向 (0-360) 划分为 x 个区间, 将图像化为 16x16 的若干个窗口, 每个窗口又划分为 x 个 block, 每个 block 再化为 4 个 cell(8x8). 对每一个 cell, 算出每一像素点的梯度方向, 按梯度方向增加对应 bin 的值, 最终综合 N 个 cell 的梯度直方图组成特征.
简单来说, 车牌的边缘与内部文字组成的一组信息(在边缘和角点的梯度值是很大的, 边缘和角点包含了很多物体的形状信息),HOG 就是抽取这些信息组成一个直方图.
HOG : 梯度方向弱化光照的影响, 适合捕获轮廓.
LBP : 中心像素的 LBP 值反映了该像素周围区域的纹理信息.
SVM 依据 HOG 提取的特征将所给的候选图片进行评分, 选取最优的:
- string CarPlateRecgnize::plateRecgnize(Mat src) {
- vector<Mat> sobel_plates;
- //sobel 定位
- sobelPlateLocation->location(src, sobel_plates);
- // 颜色定位
- vector<Mat> color_plates;
- colorPlateLocation->location(src, color_plates);
- vector<Mat> plates;
- // 把 sobel_plates 的内容 全部加入 plates 向量
- plates.insert(plates.end(),sobel_plates.begin(), sobel_plates.end());
- plates.insert(plates.end(), color_plates.begin(), color_plates.end());
- int index = -1;
- float minScore = FLT_MAX; //float 的最大值
- // 使用 svm 进行 评测
- for (int i = 0;i<plates.size();++i)
- {
- Mat plate = plates[i];
- // 先灰度化, 再二值化, 灰度化只剩下一个通道
- Mat gray;
- cvtColor(plate, gray,COLOR_BGR2GRAY);
- // 二值化 必须是以单通道进行
- Mat shold;
- threshold(gray, shold, 0, 255, THRESH_OTSU + THRESH_BINARY);
- // 提取特征
- Mat features;
- getHogFeatures(svmHog, shold, features);
- //features 进行转化, 把数据保存成一行
- Mat samples = features.reshape(1,1);
- // 转化数据存储格式
- samples.convertTo(samples, CV_32FC1 );
- // 原始模式
- // svm: 直接告诉你这个数据是属于什么类型.
- // RAW_OUTPUT: 让 svm 给出一个评分
- // char name[100];
- // sprintf(name, "候选车牌 %d", i);
- // imshow(name, plate);
- float score = svm->predict(samples, noArray(), StatModel::Flags::RAW_OUTPUT);
- printf("评分:%f\n",score);
- if (score <minScore) {
- minScore = score;
- index = i;
- }
- gray.release();
- shold.release();
- features.release();
- samples.release();
- }
- Mat dst;
- if (index>= 0) {
- dst = plates[index].clone();
- }
- // imshow("车牌", dst);
- // waitKey();
- // 释放
- for (Mat p : plates) {
- p.release();
- }
- return string("123");
- }
svm 评分如下:
/Users/xiuchengyin/Documents/Tina-NDK/OpencvCarRecgnize/cmake-build-debug/OpencvCarRecgnize
评分:-1.224322
评分: 1.255759
评分: 1.831937
评分:-0.070820
评分: 1.525869
评分: 1.117042
测试最终取出来的就是我们的车牌选图了.
参考: https://github.com/liuruoze/EasyPR
http://www.cnblogs.com/subconscious/p/3979988.html
来源: https://juejin.im/post/5bed3ef2518825604e0e42aa