车牌识别小结

一、汽车车牌定位

1.1 图像降噪(灰度处理)

import cv2
import numpy as np
import matplotlib.pyplot as plt
 
###############################
######  theme: 车牌识别   ######
######  author: CJ.Whale ######
######  time: 2021.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()

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

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

img

img

1.2 形态学处理

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

img

开运算:

​ 先进行erode再进行dilate的过程。

​ 具有消除亮度较高的细小区域、在纤细点处分离物体,对于较大物体,可以在不明显改变其面积的情况下平滑其边界等作用。

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

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

1.3 阈值分割

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

img

Ostu阈值处理:

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

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

img

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

img

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

img

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

img

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

img

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

1.4 边缘检测

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

img

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

img

​ 其实在这个时候,车牌的轮廓已经初步被选出来了,只是还有一些白色块在干扰。这个我们接下来会做相应的处理。其实每个轮廓我们可以看作是一系列的点(像素)构成的一个有序的点集,而现在我们要提取这些白色区域的轮廓。事实上,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"

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

img

img

二、字符分割

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

#根据设定的阈值和图片直方图,找出波峰,用于分隔字符
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], :])
    ##################################################
 

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

img

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

#####################二分-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()
    ############################

运行结果如下:(成功分割)

img

三、字符识别

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

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

img

img

img

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

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

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

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

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

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

img

每一个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]])
 
 
 

四、几个优质博客

OpenCV实战(一)——简单的车牌识别
基于OpenCV 的车牌识别
基于OpenCV3.0的车牌识别系统设计(二)--车牌提取
基于u-net,cv2以及cnn的中文车牌定位,矫正和端到端识别软件

[👆github源码👆](

posted @ 2021-07-08 21:51  淡水蓝鲸  阅读(540)  评论(0编辑  收藏  举报