一、简介

通过颜色定位和Sobel算子定位可以计算出一个个的矩形区域,这些区域都是潜在车牌区域,但是在进行SVM判别是否是车牌之前,还需要进行一定的处理。主要是考虑到以下几个问题:

1、定位区域存在一定程度的倾斜,需要旋转到正常视角;

2、定位区域存在偏斜,除了进行旋转之后,还需要进行仿射变换;

3、定位出区域的大小不一致,需要对车牌的尺寸进行统一。

仿射变换(Affine Transformation 或 Affine Map),又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间的过程。它保持了二维图形的“平直性”(即:直线经过变换之后依然是直线)和“平行性”(即:二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。

一个任意的仿射变换都能表示为乘以一个矩阵(线性变换)接着再加上一个向量(平移)的形式。

那么, 我们能够用仿射变换来表示如下三种常见的变换形式:

  • 旋转,rotation (线性变换)
  • 平移,translation(向量加)
  • 缩放,scale(线性变换)

如果进行更深层次的理解,仿射变换代表的是两幅图之间的一种映射关系。这类变换可以用一个3*3的矩阵M来表示,其最后一行为(0,0,1)。该变换矩阵将原坐标为(x,y)变换为新坐标(x',y')。

对应的opencv函数如下:

1、getRotationMatrix2D

  Mat getRotationMatrix2D(Point2f center,  angle,  scale)
       已知旋转中心坐标(坐标原点为图像左上端点)、旋转角度(单位为度°,顺时针为负,逆时针为正)、放缩比例,返回旋转/放缩矩阵。

2、warpAffine

void warpAffine(InputArray src,

        OutputArray dst,

           InputArray M,

        Size dsize,

           int flags=INTER_LINEAR,

           int borderMode=BORDER_CONSTANT,

        const Scalar& borderValue=Scalar())

  根据getRotationMatrix2D得到的变换矩阵,计算变换后的图像。warpAffine 方法要求输入的参数是原始图像的左上点,右上点,左下点,以及输出图像的左上点,右上点,左下点。注意,必须保证这些点的对应顺序,否则仿射的效果跟你预想的不一样。因此 opencv 需要的是三个点对(共六个点)的坐标,然后建立一个映射关系,通过这个映射关系将原始图像的所有点映射到目标图像上。
如下图所示:

二、偏斜扭转

具体偏斜扭转的代码如下所示:

 int CPlateLocate::deskew(const Mat &src, const Mat &src_b,
vector<RotatedRect> &inRects,
vector<CPlate> &outPlates, bool useDeteleArea, Color color) {
Mat mat_debug;
src.copyTo(mat_debug); for (size_t i = ; i < inRects.size(); i++) {
RotatedRect roi_rect = inRects[i]; float r = (float) roi_rect.size.width / (float) roi_rect.size.height;
float roi_angle = roi_rect.angle; Size roi_rect_size = roi_rect.size;
if (r < ) {
roi_angle = + roi_angle;
swap(roi_rect_size.width, roi_rect_size.height);
} if (m_debug) {
Point2f rect_points[];
roi_rect.points(rect_points);
for (int j = ; j < ; j++)
line(mat_debug, rect_points[j], rect_points[(j + ) % ],
Scalar(, , ), , );
} // changed
// rotation = 90 - abs(roi_angle);
// rotation < m_angel;
// m_angle=60
if (roi_angle - m_angle < && roi_angle + m_angle > ) {
Rect_<float> safeBoundRect;
bool isFormRect = calcSafeRect(roi_rect, src, safeBoundRect);
if (!isFormRect) continue; Mat bound_mat = src(safeBoundRect);
Mat bound_mat_b = src_b(safeBoundRect); if () {
imshow("bound_mat_b", bound_mat_b);
waitKey();
destroyWindow("bound_mat_b");
} Point2f roi_ref_center = roi_rect.center - safeBoundRect.tl(); Mat deskew_mat;
if ((roi_angle - < && roi_angle + > ) || 90.0 == roi_angle ||
-90.0 == roi_angle) {
deskew_mat = bound_mat;
} else { Mat rotated_mat;
Mat rotated_mat_b; if (!rotation(bound_mat, rotated_mat, roi_rect_size, roi_ref_center,
roi_angle))
continue; if (!rotation(bound_mat_b, rotated_mat_b, roi_rect_size, roi_ref_center,
roi_angle))
continue; // we need affine for rotatioed image
double roi_slope = ; if (isdeflection(rotated_mat_b, roi_angle, roi_slope)) {
affine(rotated_mat, deskew_mat, roi_slope);
} else
deskew_mat = rotated_mat;
} Mat plate_mat;
plate_mat.create(HEIGHT, WIDTH, TYPE); // haitungaga add,affect 25% to full recognition.
if (useDeteleArea)
deleteNotArea(deskew_mat, color); if (deskew_mat.cols * 1.0 / deskew_mat.rows > 2.3 &&
deskew_mat.cols * 1.0 / deskew_mat.rows < ) { if (deskew_mat.cols >= WIDTH || deskew_mat.rows >= HEIGHT)
resize(deskew_mat, plate_mat, plate_mat.size(), , , INTER_AREA);
else
resize(deskew_mat, plate_mat, plate_mat.size(), , , INTER_CUBIC); CPlate plate;
plate.setPlatePos(roi_rect);
plate.setPlateMat(plate_mat);
if (color != UNKNOWN) plate.setPlateColor(color); outPlates.push_back(plate);
}
}
} return ;
}

下面我们对代码的主要逻辑过程做一个简单的梳理,对于定位后的车牌,首先进行旋转角度的判定,在-5°~5°范围内车牌直接输出,在-60°~ -5°和 5°~60°范围内车牌,首先进行偏斜程度的判定,如果偏斜程度不严重,旋转后输出,否则旋转角度后还需要仿射变换。

rotation()函数主要用于对倾斜的图片进行旋转, 具体的代码如下:

 bool CPlateLocate::rotation(Mat &in, Mat &out, const Size rect_size,
const Point2f center, const double angle) {
Mat in_large;
in_large.create(int(in.rows * 1.5), int(in.cols * 1.5), in.type()); float x = in_large.cols / - center.x > ? in_large.cols / - center.x : ;
float y = in_large.rows / - center.y > ? in_large.rows / - center.y : ; float width = x + in.cols < in_large.cols ? in.cols : in_large.cols - x;
float height = y + in.rows < in_large.rows ? in.rows : in_large.rows - y; /*assert(width == in.cols);
assert(height == in.rows);*/ if (width != in.cols || height != in.rows) return false; Mat imageRoi = in_large(Rect_<float>(x, y, width, height));
addWeighted(imageRoi, , in, , , imageRoi); Point2f center_diff(in.cols / .f, in.rows / .f);
Point2f new_center(in_large.cols / .f, in_large.rows / .f); Mat rot_mat = getRotationMatrix2D(new_center, angle, ); /*imshow("in_copy", in_large);
waitKey(0);*/ Mat mat_rotated;
warpAffine(in_large, mat_rotated, rot_mat, Size(in_large.cols, in_large.rows),
CV_INTER_CUBIC); /*imshow("mat_rotated", mat_rotated);
waitKey(0);*/ Mat img_crop;
getRectSubPix(mat_rotated, Size(rect_size.width, rect_size.height),
new_center, img_crop); out = img_crop; if () {
imshow("out", out);
waitKey();
destroyWindow("out");
} /*imshow("img_crop", img_crop);
waitKey(0);*/ return true;
}

在旋转的过程当中,遇到一个问题,就是旋转后的图像被截断了,如下图所示:

仔细分析下代码可以发现,getRotationMatrix2D()  函数主要根据旋转中心和角度进行旋转,当旋转角度还小时,一切都还好,但当角度变大时,明显我们看到的外接矩形的大小也在扩增。在这里,外接矩形被称为视框,也就是我需要旋转的正方形所需要的最小区域。随着旋转角度的变大,视框明显增大。 如下图所示:

EasyPR使用了一个极为简单的策略,它将原始图像与目标图像都进行了扩大化。首先新建一个尺寸为原始图像 1.5 倍的新图像,接着把原始图像映射到新图像上,于是我们得到了一个显示区域(视框)扩大化后的原始图像。显示区域扩大以后,那些在原图像中没有值的像素被置了一个初值。接着调用 warpAffine 函数,使用新图像的大小作为目标图像的大小。warpAffine 函数会将新图像旋转,并用目标图像尺寸的视框去显示它。于是我们得到了一个所有感兴趣区域都被完整显示的旋转后图像,这样,我们再使用 getRectSubPix()函数就可以获得想要的车牌区域了。

接下来就是分析截取后的车牌区域。车牌区域里的车牌分为正角度和偏斜角度两种。对于正的角度而言,可以看出车牌区域就是车牌,因此直接输出即可。而对于偏斜角度而言,车牌是平行四边形,与矩形的车牌区域不重合。如何判断一个图像中的图形是否是平行四边形?

一种简单的思路就是对图像二值化,然后根据二值化图像进行判断。为了判断二值化图像中白色的部分是平行四边形。一种简单的做法就是从图像中选择一些特定的行。计算在这个行中,第一个全为0的串的长度。从几何意义上来看, 这就是平行四边形斜边上某个点距离外接矩形的长度。假设我们选择的这些行位于二值化图像高度的 1/4,2/4,3/4 处的话,如果是白色图形是矩形的话, 这些串的大小应该是相等或者相差很小的,相反如果是平行四边形的话,那么这些串的大小应该不等,并 且呈现一个递增或递减的关系。通过这种不同,我们就可以判断车牌区域里的图形,究竟是矩形还是平行 四边形。

偏斜判断的另一个重要作用就是,计算平行四边形倾斜的斜率,这个斜率值用来在下面的仿射变换中 发挥作用。我们使用一个简单的公式去计算这个斜率,那就是利用上面判断过程中使用的串大小,假设二值化图像高度的 1/4,2/4,3/4 处对应的串的大小分别为 len1,len2,len3,车牌区域的高度为 Height。 一个计算斜率 slope 的计算公式就是:(len3-len1)/Height*2。

函数 isdeflection()  的主要功能是判断车牌偏斜的程度,并且计算偏斜的值。具体代码如下:

 bool CPlateLocate::isdeflection(const Mat &in, const double angle,
double &slope) {
int nRows = in.rows;
int nCols = in.cols; assert(in.channels() == ); int comp_index[];
int len[]; comp_index[] = nRows / ;
comp_index[] = nRows / * ;
comp_index[] = nRows / * ; const uchar* p; for (int i = ; i < ; i++) {
int index = comp_index[i];
p = in.ptr<uchar>(index); int j = ;
int value = ;
while ( == value && j < nCols) value = int(p[j++]); len[i] = j;
} // len[0]/len[1]/len[2] are used to calc the slope double maxlen = max(len[], len[]);
double minlen = min(len[], len[]);
double difflen = abs(len[] - len[]); double PI = 3.14159265; double g = tan(angle * PI / 180.0); if (maxlen - len[] > nCols / || len[] - minlen > nCols / ) { double slope_can_1 =
double(len[] - len[]) / double(comp_index[]);
double slope_can_2 = double(len[] - len[]) / double(comp_index[]);
double slope_can_3 = double(len[] - len[]) / double(comp_index[]);
slope = abs(slope_can_1 - g) <= abs(slope_can_2 - g) ? slope_can_1
: slope_can_2;
return true;
} else {
slope = ;
} return false;
}

我们已经实现了旋转功能,并且在旋转后的区域中截取了车牌区域,然后判断车牌区域中的图形是一 个平行四边形。下面要做的工作就是把平行四边形扭正成一个矩形。

函数 affine() 的主要功能是对图像进行根据偏斜角度,进行仿射变换。具体代码如下:

 void CPlateLocate::affine(const Mat &in, Mat &out, const double slope) {

   Point2f dstTri[];
Point2f plTri[]; float height = (float) in.rows;
float width = (float) in.cols;
float xiff = (float) abs(slope) * height; if (slope > ) { // right, new position is xiff/2 plTri[] = Point2f(, );
plTri[] = Point2f(width - xiff - , );
plTri[] = Point2f( + xiff, height - ); dstTri[] = Point2f(xiff / , );
dstTri[] = Point2f(width - - xiff / , );
dstTri[] = Point2f(xiff / , height - );
} else { // left, new position is -xiff/2 plTri[] = Point2f( + xiff, );
plTri[] = Point2f(width - , );
plTri[] = Point2f(, height - ); dstTri[] = Point2f(xiff / , );
dstTri[] = Point2f(width - - xiff + xiff / , );
dstTri[] = Point2f(xiff / , height - );
} Mat warp_mat = getAffineTransform(plTri, dstTri); Mat affine_mat;
affine_mat.create((int) height, (int) width, TYPE); if (in.rows > HEIGHT || in.cols > WIDTH) warpAffine(in, affine_mat, warp_mat, affine_mat.size(),
CV_INTER_AREA);
else
warpAffine(in, affine_mat, warp_mat, affine_mat.size(), CV_INTER_CUBIC); out = affine_mat;
}

最后使用 resize 函数将车牌区域统一化为 EasyPR 的车牌大小,大小为136*36。

EasyPR源码剖析(5):车牌定位之偏斜扭转的更多相关文章

  1. EasyPR源码剖析(4):车牌定位之Sobel算子定位

    一.简介 sobel算子主要是用于获得数字图像的一阶梯度,常见的应用是边缘检测. Ⅰ.水平变化: 将 I 与一个奇数大小的内核进行卷积.比如,当内核大小为3时, 的计算结果为: Ⅱ.垂直变化: 将: ...

  2. EasyPR源码剖析(3):车牌定位之颜色定位

    一.简介 对车牌颜色进行识别,可能大部分人首先想到的是RGB模型, 但是此处RGB模型有一定的局限性,譬如蓝色,其值是255,还需要另外两个分量都为0,不然很有可能你得到的值是白色.黄色更麻烦,它是由 ...

  3. EasyPR源码剖析(2):车牌定位

    上一篇主要介绍了车牌识别的整体框架和流程,车牌识别主要划分为了两个过程:即车牌检测和字符识别,而车牌识别的核心环节就是这一节主要介绍的车牌定位,即 Plate Locate.车牌定位主要是将图片中有可 ...

  4. EasyPR源码剖析(7):车牌判断之SVM

    前面的文章中我们主要介绍了车牌定位的相关技术,但是定位出来的相关区域可能并非是真实的车牌区域,EasyPR通过SVM支持向量机,一种机器学习算法来判定截取的图块是否是真的“车牌”,本节主要对相关的技术 ...

  5. EasyPR源码剖析(1):概述

    EasyPR(Easy to do Plate Recognition)是本人在opencv学习过程中接触的一个开源的中文车牌识别系统,项目Git地址为https://github.com/liuru ...

  6. EasyPR源码剖析(6):车牌判断之LBP特征

    一.LBP特征 LBP指局部二值模式,英文全称:Local Binary Pattern,是一种用来描述图像局部特征的算子,LBP特征具有灰度不变性和旋转不变性等显著优点. 原始的LBP算子定义在像素 ...

  7. EasyPR源码剖析(8):字符分割

    通过前面的学习,我们已经可以从图像中定位出车牌区域,并且通过SVM模型删除“虚假”车牌,下面我们需要对车牌检测步骤中获取到的车牌图像,进行光学字符识别(OCR),在进行光学字符识别之前,需要对车牌图块 ...

  8. EasyPR源码剖析(9):字符识别

    在上一篇文章的介绍中,我们已经通过相应的字符分割方法,将车牌区域进行分割,得到7个分割字符图块,接下来要做的就是将字符图块放入训练好的神经网络模型,通过模型来预测每个图块所表示的具体字符.神经网络的介 ...

  9. HashMap源码剖析

    HashMap源码剖析 无论是在平时的练习还是项目当中,HashMap用的是非常的广,真可谓无处不在.平时用的时候只知道HashMap是用来存储键值对的,却不知道它的底层是如何实现的. 一.HashM ...

随机推荐

  1. mint linux 18.3 遇到“已安装的 post-installation 脚本 返回了错误号 127 ”问题的解决

    From https://blog.csdn.net/ropai/article/details/27171687 ubuntu 14.04遇到“已安装的 post-installation 脚本 返 ...

  2. SQL SERVER 死锁

    sp_lock 查看锁表名称 select request_session_id spid,OBJECT_NAME(resource_associated_entity_id) tableNamefr ...

  3. CSS设计中的错误大整理!

    如果有人发明时间机器,那应该将这些错误纠正,不然可把前端程序猿们给还惨了.大家一起看看都有哪些CSS规则应该完善. (CSS 代码) white-space: nowrap 应该 white-spac ...

  4. thinkphp51 重定向 redirect()

    thinkphp51 重定向 跨域 redirect() 可以重新定义新的路由,是fetch() 无法做到的.

  5. 【活动发布】捷微H5-微信新年砍价活动,开源发布了

    活动名称: 微信新年砍价 适用项目: 捷微H5活动平台源码下载:http://yun.jeecg.org/pmall/detail/924D63E4F73B4387B75DE9B8545136C5 视 ...

  6. 开源小程序CMS网站, JeeWx-App-CMS 1.1 版本升级发布,持续更新!

    JeeWx-App-CMS开源小程序CMS网站,持续更新ing~ JeeWx-App-CMS 是jeewx开发的小程序网站开源项目,基于小程序wepy语言,具备cms网站的基本功能,能够打造简单易用的 ...

  7. C# 拖拽事件

    实现一个textBox像另一个TextBox拖拽数据. 一个控件想要被拖入数据的前提是AllowDrop属性为true. 所以,首先需要将被拖入数据的textBox的AllowDrop属性设置为Tru ...

  8. chrome浏览器 新建 标签 页面 跳转到主页(或跳转到谷歌)

    我的浏览器是跳转到自己设置的主页.我在贴吧看到的以下方式,然后就可以了: 地址栏输入 chrome://flags 搜索 Enable doodles on the local NTP 这一项改为di ...

  9. docker php容器中简单添加seaslog拓展

    最近有个项目用到了seaslog,因为之前调试php的容器已经搭好了,不想再通过dockerfile重新搭建了,搜了半天没有东西可以装,就仿照着安装redis拓展操作了一顿 1.wget http:/ ...

  10. Excle批量导入到mysql调查

    1.使用navicat Premium 导入时候提示打不开.问题的链接如下: https://blog.csdn.net/wsyzxss/article/details/77864313 2.只能使用 ...