对于车牌分割我会从以下几个知识点来论述,最后对车牌分割的代码进行详解
1.图像边缘
图像边缘,即表示图像中一个区域的终结和另一个区域的开始,图像中相邻区域之间的像素集合构成了图像的边缘。所以,图像边缘可以理解为图像灰度发生空间突变的像素的集合。图像边缘有两个要素,即:方向和幅度。沿着边缘走向的像素值变化比较平缓;而沿着垂直于边缘的走向,像素值则变化得比较大。因此,根据这一变化特点,通常会采用一阶和二阶导数来描述和检测边缘。
综上,图像中的边缘检测可以通过对灰度值求导数来确定,而导数可以通过微分算子计算来实现。在数字图像处理中,通常是利用差分计算来近似代替微分运算。如下图所示。
2.Canny算子
Canny边缘检测算法的原理:
- 首先要对原始图像进行灰度化
- 对图像进行高斯滤波:这个应该都很清楚了,就是采用高斯核函数,根据其sigma 的不同对图像进行不同程度的模糊处理,虽然叫模糊处理,实质是滤除了高频的噪声。
- 根据Canny算法所采用的卷积算子,对灰度图像求梯度。
卷积算子为:
- 对梯度幅值进行非极大值抑制。
通过上面的计算,就已经确定了每个像素的梯度和幅值,该点的像素梯度方向会根据邻域像素,计算8个方向的梯度直方图,由此确定该点像素的主方向。通常像素点局部最大值就是边缘点,垂直于该梯度方向,下一个点(可能点 s 和 r)为下一个边缘点(在两个像素点之间,可以通过邻域两个像素点进行插值来计算)
3.形态学(膨胀和腐蚀)
膨胀就是求局部最大值的操作。按数学方面来说,膨胀或者腐蚀操作就是将图像(或图像的一部分区域,我们称之为A)与核(我们称之为B)进行卷积,即计算核B覆盖的区域的像素点的最大值,并把这个最大值赋值给参考点指定的像素。这样就会使图像中的高亮区域逐渐增长。(膨胀的处理效果)如下图所示。
腐蚀就是求局部最小值的操作。(腐蚀的处理效果)如下图所示。
数学公式对比:
4.开运算和闭运算
因为没有深入去了解基于数学基础是怎么实现的,这里只赘述了开运算和闭运算对图像有什么作用,以及试验的结果:
开运算(Opening Operation)其实就是先腐蚀后膨胀的过程:开运算可以用来消除小物体、在纤细点处分离物体、平滑较大物体的边界的同时并不明显改变其面积,如下图所示。
闭运算(Closing Operation)是先膨胀后腐蚀的过程:闭运算能够排除小型黑洞。如下图所示。
5.车牌分割完整代码
import cv2
import numpy as np
from PIL import Image
import os.path
from skimage import io,data
def stretch(img):
#图像拉伸函数
maxi = float(img.max())
#print(maxi) #255.0
mini = float(img.min())
#print(mini) 0.0
#[0]图像的高 [1]图像的宽 [2]图像的通道数
for i in range(img.shape[0]):
for j in range(img.shape[1]):
img[i,j] = (255/(maxi-mini)*img[i,j]-(255*mini)/(maxi-mini))
#print(img.shape[0]) 266
#print(img.shape[1]) 400
return img
def dobinaryzation(img):
#二值化处理函数
maxi = float(img.max())
#print(img.max()) #205
mini = float(img.min())
#print(img.min()) 0
x = maxi-((maxi-mini)/2)
#print(x) 102.5
#二值化,返回阈值ret和二值化操作后的图像thresh
ret,thresh = cv2.threshold(img,x,255,cv2.THRESH_BINARY)
#返回二值化后的黑白图像
return thresh
def find_rectangle(contour):
#寻找矩形轮廓
y,x = [],[]
for p in contour:
y.append(p[0][0])
x.append(p[0][1])
return [min(y),min(x),max(y),max(x)]
def locate_license(img,afterimg):
#定位车牌号
img,contours,hierarchy = cv2.findContours(img,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
#找出最大的三个区域
n=len(contours)
print(n) #16
block = []
for c in contours:
#找出轮廓的左上点和右下点,由此计算它的面积和长度比
r = find_rectangle(c)
a = (r[2]-r[0])*(r[3]-r[1])#面积
s = (r[2]-r[0])/(r[3]-r[1])#长度比
block.append([r,a,s])
#选出面积最大的三个区域
block = sorted(block,key=lambda b:b[1])[-3:]
# print(len(block)) 16
#使用颜色识别判断找出最像车牌的区域
maxweight,maxindex = 0,-1
for i in range(len(block)):
b = afterimg[block[i][0][1]:block[i][0][3],block[i][0][0]:block[i][0][2]]
#bgr转hsv
hsv = cv2.cvtColor(b,cv2.COLOR_BGR2HSV)
#蓝色车牌的范围
lower = np.array([100,50,50])
upper = np.array([140,255,255])
#根据阈值构建掩摸
mask = cv2.inRange(hsv,lower,upper)
#print(mask)
#统计权值
w1 = 0
for m in mask:
w1 += m/255
w2 = 0
#选出权值最大的区域
for n in w1:
w2 += n
if w2 > maxweight:
maxindex = i #16个矩形框中的哪一个
maxweight = w2
return block[maxindex][0]
def find_license(img):
#预处理函数
m = 400*img.shape[0]/img.shape[1]
print(m)
#print(img.shape[0]) 600 高
#print(img.shape[1]) 900 宽
#压缩图像
img = cv2.resize(img,(400,int(m)),interpolation=cv2.INTER_CUBIC)
#print(img.shape[0]) 266 img.shape[1] 400 (266,400)
#bgr转换成灰度图像
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#灰度拉伸
stretchedimg = stretch(gray_img)
#开运算,用来去除噪声
r=16
h=w=r*2+1
kernel =np.zeros((h,w),np.uint8)
cv2.circle(kernel,(r,r),r,1,-1)
#开运算
openingimg = cv2.morphologyEx(stretchedimg,cv2.MORPH_OPEN,kernel)
#获取差分图像,两幅图像做差
strtimg = cv2.absdiff(stretchedimg,openingimg)
#图像二值化
binaryimg = dobinaryzation(strtimg)
#canny边缘检测
canny = cv2.Canny(binaryimg,binaryimg.shape[0],binaryimg.shape[1])
#消除小的区域,保留大的区域,从而定位车牌
#进项闭运算
kernel = np.ones((5,19),np.uint8)
closingimg = cv2.morphologyEx(canny,cv2.MORPH_CLOSE,kernel)
#进行开运算
openingimg = cv2.morphologyEx(closingimg, cv2.MORPH_OPEN, kernel)
#再次进行开运算
kernel = np.ones((11,5),np.uint8)
openingimg = cv2.morphologyEx(openingimg,cv2.MORPH_OPEN,kernel)
#消除小区域,定位车牌位置
rect = locate_license(openingimg,img)
return rect,img
def cut_license(afterimg,rect):
#图像分割函数
#转换为宽度和高度
rect[2]=rect[2]-rect[0]
rect[3]=rect[3]-rect[1]
rect_copy = tuple(rect.copy())
rect=[0,0,0,0]
#创建掩摸
mask = np.zeros(afterimg.shape[:2],np.uint8)
#创建背景模型,大小只能为13*5 行数只能为1 单通道浮点数
bgdmodel = np.zeros((1,65),np.float64)
#创建前景模型
fgdmodel = np.zeros((1,65),np.float64)
#分割图像
cv2.grabCut(afterimg,mask,rect_copy,bgdmodel,fgdmodel,5,cv2.GC_INIT_WITH_RECT)
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
#找到所有确定的背景或可能的背景像素,并将它们设置为0 ,所有其他像素应该标记为1(即:,前景)
img_show = afterimg*mask2[:,:,np.newaxis]
return img_show
def deal_license(licenseimg):
#车牌图片二值化
#车牌变为灰度图像
gray_img = cv2.cvtColor(licenseimg,cv2.COLOR_BGR2GRAY)
#均值滤波,去除噪声
kernel = np.ones((3,3),np.float32)/9
gray_img = cv2.filter2D(gray_img,-1,kernel)
#二值化处理
ret,thresh = cv2.threshold(gray_img,120,255,cv2.THRESH_BINARY)
return thresh
if __name__ == '__main__':
img = cv2.imread('car3.jpg',cv2.IMREAD_COLOR)
#预处理图像
#img = stretch(img)
#cv2.imshow('img',img)
rect,afterimg = find_license(img)
#框处车牌号
cv2.rectangle(afterimg,(rect[0],rect[1]),(rect[2],rect[3]),(0,255,0),2)
cv2.imshow('afterimg',afterimg)
#分割车牌与背景
cutimg = cut_license(afterimg,rect)
cv2.imshow('cutimg',cutimg)
#二值化生成黑白图
thresh = deal_license(cutimg)
cv2.imshow('thresh',thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()
6.实验结果
以上是车牌区域分割的完整代码,分割效果如下:
从分割结果来看,还是不错的,那么接下来我们就仔细的剖析一下代码(说明:图片完全是我自己整理的笔记然后截图过来的,因为这是老师给我们布置的一次小实验,我是Word整理好的。图片解析的顺序也是根据函数的顺序依次进行排列的,有想要PDF或者Word的小伙伴可以直接私信我):
7.代码解析
1.预处理 图像拉伸
2.图像预处理 二值化处理
3.寻找矩形轮廓
4.对图像拉伸(涉及函数的调用)、开运算和闭运算、以及对图像Canny边缘检测
5.图像分割函数
6.分割得到的车牌区域二值化
到这里车牌分割的代码就解读完了,其实车牌分割还可以应用很多种方法,一般的车牌区域都是长方形区域,就可以根据查找车身的矩形框来做;还可以根据车牌的颜色来分割,一般的车牌都是蓝白和黄白,完全可以根据颜色实现分割。
7.实验不足与展望
首先对于车牌区域模糊的图像,处理的效果并不是很理想,还有对倾斜车身的这样的情况,车牌分割的效果也不是很理想,我看了很多的文章,有很多人用到了仿射变换的方法,这个我没有尝试。另外就是程序中,对图像进行开运算和闭运算,这个开和闭的顺序朋友们可以自己去尝试,也可以试试两次开运算一次闭运算,反之两次闭运算一次开运算。总的来说,需要改进的地方还很多~~
2022-4-28新的代码(因为上面的代码也是参考别人的,所以呢,过了好久,我在运行我以前写的程序,各种报错,实在是懒得看,所以我自己又写了一个简易的分割代码,非常简单)
import cv2
from matplotlib import pyplot as plt
## 根据每行和每列的黑色和白色像素数进行图片分割。
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
#
# 读取彩色的图片
def gray(img):
# 转换为灰度图
img1 = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
plt.title("huidutu")
plt.imshow(img1)
plt.show()
return img1
def gaosi(img1):
#高斯滤波
img2 = cv.GaussianBlur(img1, (5, 5), 10)
plt.title("gaositu")
plt.imshow(img2)
plt.show()
return img2
def sobel(img2):
# 用Sobel进行边缘检测
img3 = cv.Sobel(img2, cv.CV_8U, 1, 0, ksize=1)
plt.title("bianyuantu")
plt.imshow(img3)
plt.show()
return img3
def cannyy(img3):
#canny边缘检测
img4 = cv.Canny(img3, 250, 100)
plt.title("cannytu")
plt.imshow(img4)
plt.show()
return img4
def erzhihua(img4):
#二值化
i, img5 = cv.threshold(img4, 0, 255, cv.THRESH_BINARY)
plt.title("erzhihua")
plt.imshow(img5)
plt.show()
return i,img5
def fushi_pengzhang(img5):
# 腐蚀和膨胀
kernel = cv.getStructuringElement(cv.MORPH_RECT, (43, 33))
img6 = cv.dilate(img5, kernel)
plt.title("fushi_pengzahng")
plt.imshow(img6)
plt.show()
return img6
def deal_license(licenseimg):
# 车牌图片二值化
# 车牌变为灰度图像
gray_img = cv2.cvtColor(licenseimg, cv2.COLOR_BGR2GRAY)
# 均值滤波,去除噪声
kernel = np.ones((3, 3), np.float32) / 9
gray_img = cv2.filter2D(gray_img, -1, kernel)
# 二值化处理
ret, thresh = cv2.threshold(gray_img, 120, 255, cv2.THRESH_BINARY)
return thresh
if __name__=='__main__':
# 这是调用上面函数的一个主程序
#------------读入汽车图片------------
img = cv.imread("car.png")
#------显示读入的原图------
plt.title("yuantu")
plt.imshow(img)
plt.show()
# 转换为灰度图
img1 = gray(img)
# 高斯模糊
img2 = gaosi(img1)
# 用Sobel进行边缘检测
img3 = sobel(img2)
# 用canny边缘检测
img4 = cannyy(img3)
# 进行二值化处理
i,img5 = erzhihua(img4)
# 可以侵蚀和膨胀
img6 = fushi_pengzhang(img5)
# # 循环找到所有的轮廓
i,j = cv.findContours(img6,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE)
result = None
y,x = 0,0
h,w = 0,0
for i1 in i:
x,y,w,h = cv.boundingRect(i1)
if w>2*h:
print(1)
# 显示最后车牌区域的图片(图片标题)
plt.title("last_img")
plt.imshow(img[y:y+h,x:x+w])
# 保存最后的车牌区域图片(直接这样写,是保存在该程序的同级文件夹下)
plt.savefig('last.jpg')
plt.show()
result = img[y:y+h,x:x+w]
# 对分割出来的车牌区域进行二值化生成黑白图
thresh = deal_license(result)
#显示图片
cv2.imshow('thresh', thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()
结果如下
** 原图**
分割后
分割后二值化
总结
新程序里面的寻找矩形框可以结合最上面的代码进行替换,效果会更好,其实其中主要的原理就是根据opencv中的函数寻找矩形框,因为大多数车牌区域都是矩形。对二值化后的车牌区域可以进行字符识别分割,我看网上也有人实现,基本就是在该程序后面加后续的字符识别,直接堆叠就完事了,如果有不懂的,也可以在私信我。