关于HyperLPR
HyperLPR是一个使用深度学习针对对中文车牌识别的实现,与较为流行的开源的EasyPR相比,它的检测速度和鲁棒性和多场景的适应性都要好于目前开源的EasyPR,HyperLPR可以识别多种中文车牌包括白牌,新能源车牌,使馆车牌,教练车牌,武警车牌等。
Github地址:https://github.com/zeusees/HyperLPR
车牌的精定位
我们在训练完cascade分类器之后就会,haar adaboost cascade目标检测只能输出三个参数
- 目标的x
- 目标的y
- 目标的尺度
也就是说我们不像DPM和现代目标检测那样可以输出目标的长宽比。
所以我们光凭这三个参数还远远不够,在车牌有角度的倾斜的情况下。与传统目标检测不同的是,我们更需要确定车牌精确的位置。这里我尝试了很多种做法包括直接对ROI区域进行Regression的方法,在我少量的测试结果来看并不能取得良好的效果。最后拍脑袋想出了一种较为简单粗暴的精定位算法。
首先为了最大程度的保留车牌图像,我们对cascade目标检测后的区域进行扩展。我们在扩展后矩形中做文章。这里我想了一种另外的车牌精定位的方法。
精定位-确定上下边界
这是一种有使用条件的方法,前提是车牌的6个字符不能有完全的粘连。所谓的完全粘连就是指如下情况。
这种情况只有在车牌过爆的时候才会出现,其实这种情况发生的概率是相对很低的,而且很多商业产品都不能很好的处理情况。不过这种方法在我的实测下取得的非常好的鲁棒性,速度也特别快。实现也非常简单。首先我们要确定上下边界,我们把extend好的检测到的车牌区域提取出来。
我选取下面一张较为模糊的车牌作为例子来讲解这个算法的处理流程:
接着做下列流程的处理:
接着使用多个参数对这个区域进行多次自适应二值化。我们对opencv中adaptiveThreshold函数的k的参数从选择从-50变化到0。做15次二值化。
接着我们对每次二值化的图像进行连通域分析寻找满足字符长宽比的轮廓
我们将矩形框的两个点画出来
接着我们要对下面的点做直线拟合,在做直线拟合之前特别的,我们要特别的介绍一下随机抽样一致(RANSAC)算法。
在实际应用中获取到的数据,常常会包含有噪声数据,这些噪声数据会使对模型的构建造成干扰,我们称这样的噪声数据点为outliers,那些对于模型构建起积极作用的我们称它们为inliers,RANSAC做的一件事就是先随机的选取一些点,用这些点去获得一个模型(这个讲得有点玄,如果是在做直线拟合的话,这个所谓的模型其实就是斜率),然后用此模型去测试剩余的点,如果测试的数据点在误差允许的范围内,则将该数据点判为inlier,否则判为outlier。inliers的数目如果达到了某个设定的阈值,则说明此次选取的这些数据点集达到了可以接受的程度,否则继续前面的随机选取点集后所有的步骤,不断重复此过程,直到找到选取的这些数据点集达到了可以接受的程度为止,此时得到的模型便可认为是对数据点的最优模型构建。
上图中的红线是直接使用最小二乘的拟合结果,蓝线是使用RANSAC算法后的拟合结果。
由于在做连通域分析的时候,我们仅仅使用满足字符长宽比例的boundingbox作为判断条件,所以会带来一定的噪声。如下图又下角就存在着满足条件的错误点。所以RANSAC算法能帮助我们剔除这些噪声点。
我们使用RANSAC算法对上图中的点进行拟合。
这样就找到了上边界和下边界,下面我们只需要把这个区域crop出来就行了。
这样我们就做完了上下边界的精定位。
总的流程,可以用下图来概述。
我们用OpenCV实现了这个算法的C++和Python代码
Python
def fitLine_ransac(pts,zero_add = 0 ):
if len(pts)>=2:
[vx, vy, x, y] = cv2.fitLine(pts, cv2.DIST_HUBER, 0, 0.01, 0.01)
lefty = int((-x * vy / vx) + y)
righty = int(((136- x) * vy / vx) + y)
return lefty+30+zero_add,righty+30+zero_add
return 0,0
#精定位算法
def findContoursAndDrawBoundingBox(image_rgb):
line_upper = [];
line_lower = [];
line_experiment = []
grouped_rects = []
gray_image = cv2.cvtColor(image_rgb,cv2.COLOR_BGR2GRAY)
# for k in np.linspace(-1.5, -0.2,10):
for k in np.linspace(-50, 0, 16):
# thresh_niblack = threshold_niblack(gray_image, window_size=21, k=k)
# binary_niblack = gray_image > thresh_niblack
# binary_niblack = binary_niblack.astype(np.uint8) * 255
binary_niblack = cv2.adaptiveThreshold(gray_image,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,17,k)
# cv2.imshow("image1",binary_niblack)
# cv2.waitKey(0)
imagex, contours, hierarchy = cv2.findContours(binary_niblack.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
bdbox = cv2.boundingRect(contour)
if ((bdbox[3]/float(bdbox[2])>0.7 and bdbox[3]*bdbox[2]>100 and bdbox[3]*bdbox[2]<1200) or (bdbox[3]/float(bdbox[2])>3 and bdbox[3]*bdbox[2]<100)) :
# cv2.rectangle(rgb,(bdbox[0],bdbox[1]),(bdbox[0]+bdbox[2],bdbox[1]+bdbox[3]),(255,0,0),1)
line_upper.append([bdbox[0],bdbox[1]])
line_lower.append([bdbox[0]+bdbox[2],bdbox[1]+bdbox[3]])
line_experiment.append([bdbox[0],bdbox[1]])
line_experiment.append([bdbox[0]+bdbox[2],bdbox[1]+bdbox[3]])
# grouped_rects.append(bdbox)
rgb = cv2.copyMakeBorder(image_rgb,30,30,0,0,cv2.BORDER_REPLICATE)
leftyA, rightyA = fitLine_ransac(np.array(line_lower),3)
rows,cols = rgb.shape[:2]
# rgb = cv2.line(rgb, (cols - 1, rightyA), (0, leftyA), (0, 0, 255), 1,cv2.LINE_AA)
leftyB, rightyB = fitLine_ransac(np.array(line_upper),-3)
rows,cols = rgb.shape[:2]
# rgb = cv2.line(rgb, (cols - 1, rightyB), (0, leftyB), (0,255, 0), 1,cv2.LINE_AA)
pts_map1 = np.float32([[cols - 1, rightyA], [0, leftyA],[cols - 1, rightyB], [0, leftyB]])
pts_map2 = np.float32([[136,36],[0,36],[136,0],[0,0]])
mat = cv2.getPerspectiveTransform(pts_map1,pts_map2)
image = cv2.warpPerspective(rgb,mat,(136,36),flags=cv2.INTER_CUBIC)
image,M = deskew.fastDeskew(image)
# image,M = deskew.fastDeskew(image)
return image
C++
std::pair<int,int> FitLineRansac(std::vector<cv::Point> pts,int zeroadd = 0 )
{
std::pair<int,int> res;
if(pts.size()>2)
{
cv::Vec4f line;
cv::fitLine(pts,line,cv::DIST_HUBER,0,0.01,0.01);
float vx = line[0];
float vy = line[1];
float x = line[2];
float y = line[3];
int lefty = static_cast<int>((-x * vy / vx) + y);
int righty = static_cast<int>(((136- x) * vy / vx) + y);
res.first = lefty+PADDING_UP_DOWN+zeroadd;
res.second = righty+PADDING_UP_DOWN+zeroadd;
return res;
}
res.first = zeroadd;
res.second = zeroadd;
return res;
}
cv::Mat FineMapping::FineMappingVertical(cv::Mat InputProposal,int sliceNum,int upper,int lower,int windows_size){
cv::Mat PreInputProposal;
cv::Mat proposal;
cv::resize(InputProposal,PreInputProposal,cv::Size(FINEMAPPING_W,FINEMAPPING_H));
cv::cvtColor(PreInputProposal,proposal,cv::COLOR_BGR2GRAY);
float diff = static_cast<float>(upper-lower);
diff/=static_cast<float>(sliceNum-1);
cv::Mat binary_adaptive;
std::vector<cv::Point> line_upper;
std::vector<cv::Point> line_lower;
for(int i = 0 ; i < sliceNum ; i++)
{
std::vector<std::vector<cv::Point> > contours;
float k =lower + i*diff;
cv::adaptiveThreshold(proposal,binary_adaptive,255,cv::ADAPTIVE_THRESH_MEAN_C,cv::THRESH_BINARY,windows_size,k);
cv::Mat draw;
binary_adaptive.copyTo(draw);
cv::findContours(binary_adaptive,contours,cv::RETR_EXTERNAL,cv::CHAIN_APPROX_SIMPLE);
for(auto contour: contours)
{
cv::Rect bdbox =cv::boundingRect(contour);
float lwRatio = bdbox.height/static_cast<float>(bdbox.width);
int bdboxAera = bdbox.width*bdbox.height;
if (( lwRatio>0.7&&bdbox.width*bdbox.height>100 && bdboxAera<1200)
|| (lwRatio>3.0 && bdboxAera<100 && bdboxAera>10))
{
cv::Point p1(bdbox.x, bdbox.y);
cv::Point p2(bdbox.x + bdbox.width, bdbox.y + bdbox.height);
line_upper.push_back(p1);
line_lower.push_back(p2);
}
}
}
cv::Mat rgb;
cv::copyMakeBorder(PreInputProposal,rgb,30,30,0,0,cv::BORDER_REPLICATE);
std::pair<int,int> A;
std::pair<int,int> B;
A = FitLineRansac(line_upper,-3);
B = FitLineRansac(line_lower,3);
int leftyB = A.first;
int rightyB = A.second;
int leftyA = B.first;
int rightyA = B.second;
int cols = rgb.cols;
int rows = rgb.rows;
// pts_map1 = np.float32([[cols - 1, rightyA], [0, leftyA],[cols - 1, rightyB], [0, leftyB]])
// pts_map2 = np.float32([[136,36],[0,36],[136,0],[0,0]])
// mat = cv2.getPerspectiveTransform(pts_map1,pts_map2)
// image = cv2.warpPerspective(rgb,mat,(136,36),flags=cv2.INTER_CUBIC)
std::vector<cv::Point2f> corners(4);
corners[0] = cv::Point2f(cols - 1,rightyA);
corners[1] = cv::Point2f(0,leftyA);
corners[2] = cv::Point2f(cols - 1,rightyB);
corners[3] = cv::Point2f(0,leftyB);
std::vector<cv::Point2f> corners_trans(4);
corners_trans[0] = cv::Point2f(136,36);
corners_trans[1] = cv::Point2f(0,36);
corners_trans[2] = cv::Point2f(136,0);
corners_trans[3] = cv::Point2f(0,0);
cv::Mat transform = cv::getPerspectiveTransform(corners,corners_trans);
cv::Mat quad = cv::Mat::zeros(36, 136, CV_8UC3);
cv::warpPerspective(rgb, quad, transform, quad.size());
return quad;
}
精定位-确定左右边界
![Screen Shot 2017-10-13 at 1.49.08 PM](finemapping/Screen Shot 2017-10-13 at 1.49.08 PM.png)
在做完精定位之后,我们接着确定左右边界。我们知道存在车牌这类文字密集的地方,往往垂直边缘的聚集就特别强烈,由于在精定位后,车牌几乎是在一个尺度下,所以就无需关心多尺度的问题。
使用垂直边缘来进行车牌精定位
这种方法和基于边缘的车牌定位方法差不多,只不过不再需要考虑多尺度的问题。我们对上图做Sobel算子找到垂直边缘。
接着我们对垂直方向求和,画出投影直方图。
对直方图的左右两边做一些简单的处理即可获取左右边界。
这样我们就成功的精确定位了车牌。
数据分享和捐赠
车牌识别框架开发时使用的数据并不是很多,有意着可以为我们提供相关车牌数据。联系邮箱 455501914@qq.com。
如果您愿意支持我们持续对这个框架的开发,可以通过下面的链接来对我们捐赠。