上一小节我们对图片进行了灰度化,二值化,降噪,二值化,这一系列的目的就是为了我们这一小节,识别数字的外轮廓,进行分割
第一步,找出每个数字的外轮廓
import cv2
import numpy as np
import time
import os
def showcontours(im_fin):
contours, hierarchy = cv2.findContours(im_fin,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
im2=cv2.drawContours(im, contours, -1, (0,0,255),3)
#注意这个画出轮廓的函数,第一个参数是在哪个图上画,如果我们直接在后面的二值化图上来画的话,可能会看不见,所以我们选用了第一张也就是原图,第二个参数就是contours相当于传入了几个轮廓点的坐标,-1表示画出所有的,后面两个参数就是颜色之类的不重要
#如果只是想查看的话就用这个
'''
cv2.imshow('imshow', im2)
cv2.waitKey(0)
cv2.destroyAllWindows()
'''
#如果想保存下来的话就用这个
filename = "12123.jpg"
filepath = os.path.join(dirname, filename)
cv2.imwrite(filepath, im2)
# process photo before divide it into single number
def photo_to_gray(im):
im_gray=cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
ret,im_bin=cv2.threshold(im_gray,127,255,cv2.THRESH_BINARY_INV)
kernel=1/16*np.array([[1,2,1],[2,4,2],[1,2,1]])
im_blur=cv2.filter2D(im_bin,-1,kernel)
ret,im_fin=cv2.threshold(im_blur,127,255,cv2.THRESH_BINARY)
return im_fin
dirname="test"
files = os.listdir(dirname)
for file in files:
filepath = os.path.join(dirname, file)
im = cv2.imread(filepath)
im_fin=photo_to_gray(im)
showcontours(im_fin)
这里因为让大家更直观的看到轮廓的效果,我就先定义了一个showcontours这个函数,不过这个函数之后是不需要的
先介绍一下cv2.findContours函数,看函数名字都可以看出来,这是一个寻找轮廓的函数,然后这个函数使用的时候可能会报错,这个跟安装版本有关系
如果你的opencv是3.x版本的,你需要用三个变量接受返回值,而如果是4版本的只需要两个变量,老的版本这个函数返回三个值,其中一个就是输出的结果,也就是寻找到轮廓之后的图,而新版本只有两个返回值,取消了这个返回值,也就是默认直接在原图上进行修改,所以其实影响也不大,如果需要调整的话就修改一下返回值就可以,在不在原图上修改都不影响
这个函数检测的对象必须要是黑白图片,彩色的图片不可以,所以我们上一节废了那么多力气,而我们为了增加识别的准确率,还做了其他的一些操作,可以看到,我们这个验证码5和9连在一起了,如果不事先处理的话可能识别边界上有一些复杂
正式开始介绍
用法
contours, h = cv2.findContours(im_fin,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
第一个参数是我们要寻找轮廓的图像,也就是上一节我们处理后得到的那个黑白图片
第二个参数表示轮廓的检索模式,有四种
cv2.RETR_EXTERNAL表示只检测外轮廓
cv2.RETR_LIST检测的轮廓不建立等级关系
cv2.RETR_CCOMP建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息
cv2.RETR_TREE建立一个等级树结构的轮廓
我们这里只是需要检测外轮廓,所以就用第一个啦
第三个参数为轮廓的近似办法
cv2.CHAIN_APPROX_NONE
存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))==1
cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息
cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
返回两个值,一个是轮廓本身,还有一个是每条轮廓对应的属性
contours是一个 list类型,中的每个元素都是图像中的一个轮廓,用numpy中的ndarray表示,所以我们可以通过contours遍历画出每一个数字的轮廓,也是我们分割的基础,通过print(countours[0])可以看到里面其实储存的是一个个点的坐标
然后第二个返回值很少用上,所以不细讲了
我们可以先查看一下识别完轮廓的效果
因为这里的线比较粗可能看不太清楚
接下来,我们就需要根据这个轮廓进行分割,得到一张张的图片,这也是比较麻烦的一步
得到单个数字图片
一般情况下,数字之间距离比较远,分割很容易,得到图片也比较轻松,但是有些情况下,由于数字之间距离比较近,可能会导致识别的时候将他们识别为一个物体,所以我们需要在后面的策略中进行区分
其实也比较好处理,如果是黏在一起了,就用最常用的对半分,不过这样处理可能得到的图片效果会不太理想,但我目前没有什么更好的方法
这里的主要流程就是遍历每一个轮廓
调用cv2.boundingRect(contour)
这个函数其实就是找一个最小的正矩形(边平行于坐标系)可以框住我们轮廓的,传入的参数就是储存轮廓信息的contour,因为contour里面点太多了,如果用contour里面描点切割就太麻烦了,所以我们就偷懒找矩形
返回四个值,分别是x,y,w,h
x,y是矩阵左上点的坐标,w,h是矩阵的宽和高
图像坐标系水平x,垂直y轴,y方向是上小下大(注意,并不是下小上大),水平x轴方向是左小右大,和我们常用的坐标系稍有不同
然后按照左上,右上,右下,左下的顺序保存为np数组(int0就是转化为整形,等于int64),box就是我们临时储存4个坐标的变量,最后全部放到result里并返回
具体过程中的处理我都写在代码里了
import cv2
import numpy as np
import time
import os
#divide phto into single number
def divide(im):
contours, hierarchy = cv2.findContours(im,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
result=[]#用来保存我们分割得到的结果
if len(contours) == 4:
#如果是4个的话,直接把每个矩形的点位置保存下来
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
box = np.int0([[x,y], [x+w,y], [x+w,y+h], [x,y+h]])
result.append(box)
elif len(contours)==3:
#如果是3个的话,说明有两个在一起了,那么先找出最宽的那一个,然后对最宽的这个进行二分,其他的跟上一步一样
a=[]
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
a.append(w)
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
if(w!=max(a)):
box = np.int0([[x,y], [x+w,y], [x+w,y+h], [x,y+h]])
result.append(box)
else:
box_left = np.int0([[x,y], [x+w/2,y], [x+w/2,y+h], [x,y+h]])
box_right = np.int0([[x+w/2,y], [x+w,y], [x+w,y+h], [x+w/2,y+h]])
result.append(box_left)
result.append(box_right)
elif len(contours)==1:
#如果是1个也比较简单,直接4分
x, y, w, h = cv2.boundingRect(contour)
box0 = np.int0([[x,y], [x+w/4,y], [x+w/4,y+h], [x,y+h]])
box1 = np.int0([[x+w/4,y], [x+w*2/4,y], [x+w*2/4,y+h], [x+w/4,y+h]])
box2 = np.int0([[x+w*2/4,y], [x+w*3/4,y], [x+w*3/4,y+h], [x+w*2/4,y+h]])
box3 = np.int0([[x+w*3/4,y], [x+w,y], [x+w,y+h], [x+w*3/4,y+h]])
result.extend([box0, box1, box2, box3])
elif len(contours) == 2:
#两个的话就用两种情况,31和22可以通过之前的倍数比较,如果是31w相差是很大的,22就没那么大
a=[]
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
a.append(w)
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
if w == max(a) and max(a) >=min(a) * 2:
box_left = np.int0([[x,y], [x+w/3,y], [x+w/3,y+h], [x,y+h]])
box_mid = np.int0([[x+w/3,y], [x+w*2/3,y], [x+w*2/3,y+h], [x+w/3,y+h]])
box_right = np.int0([[x+w*2/3,y], [x+w,y], [x+w,y+h], [x+w*2/3,y+h]])
result.append(box_left)
result.append(box_mid)
result.append(box_right)
elif max(a)< min(a) * 2:
box_left = np.int0([[x,y], [x+w/2,y], [x+w/2,y+h], [x,y+h]])
box_right = np.int0([[x+w/2,y], [x+w,y], [x+w,y+h], [x+w/2,y+h]])
result.append(box_left)
result.append(box_right)
else:
box = np.int0([[x,y], [x+w,y], [x+w,y+h], [x,y+h]])
result.append(box)
elif len(contours) > 4:
print("you may have wrong when divided")
return result// 返回保存的结果
# process photo before divide it into single number
def photo_to_gray(im):
im_gray=cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
ret,im_bin=cv2.threshold(im_gray,127,255,cv2.THRESH_BINARY_INV)
kernel=1/16*np.array([[1,2,1],[2,4,2],[1,2,1]])
im_blur=cv2.filter2D(im_bin,-1,kernel)
ret,im_fin=cv2.threshold(im_blur,127,255,cv2.THRESH_BINARY)
return im_fin
dirname="test"
files = os.listdir(dirname)
for file in files:
filepath = os.path.join(dirname, file)
im = cv2.imread(filepath)
im_fin=photo_to_gray(im)
divide(im_fin)
然后我们再加上保存的函数就可以把第一步我们分割得到的数字样本保存下来啦
首先还是利用得到的矩形信息画出数字的框架(这里是为了方便大家看才使用的彩色照片,代码里面是黑白色的)
这是box里面的数据,接下来我们就可以利用这里面的坐标来提取这个数字
这里使用的方法是im[y1:y2,x1:x2]
如果调换了顺序就切割反了
调整一下大小,利用时间戳保存,不过这里最好用新的目录,这个目录专门放置切割好的图片
效果如下
import cv2
import numpy as np
import time
import os
#divide phto into single number
def divide(im):
contours, hierarchy = cv2.findContours(im,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
result=[]
if len(contours) == 4:
#如果是4个的话,直接把每个矩形的点位置保存下来
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
box = np.int0([[x,y], [x+w,y], [x+w,y+h], [x,y+h]])
result.append(box)
elif len(contours)==3:
#如果是3个的话,说明有两个在一起了,那么先找出最宽的那一个,然后对最宽的这个进行二分,其他的跟上一步一样
a=[]
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
a.append(w)
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
if(w!=max(a)):
box = np.int0([[x,y], [x+w,y], [x+w,y+h], [x,y+h]])
result.append(box)
else:
box_left = np.int0([[x,y], [x+w/2,y], [x+w/2,y+h], [x,y+h]])
box_right = np.int0([[x+w/2,y], [x+w,y], [x+w,y+h], [x+w/2,y+h]])
result.append(box_left)
result.append(box_right)
elif len(contours)==1:
#如果是1个也比较简单,直接4分
x, y, w, h = cv2.boundingRect(contour)
box0 = np.int0([[x,y], [x+w/4,y], [x+w/4,y+h], [x,y+h]])
box1 = np.int0([[x+w/4,y], [x+w*2/4,y], [x+w*2/4,y+h], [x+w/4,y+h]])
box2 = np.int0([[x+w*2/4,y], [x+w*3/4,y], [x+w*3/4,y+h], [x+w*2/4,y+h]])
box3 = np.int0([[x+w*3/4,y], [x+w,y], [x+w,y+h], [x+w*3/4,y+h]])
result.extend([box0, box1, box2, box3])
elif len(contours) == 2:
#两个的话就用两种情况,31和22可以通过之前的倍数比较,如果是31w相差是很大的,22就没那么大
a=[]
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
a.append(w)
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
if w == max(a) and max(a) >=min(a) * 2:
box_left = np.int0([[x,y], [x+w/3,y], [x+w/3,y+h], [x,y+h]])
box_mid = np.int0([[x+w/3,y], [x+w*2/3,y], [x+w*2/3,y+h], [x+w/3,y+h]])
box_right = np.int0([[x+w*2/3,y], [x+w,y], [x+w,y+h], [x+w*2/3,y+h]])
result.append(box_left)
result.append(box_mid)
result.append(box_right)
elif max(a)< min(a) * 2:
box_left = np.int0([[x,y], [x+w/2,y], [x+w/2,y+h], [x,y+h]])
box_right = np.int0([[x+w/2,y], [x+w,y], [x+w,y+h], [x+w/2,y+h]])
result.append(box_left)
result.append(box_right)
else:
box = np.int0([[x,y], [x+w,y], [x+w,y+h], [x,y+h]])
result.append(box)
elif len(contours) > 4:
print("you may have wrong when divided")
return result #返回保存的结果
# process photo before divide it into single number
def photo_to_gray(im):
im_gray=cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
ret,im_bin=cv2.threshold(im_gray,127,255,cv2.THRESH_BINARY_INV)
kernel=1/16*np.array([[1,2,1],[2,4,2],[1,2,1]])
im_blur=cv2.filter2D(im_bin,-1,kernel)
ret,im_fin=cv2.threshold(im_blur,127,255,cv2.THRESH_BINARY)
return im_fin
#save photo being divided
def write_single_number(im,boxs):
for box in boxs:
im_divide= im[box[0][1]:box[3][1],box[0][0]:box[1][0]]
im_adjust= cv2.resize(im_divide, (30, 30))
timestamp = int(time.time() * 1e6)
filename = "{}.jpg".format(timestamp)
filepath = os.path.join(dir_afterdivide, filename)
cv2.imwrite(filepath, im_adjust)
dirname="test"
dir_afterdivide="number"
files = os.listdir(dirname)
for file in files:
filepath = os.path.join(dirname, file)
im = cv2.imread(filepath)
im_fin=photo_to_gray(im)
boxs=divide(im_fin)
write_single_number(im_fin,boxs)
那么今天到这里也差不多结束了,等着下一节哟