OpenCV实战(一)——简单的车牌识别

    前言:

        最近一直在学习图像处理,想着找个实践的例子,这样让自己学习起来更加有激情,于是就找了车牌识别的例子,并把它写下来当作学习总结吧。

       近年来,汽车车牌识别(License Plate Recognition)已经越来越受到人们的重视。特别是在智能交通系统中,汽车牌照识别发挥了巨大的作用。汽车牌照的自动识别技术是把处理图像的方法与计算机的软件技术相连接在一起,以准确识别出车牌牌照的字符为目的,将识别出的数据传送至交通实时管理系统,以最终实现交通监管的功能。在车牌自动识别系统中,从汽车图像的获取到车牌字符处理是一个复杂的过程,主要分为四个阶段:图像获取、车牌定位、字符分割以及字符识别。目前关于车牌识别的算法有很多,本文将从简单的方法入手去尝试了解车牌识别的整个流程,最后的结果可能不尽人意,但也算是对图像处理入门吧。 

    摘要:

        本文主要是使用python环境下的OpenCV来处理图像。OpenCV(Open Source Computer Vision Library)是开源的计算机视觉和机器学习库,提供了C++、C以及python等接口,并支持Windows、Linux、Android、MacOS平台。OpenCV自1999年问世以来,就已经成为计算机视觉邻域学者和开发人员的首选工具。OpenCV目前有2.X和3.X两个大的版本,但是本文使用的代码都是OpenCV3.X的版本。

       本文第一部分是汽车车牌的定位,此时我们假设已经从视觉传感器中得到车辆的图像信息,这一节将展示从原始图像中去找到车牌的位置,期间也会介绍一些图像处理的知识。第二部分是对汽车牌照字符的分割,当对车牌进行定位以后我们需要将车牌分割出来,然后对车牌部分进行字符分割,将车牌分为七个字符用于后续的识别。第三部分就是对分离出来的单个字符进行识别,让机器告诉我们车牌的值。


一、汽车车牌定位

       我国的汽车牌照一般由七个字符和一个点组成,车牌字符的高度和宽度是固定的,分别为90mm和45mm,七个字符之间的距离也是固定的12mm,点分割符的直径是10mm,当然字符间的差异可能会引起字符间的距离变化。在民用车牌中,字符的排列位置遵循以下规律:第一个字符通常是我国各省区的简称,用汉字表示;第二个字符通常是发证机关的代码号,最后五个字符由英文字母和数字组合而成,字母是二十四个大写字母(除去I和O这两个字母)的组合,数字用"0-9"之间的数字表示。

       从图像处理角度看,汽车牌照有以下几个特征:第一个特征是是车牌的几何特征,即车牌形状统一为长宽高固定的矩形;第二个特征是车牌的灰度分布呈现出连续的波谷-波峰-波谷分布,这是因为我国车牌颜色单一,字符直线排列;第三个特征是车牌直方图呈现出双峰状的特点,即车牌直方图中可以看到双个波峰;第四个特征是车牌具有强边缘信息,这是因为车牌的字符相对集中在车牌的中心,而车牌边缘无字符,因此车牌的边缘信息感较强;第五个特征是车牌的字符颜色和车牌背景颜色对比鲜明。目前,我国国内的车牌大致可分为蓝底白字和黄底黑字,特殊用车采用白底黑字或黑底白字,有时辅以红色字体等。为了简化处理,本次学习中只考虑蓝底白字的车牌。

      我从网上找了一张别人用过的照片,如有侵权,请联系我,立删。为了使用相对路径,我把它放在了我的工作目录下并命名为car_test.jpg。接下来,我们读取这张照片,并把它显示出来:

import cv2
import numpy as np
import matplotlib.pyplot as plt

###############################
######  theme: 车牌识别   ######
######   author: 行歌     ######
######   time: 2018.3.23  ######
################################


def imread_photo(filename,flags = cv2.IMREAD_COLOR ):
    """
    该函数能够读取磁盘中的图片文件,默认以彩色图像的方式进行读取
    输入: filename 指的图像文件名(可以包括路径)
          flags用来表示按照什么方式读取图片,有以下选择(默认采用彩色图像的方式):
              IMREAD_COLOR 彩色图像
              IMREAD_GRAYSCALE 灰度图像
              IMREAD_ANYCOLOR 任意图像
    输出: 返回图片的通道矩阵
    """
    return  cv2.imread(filename,flags)



if __name__ == "__main__":
    img = imread_photo("car_test.jpg")
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    cv2.imshow('img',img)
    cv2.imshow('gray_img', gray_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

执行程序,其彩色图和灰度图显示如下:


1.1 图像数字化的基本知识

        在往下进行操作之前,首先给大家介绍以下图像数字化的基本知识。 针对计算机本地磁盘中的图像,单机鼠标右键,从弹出的快捷菜单中选择“属性”——>详细信息,会看到该图像的基本信息。现在我们以car_test.jpg为例,看到如下所示的基本信息:


      其实,大家应该发现,当我们使用Photoshop等图像编辑器打开图像,无限放大图像,用放大镜方式查看图像时,会看到很多彩色的小方格(针对彩色图而言)。其实这其中的每一个彩色的方格都是由三个数值量化的,或者说是由一个具有三个元素的向量量化的。灰度图像的每一个像素都是由一个数字量化的,而彩色图像的每一个像素都是由三个数字组成的向量量化的。最常用的是由R、G、B三个分量来量化的,RGB模型使用加性色彩混合以获知需要发出什么样的光来产生给定的色彩,源于使用阴极射线管(CRT)的彩色电视,具体色彩的值用三个元素的向量来表示,这三个元素的数值分别代表三种基色:Red、Green、Blue的亮度。假设每种基色的数值量化成个数,就如同8位灰度图像一样,灰度量化成个数。RGB图像的红、绿、蓝三个通道的图像都是一张8位图,因此颜色的总数为,如(0,0,0)代表黑色,(255,255,255)代表白色,(255,0,0)代表红色。

     因此计算机“看到”的图像或者图像在计算机内部表现形式是数字矩阵,简单来说,彩色图在每个像素上有三个值,而灰度图只有一个值,为了演示方便,我们看一下上面那张灰度图所对应的数字矩阵:

  

    显然灰度图跟彩色图不一样,灰度图每一个方格代表图像的一个像素。那么针对图像的基本信息,如何解读其中的宽度、高度和位深度呢?灰度图所对应的数字矩阵中,水平方向上的方格数对应其基本信息中的“宽度”,垂直方向上的方格数对应其基本信息中的“高度”,而计算机会将每一个像素数字化为一个数值,灰度图的”位深度“是8bit(彩色是24位),代表将每一个方格数字化为[0,255]之间的uchar类型数字,即用256个数字来衡量灰度的深浅,值越大,代表越亮,值越小,代表越灰,255代表白色,0代表黑色。因此,将上述彩色图灰度化以后,计算机看到的是一个266行(高度)368列(宽度)的二维数字矩阵。opencv中的cvtColor函数实现BGR彩色空间的图像向灰度图像和其他颜色空间转换,并且,在opencv中,实现将彩色图像(一个向量)转化为灰度像素(一个数值)的公式如下:


为什么要将彩色灰度处理,当然是为了后续的处理啦。既然是处理图像,我们当然要保证输入图像的大小不能太大啦!因此我们通过以下函数限制图像的最大宽度:

def resize_photo(imgArr,MAX_WIDTH = 1000):
    """
    这个函数的作用就是来调整图像的尺寸大小,当输入图像尺寸的宽度大于阈值(默认1000),我们会将图像按比例缩小
    输入: imgArr是输入的图像数字矩阵
    输出:  经过调整后的图像数字矩阵

    拓展:OpenCV自带的cv2.resize()函数可以实现放大与缩小,函数声明如下:
            cv2.resize(src, dsize[, dst[, fx[, fy[, interpolation]]]]) → dst
        其参数解释如下:
            src 输入图像矩阵
            dsize 二元元祖(宽,高),即输出图像的大小
            dst 输出图像矩阵
            fx 在水平方向上缩放比例,默认值为0
            fy 在垂直方向上缩放比例,默认值为0
            interpolation 插值法,如INTER_NEAREST,INTER_LINEAR,INTER_AREA,INTER_CUBIC,INTER_LANCZOS4等            
    """
    img = imgArr
    rows, cols= img.shape[:2]     #获取输入图像的高和宽
    if cols >  MAX_WIDTH:
        change_rate = MAX_WIDTH / cols
        img = cv2.resize(img ,( MAX_WIDTH ,int(rows * change_rate) ), interpolation = cv2.INTER_AREA)
    return img

 

 1.2 图像降噪

      每一副图像都包含某种程度的噪声,噪声可以理解为由一种或者多种原因造成的灰度值的随机变化,如由光子通量的随机性造成的噪声等,在大多数情况下,需要平滑技术(也常称为滤波或者降噪技术)进行抑制或者去除。比较常用的平滑处理算法包括基于二维离散卷积的高斯平滑、均值平滑、基于统计学方法的中值平滑,以及具备保持边缘作用的平滑算法的双边滤波、导向滤波等。

      在这里呢,我们采用基于二维离散卷积的高斯平滑对灰度图像进行降噪处理:处理后的效果如下所示:


      有小伙伴会好奇,高斯平滑的数学原理是怎样的呢?高斯平滑是通过做两个矩阵之间的二维离散卷积运算完成的,进行二维离散卷积运算之前,先讲述高斯平滑的核心——高斯卷积核的构建:

    假设构造宽(列数)为、高(行数)为的高斯卷积算子,其中均为奇数,参考点(anchor point)的位置在,构建步骤如下:

    第一步:计算高斯矩阵。

其中

 * 

r、c代表位置索引,其中,且均为整数。

     第二步:计算高斯矩阵的和。

                      

     第三步:高斯矩阵除以其本身的和,即归一化,得到的便是高斯卷积算子。


那么有了高斯卷积核,以后,我们就进行二维离散卷积的运算,假设为图像矩阵,为高斯卷积算子,其步骤如下:

    第一步:将逆时针翻转,得到

   第二步:沿着按照先行后列的顺序移动,每移动到一个固定位置,对应位置就相乘,然后求和。为了使得到的卷积结果和原图像的高、宽相等,所以通常在计算过程中给指定一个参考点(anchor point),然后将这个”参考点“循环移至图像矩阵的处,其中,接下来对应位置的元素逐个相乘,最后对所有的积进行求和作为输出图像矩阵在处的输出值。具体的运算过程大家可以自己了解。


1.3  形态学处理

    完成了高斯去噪以后,为了后面更加准确的提取车牌的轮廓,我们需要对图像进行形态学处理,在这里,我们对它进行开运算,处理后如下所示:


    

        那么什么是开运算呢? 先进行erode再进行dilate的过程就是开运算,它具有消除亮度较高的细小区域、在纤细点处分离物体,对于较大物体,可以在不明显改变其面积的情况下平滑其边界等作用。

       erode操作也就是腐蚀操作,类似于卷积,也是一种邻域运算,但计算的不是加权求和,而是对邻域中的像素点按灰度值进行排序,然后选择该组的最小值作为输出的灰度值。具体过程就是:

       假设输入图像为,高为、宽为,对于图像中的任意位置,取以为中心、高为、宽为的邻域,其中均为奇数,对邻域中的像素点灰度值进行排序,然后去最小值,作为输出图像位置处的灰度值。以以下图像矩阵为例:


 取以位置(1,1)为中心(也就是值205的位置)的3*3邻域

           

对邻域中的像素点灰度值按从小到大进行排序:     


可以看出26是该组灰度值的最小值,那么输出图像(1,1) =26,依此类推,会得到输出图像的所有像素点的灰度值。另外,对于腐蚀操作,这里的邻域不再单纯是矩形结构,也可以是椭圆形结构、十字形结构等,只是一般采用矩形结构。

       dilate操作就是膨胀操作,与腐蚀操作类似,膨胀是取每一个位置邻域内的最大值。既然是取邻域内的最大值,那么显然膨胀后的输出图像的总体亮度的平均值比起原图会有所上升,而图像中较亮物体的尺寸会变大;相反,较暗物体的尺寸会减小,甚至消失。


1.4 阈值分割

    完成初步的形态学处理以后,我们需要对图像进行阈值分割,我们在这里采用了Otsu阈值处理,处理后的效果如下所示:


      对图像进行数字处理时,我们需要把图像分成若干个特定的、具有独特性质的区域,每一个区域代表一个像素的集合,每一个集合又代表一个物体,而完成该过程的技术通常称为图像分割,它是从图像处理到图像分析的关键步骤。其实这个过程不难理解,就好比我们人类看景物一样,我们所看到的世界是由许许多多的物体组合而成的,就像教室是由人、桌子、书本、黑板等等组成。我们通过阈值处理,就是希望能够从背景中分离出我们的研究对象。

       那么什么是Ostu阈值处理呢?

       假设输入图像为,高为、宽为代表归一化的图像灰度直方图(灰度直方图是图像灰度级的函数,用来描述每个灰度级在图像矩阵中的像素个数或者占有率,归一化直方图就是用占有率表示),代表灰度值等于的像素点个数在图像中的所占的比率,其中.。该算法的详细步骤如下:

       第一步:计算灰度直方图的零阶累积矩(或称累加直方图)。

                                            

      第二步:计算灰度直方图的一阶累积矩。


     第三步:计算图像总体的灰度平均值mean,其实就是时的一阶累积矩,即


     第四步:计算每一个灰度级作为灰度级作为阈值时,前景区域的平均灰度、背景区域的平均灰度与整幅图像的平均灰度的方差。对方差的衡量采用以下度量:

                     

     第五步:找到使取值最大时的,这个值就是Ostu自动选取的阈值,即


    第六步:以作为阈值,进行全局阈值分割,即将灰度值大于阈值的像素设为白色(255),小于或者等于阈值的像素设为黑色;或者反过来,将大于阈值的像素设为黑色,小于或者等于阈值的像素设为白色,两者的区别只是呈现形式不同。

                        或者   

   

1.5 边缘检测

     经过Otsu阈值分割以后,我们要对图像进行边缘检测,我们这里采用的是Canny边缘检测(算法过于复杂,不在此详细介绍),处理后的结果如下:



    接下来再进行一次闭运算和开运算,填充白色物体内细小黑色空洞的区域并平滑其边界,处理后的效果如下:

       其实在这个时候,车牌的轮廓已经初步被选出来了,只是还有一些白色块在干扰。这个我们接下来会做相应的处理。其实每个轮廓我们可以看作是一系列的点(像素)构成的一个有序的点集,而现在我们要提取这些白色区域的轮廓。事实上,OpenCV就提供了这样一个函数,用来找到多个轮廓,如下所示:

  findContours(image, mode, method[, contours[, hierarchy[, offset]]]) -> image, contours, hierarchy

 上述我们所完成的所有操作的代码如下所示:

def predict(imageArr):
    """
    这个函数通过一系列的处理,找到可能是车牌的一些矩形区域
    输入: imageArr是原始图像的数字矩阵
    输出:gray_img_原始图像经过高斯平滑后的二值图
          contours是找到的多个轮廓
    """
    img_copy = imageArr.copy()
    gray_img = cv2.cvtColor(img_copy , cv2.COLOR_BGR2GRAY)
    gray_img_ = cv2.GaussianBlur(gray_img, (5,5), 0, 0, cv2.BORDER_DEFAULT)
    kernel = np.ones((23, 23), np.uint8)
    img_opening = cv2.morphologyEx(gray_img, cv2.MORPH_OPEN, kernel)
    img_opening = cv2.addWeighted(gray_img, 1, img_opening, -1, 0)
    # 找到图像边缘
    ret, img_thresh = cv2.threshold(img_opening, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    img_edge = cv2.Canny(img_thresh, 100, 200)
    # # 使用开运算和闭运算让图像边缘成为一个整体
    kernel = np.ones((10, 10), np.uint8)
    img_edge1 = cv2.morphologyEx(img_edge, cv2.MORPH_CLOSE, kernel)
    img_edge2 = cv2.morphologyEx(img_edge1, cv2.MORPH_OPEN, kernel)
    # # 查找图像边缘整体形成的矩形区域,可能有很多,车牌就在其中一个矩形区域中
    image, contours, hierarchy = cv2.findContours(img_edge2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    return gray_img_,contours

     

     现在我们已经有了轮廓,我们需要筛选出车牌所在的那个轮廓,由于车牌宽和高的比例是固定的,依据这个几何特征,我们进行筛选,然后用绿色的线条将得到的车牌框选出来,同时截取出车牌用来做下一步的字符分割。

def  chose_licence_plate(contours,Min_Area = 2000):
    """
    这个函数根据车牌的一些物理特征(面积等)对所得的矩形进行过滤
    输入:contours是一个包含多个轮廓的列表,其中列表中的每一个元素是一个N*1*2的三维数组
    输出:返回经过过滤后的轮廓集合
    
    拓展:
    (1) OpenCV自带的cv2.contourArea()函数可以实现计算点集(轮廓)所围区域的面积,函数声明如下:
            contourArea(contour[, oriented]) -> retval
        其中参数解释如下:
            contour代表输入点集,此点集形式是一个n*2的二维ndarray或者n*1*2的三维ndarray
            retval 表示点集(轮廓)所围区域的面积
    (2) OpenCV自带的cv2.minAreaRect()函数可以计算出点集的最小外包旋转矩形,函数声明如下:
             minAreaRect(points) -> retval      
        其中参数解释如下:
            points表示输入的点集,如果使用的是Opencv 2.X,则输入点集有两种形式:一是N*2的二维ndarray,其数据类型只能为 int32
                                    或者float32, 即每一行代表一个点;二是N*1*2的三维ndarray,其数据类型只能为int32或者float32
            retval是一个由三个元素组成的元组,依次代表旋转矩形的中心点坐标、尺寸和旋转角度(根据中心坐标、尺寸和旋转角度
                                    可以确定一个旋转矩形)
    (3) OpenCV自带的cv2.boxPoints()函数可以根据旋转矩形的中心的坐标、尺寸和旋转角度,计算出旋转矩形的四个顶点,函数声明如下:
             boxPoints(box[, points]) -> points
        其中参数解释如下:
            box是旋转矩形的三个属性值,通常用一个元组表示,如((3.0,5.0),(8.0,4.0),-60)
            points是返回的四个顶点,所返回的四个顶点是4行2列、数据类型为float32的ndarray,每一行代表一个顶点坐标              
    """
    temp_contours = []
    for contour in contours:
        if cv2.contourArea( contour ) > Min_Area:
            temp_contours.append(contour)
    car_plate = []
    for temp_contour in temp_contours:
        rect_tupple = cv2.minAreaRect( temp_contour )
        rect_width, rect_height = rect_tupple[1]
        if rect_width < rect_height:
            rect_width, rect_height = rect_height, rect_width
        aspect_ratio = rect_width / rect_height
        # 车牌正常情况下宽高比在2 - 5.5之间
        if aspect_ratio > 2 and aspect_ratio < 5.5:
            car_plate.append( temp_contour )
            rect_vertices = cv2.boxPoints( rect_tupple )
            rect_vertices = np.int0( rect_vertices )
    return  car_plate



def license_segment( car_plates ):
    """
    此函数根据得到的车牌定位,将车牌从原始图像中截取出来,并存在当前目录中。
    输入: car_plates是经过初步筛选之后的车牌轮廓的点集 
    输出:   "card_img.jpg"是车牌的存储名字
    """
    if len(car_plates)==1:
        for car_plate in car_plates:
            row_min,col_min = np.min(car_plate[:,0,:],axis=0)
            row_max, col_max = np.max(car_plate[:, 0, :], axis=0)
            cv2.rectangle(img, (row_min,col_min), (row_max, col_max), (0,255,0), 2)
            card_img = img[col_min:col_max,row_min:row_max,:]
            cv2.imshow("img", img)
        cv2.imwrite( "card_img.jpg", card_img)
        cv2.imshow("card_img.jpg", card_img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    return  "card_img.jpg"

得到的结果如下所示,我们顺利的完成了车牌的定位:




二、字符分割

       找到车牌的位置后,我们从它的二值图中截取出来,这个时候的车牌牌照的上下边界通常都是不规范的,我们需要边缘没用的部分,代码如下所示:

#根据设定的阈值和图片直方图,找出波峰,用于分隔字符
def find_waves(threshold, histogram):
	up_point = -1#上升点
	is_peak = False
	if histogram[0] > threshold:
		up_point = 0
		is_peak = True
	wave_peaks = []
	for i,x in enumerate(histogram):
		if is_peak and x < threshold:
			if i - up_point > 2:
				is_peak = False
				wave_peaks.append((up_point, i))
		elif not is_peak and x >= threshold:
			is_peak = True
			up_point = i
	if is_peak and up_point != -1 and i - up_point > 4:
		wave_peaks.append((up_point, i))
	return wave_peaks



def remove_plate_upanddown_border(card_img):
    """
    这个函数将截取到的车牌照片转化为灰度图,然后去除车牌的上下无用的边缘部分,确定上下边框
    输入: card_img是从原始图片中分割出的车牌照片
    输出: 在高度上缩小后的字符二值图片
    """
    plate_Arr = cv2.imread(card_img)
    plate_gray_Arr = cv2.cvtColor(plate_Arr, cv2.COLOR_BGR2GRAY)
    ret, plate_binary_img = cv2.threshold( plate_gray_Arr, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU )
    row_histogram = np.sum(plate_binary_img, axis=1)   #数组的每一行求和
    row_min = np.min( row_histogram )
    row_average = np.sum(row_histogram) / plate_binary_img.shape[0]
    row_threshold = (row_min + row_average) / 2
    wave_peaks = find_waves(row_threshold, row_histogram)
    #接下来挑选跨度最大的波峰
    wave_span = 0.0
    for wave_peak in wave_peaks:
        span = wave_peak[1]-wave_peak[0]
        if span > wave_span:
            wave_span = span
            selected_wave = wave_peak
    plate_binary_img = plate_binary_img[selected_wave[0]:selected_wave[1], :]
    #cv2.imshow("plate_binary_img", plate_binary_img)

    return  plate_binary_img

    ##################################################
    #测试用
    # print( row_histogram )
    # fig = plt.figure()
    # plt.hist( row_histogram )
    # plt.show()
    # 其中row_histogram是一个列表,列表当中的每一个元素是车牌二值图像每一行的灰度值之和,列表的长度等于二值图像的高度
    # 认为在高度方向,跨度最大的波峰为车牌区域
    # cv2.imshow("plate_gray_Arr", plate_binary_img[selected_wave[0]:selected_wave[1], :])
    ##################################################

执行程序后的结果如下所示:


      接下来的任务就是要把这其中的七个字符分割出来,因此后面还要去识别字符。这个时候有些人可能觉得很简单,其实不简单,因为机器并不知道从哪里下手去分割字符,所以我们需要计算出每个字符所在的位置,这样我们才能去分割。先展示代码和结果,再介绍分割的原理。如下所示:

#####################二分-K均值聚类算法############################

def distEclud (vecA, vecB):
    """
    计算两个坐标向量之间的街区距离 
    """
    return np.sum(abs(vecA - vecB))

def randCent( dataSet, k):
    n = dataSet.shape[1]  #列数
    centroids = np.zeros((k,n)) #用来保存k个类的质心
    for j in range(n):
        minJ = np.min(dataSet[:,j],axis = 0)
        rangeJ = float(np.max(dataSet[:,j])) - minJ
        for i in range(k):
            centroids[i:,j] = minJ + rangeJ * (i+1)/k
    return centroids

def kMeans (dataSet,k,distMeas = distEclud, createCent=randCent):
    m = dataSet.shape[0]
    clusterAssment = np.zeros((m,2))  #这个簇分配结果矩阵包含两列,一列记录簇索引值,第二列存储误差。这里的误差是指当前点到簇质心的街区距离
    centroids = createCent(dataSet,k)
    clusterChanged = True
    while clusterChanged:
        clusterChanged = False
        for i in range(m):
            minDist = np.inf
            minIndex = -1
            for j in range(k):
                distJI = distMeas(centroids[j,:],dataSet[i,:])
                if distJI < minDist:
                    minDist = distJI
                    minIndex = j
            if clusterAssment[i,0] != minIndex:
                clusterChanged = True
            clusterAssment[i,:] = minIndex,minDist ** 2
        for cent in range(k):
            ptsInClust = dataSet[ np.nonzero(clusterAssment[:,0]==cent)[0]]
            centroids[cent,:] = np.mean(ptsInClust, axis = 0)
    return centroids , clusterAssment



def biKmeans(dataSet,k,distMeas= distEclud):
    """
    这个函数首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。
    输入:dataSet是一个ndarray形式的输入数据集 
          k是用户指定的聚类后的簇的数目
         distMeas是距离计算函数
    输出:  centList是一个包含类质心的列表,其中有k个元素,每个元素是一个元组形式的质心坐标
            clusterAssment是一个数组,第一列对应输入数据集中的每一行样本属于哪个簇,第二列是该样本点与所属簇质心的距离
    """
    m = dataSet.shape[0]
    clusterAssment =np.zeros((m,2))
    centroid0 = np.mean(dataSet,axis=0).tolist()
    centList = []
    centList.append(centroid0)
    for j in range(m):
         clusterAssment[j,1] = distMeas(np.array(centroid0),dataSet[j,:])**2
    while len(centList) <k:       #小于K个簇时
        lowestSSE = np.inf
        for i in range(len(centList)):
            ptsInCurrCluster = dataSet[np.nonzero(clusterAssment[:,0] == i)[0],:]
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster,2,distMeas)
            sseSplit = np.sum(splitClustAss[:,1])
            sseNotSplit = np.sum( clusterAssment[np.nonzero(clusterAssment[:,0]!=i),1])
            if (sseSplit + sseNotSplit) < lowestSSE:         #如果满足,则保存本次划分
                bestCentTosplit = i
                bestNewCents = centroidMat
                bestClustAss = splitClustAss.copy()
                lowestSSE = sseSplit + sseNotSplit
        bestClustAss[np.nonzero(bestClustAss[:,0] ==1)[0],0] = len(centList)
        bestClustAss[np.nonzero(bestClustAss[:, 0] == 0)[0], 0] = bestCentTosplit
        centList[bestCentTosplit] = bestNewCents[0,:].tolist()
        centList.append( bestNewCents[1,:].tolist())
        clusterAssment[np.nonzero(clusterAssment[:,0] == bestCentTosplit)[0],:] = bestClustAss
    return centList, clusterAssment


def split_licensePlate_character(plate_binary_img):
    """
    此函数用来对车牌的二值图进行水平方向的切分,将字符分割出来
    输入: plate_gray_Arr是车牌的二值图,rows * cols的数组形式
    输出: character_list是由分割后的车牌单个字符图像二值图矩阵组成的列表
    """
    plate_binary_Arr = np.array ( plate_binary_img )
    row_list,col_list = np.nonzero (  plate_binary_Arr >= 255 )
    dataArr = np.column_stack(( col_list,row_list))   #dataArr的第一列是列索引,第二列是行索引,要注意
    centroids, clusterAssment = biKmeans(dataArr, 7, distMeas=distEclud)
    centroids_sorted = sorted(centroids, key=lambda centroid: centroid[0])
    split_list =[]
    for centroids_ in  centroids_sorted:
        i = centroids.index(centroids_)
        current_class = dataArr[np.nonzero(clusterAssment[:,0]==i)[0],:]
        x_min,y_min = np.min(current_class,axis =0 )
        x_max, y_max = np.max(current_class, axis=0)
        split_list.append([y_min, y_max,x_min,x_max])
    character_list = []
    for i in range(len(split_list)):
        single_character_Arr = plate_binary_img[split_list[i][0]: split_list[i][1], split_list[i][2]:split_list[i][3]]
        character_list.append( single_character_Arr )
        cv2.imshow('character'+str(i),single_character_Arr)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    return character_list              #character_list中保存着每个字符的二值图数据

    ############################
    #测试用
    #print(col_histogram )
    #fig = plt.figure()
    #plt.hist( col_histogram )
    #plt.show()
    ############################

运行结果如下,它成功的分割出来了,有木有!!!此时我的心情挺鸡动的!! 



这个过程当中,我选择了聚类算法。什么是聚类算法呢?

      聚类是一种机器学习方法,准确的说,它是一种无监督的学习。它将相似的对象归到同一个簇中,它有点像自动分类一样。在“无监督学习“中,训练样本的标记信息是未知的,目标是通过对无标记训练样本的学习来揭示数据的内在性质及规律,为进一步的数据分析提供基础。聚类试图将数据集中的样本划分为若干个通常是不相交的子集,每个子集称为一个”簇“。通过这样的划分,每个簇可能对应于一些潜在的概念(类别),但是要注意,这些概念对聚类算法而言事先是未知的,聚类过程仅能自动形成簇结构,簇所对应的概念语义需要我们来把握和命名。聚类既能作为一个单独过程,用于找寻数据内在的分布结构,也可作为分类等其他学习任务的前驱过程。例如,在一些商业应用中需要对新用户的类型进行判别,但定义”用户类型“对商家来说可能不太容易,此时可往往先对用户数据进行聚类。

      形象化的说,假定样本集包含个无标记样本,每个样本(逗号表示这是一个行向量,分号表是这是个列向量)是一个维特征向量,则聚类算法将样本集D划分为个不相交的簇,其中。相应地,我们用表示样本的“簇标记”,即。于是,聚类的结果可用包含个元素的簇标记向量表示。  

     本文的字符分割借助于二分K-均值算法,在介绍二分K-均值算法之前,先了解K-均值算法:

            给定样本集,”K均值“算法针对聚类所得簇划分最小化平方误差


       其中    是簇的均值向量。

    因此K-均值算法的流程(伪代码)如下:

*******************************************************************************************

输入:样本集、聚类簇数

过程: 

1:从中随机选择个样本作为初始均值向量

2: repeat

3:     令

4:     for  do

5:        计算样本与各均值向量的曼哈顿距离(又称街区距离):

6:        根据距离最近的均值向量确定的簇标记:

7:        将样本划入相应的簇:

8:        end for 

9:        for  do 

10:          计算新均值向量(其中对集合去绝对值符号是求该集合的元素个数): 

11:        if   then

12:              将当前均值向量更新为

13:        else

14:                保持当前均值向量不变

15:        end if

16:    end for 

17:until 当前均值向量均为更新

输出:簇划分

****************************************************************************************************    

      二分K-均值算法是在K-均值算法上面演变而来的,该算法首先将所有的点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续划分,选择哪一个簇进行划分取决于对其划分是否可以最大程度降低SSE的值。上述基于SSE的划分过程不断重复,直到得到用户指定的簇数目为止。在聚类算法中,通常采用误差来评价聚类质量。在本算法中,采用了一种用于度量聚类效果的指标——SSE(Sum of Squared Error,误差平方和)。这里的误差指的是样本点到簇质心的距离平方和。


三、字符识别

      现在我们已经有单个字符的二值图了,接下来的任务是要让机器能够告诉我们,这些字符什么?我们当然认识,可是傻逼的电脑它就蒙逼了。你给一张照片,它是不知道里面的内容代表着什么的。我采用支持向量机的方法去识别字符,由于复杂,我就不自己编SVM的算法程序,而是选择调用现成的程序的方法。在这里介绍一个强大的机器学习库——scikit-learn。

     python库的scikit-learn整合了多种机器学习算法,2007年,Cournapeu开始开发这个库,但直到2010年才发布它的第一个版本,这个库是Scipy工具集的一部分,该工具集包含多个为科学计算尤其是数据分析而开发的库,其中不少库被称作SciKits,库名scikit-learn的前半部分正是来源于此,而后半部分则是来自该库所面向的应用领域——机器学习,即Machine Learning。

    为了训练支持向量机我收集了13156张数字和字母的字符二值图。部分展示如下:




接下我需要完成以下步骤:

      1、依次读取每张字符二值图,得到它的数字矩阵(20行*20列的数组),然后转化为一个1*400的数组(即400列,每一列代表一个特征)。

    2、遍历每一个字符照片,得到13156个1*400的一维数组,把它们合并成为一个13156*400(即13156行400列)的数据集。

    3、A用10表示,Z用34表示,将数据集中每一行所对应的真实值作为类别标签,得到1*13156的类别数组。

    4、导入机器学习模型当中进行训练,最后导入预测数据。

为了程序读取文件,我将图片文件名保存在txt文件中,如下所示:


每一个txt文件里面都存放了该文件夹下的所有照片名,以便于编写程序逐行读取。因为没有省份的简称所对应的汉字数据集,所以这里只训练了数字和字母。程序如下:

############################机器学习识别字符##########################################
#这部分是支持向量机的代码
import numpy as np
import cv2
import sklearn


def load_data(filename_1):
    """
    这个函数用来加载数据集,其中filename_1是一个文件的绝对地址
    """
    with open(filename_1, 'r') as fr_1:
        temp_address = [row.strip() for row in fr_1.readlines()]
        # print(temp_address)
        # print(len(temp_address))
    middle_route = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K',
                    'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
    sample_number = 0  # 用来计算总的样本数
    dataArr = np.zeros((13156, 400))
    label_list = []
    for i in range(len(temp_address)):
        with open(r'C:\Users\Administrator\Desktop\python code\OpenCV\121\\' + temp_address[i], 'r') as fr_2:
            temp_address_2 = [row_1.strip() for row_1 in fr_2.readlines()]
        # print(temp_address_2)
        # sample_number += len(temp_address_2)
        for j in range(len(temp_address_2)):
            sample_number += 1
            # print(middle_route[i])
            # print(temp_address_2[j])
            temp_img = cv2.imread(
                'C:\\Users\Administrator\Desktop\python code\OpenCV\plate recognition\\train\chars2\chars2\\' +
                middle_route[i] + '\\' + temp_address_2[j], cv2.COLOR_BGR2GRAY)
            # print('C:\\Users\Administrator\Desktop\python code\OpenCV\plate recognition\train\chars2\chars2\\'+ middle_route[i]+ '\\' +temp_address_2[j] )
            # cv2.imshow("temp_img",temp_img)
            # cv2.waitKey(0)
            # cv2.destroyAllWindows()
            temp_img = temp_img.reshape(1, 400)
            dataArr[sample_number - 1, :] = temp_img
        label_list.extend([i] * len(temp_address_2))
    # print(label_list)
    # print(len(label_list))
    return dataArr, np.array(label_list)


def SVM_rocognition(dataArr, label_list):
    from sklearn.decomposition import PCA  # 从sklearn.decomposition 导入PCA
    estimator = PCA(n_components=20)  # 初始化一个可以将高维度特征向量(400维)压缩至20个维度的PCA
    new_dataArr = estimator.fit_transform(dataArr)
    new_testArr = estimator.fit_transform(testArr)

    import sklearn.svm
    svc = sklearn.svm.SVC()
    svc.fit(dataArr, label_list)  # 使用默认配置初始化SVM,对原始400维像素特征的训练数据进行建模,并在测试集上做出预测
    from sklearn.externals import joblib  # 通过joblib的dump可以将模型保存到本地,clf是训练的分类器
    joblib.dump(svc,"based_SVM_character_train_model.m")  # 保存训练好的模型,通过svc = joblib.load("based_SVM_character_train_model.m")调用


def SVM_rocognition_character( character_list ):
    character_Arr = np.zeros((len(character_list),400))
    #print(len(character_list))
    for i in range(len(character_list)):
        character_ = cv2.resize(character_list[i], (20, 20), interpolation=cv2.INTER_LINEAR)
        new_character_ = character_.reshape((1,400))[0]
        character_Arr[i,:] =  new_character_

    from sklearn.decomposition import PCA  # 从sklearn.decomposition 导入PCA
    estimator = PCA(n_components=20)  # 初始化一个可以将高维度特征向量(400维)压缩至20个维度的PCA
    character_Arr = estimator.fit_transform(character_Arr)
    ############
    filename_1 = r'C:\Users\Administrator\Desktop\python code\OpenCV\dizhi.txt'
    dataArr, label_list = load_data(filename_1)
    SVM_rocognition(dataArr, label_list)
    ##############
    from sklearn.externals import joblib
    clf = joblib.load("based_SVM_character_train_model.m")
    predict_result = clf.predict(character_Arr)
    middle_route = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', \
                    'G', 'H', 'J', 'K','L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
    print(predict_result.tolist())
    for k in range(len(predict_result.tolist())):
        print('%c'%middle_route[predict_result.tolist()[k]])



除去汉字,将剩余的六个字符输入进去预测,得到结果如下:

 


预测是正确的!!至此,车牌识别就完成了,但是这个只能做到对特定的图像进行定位和识别,泛化能力几乎为零。因此下次尝试用深度学习——神经网络来做这个。



参考文献:

[1]   周志华 《机器学习》

[2]   Peter Harrington 《机器学习实战》

[3]   张平 《OpenCV算法精讲》



本博文为作者原创,作品之著作权属本人所有,未经许可禁止转载。


已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页