本文参考https://blog.csdn.net/Kuo_Jun_Lin/article/details/80428653的方法,通过手机拍摄建立了300张的图片数据库,通过tiny-yolo2训练后,能够准确识别门牌号,效果如下。
一,首先需要收集足够数量的图片数据,一般有三种方法:
1,直接在网下下载数据集
2,通过爬虫转件爬取大量图片
在此提供一个爬虫代码
# 一个用来调用网路资源的包,包含解析网址,设定头档
import urllib.request as urst
# 全名为 Regular Expression 正则表达式,用来 “制定规则” 的匹配机制
import re
# 一个可以让我们自由方便操纵系统文件与路径的包
import os
# 一个全世界电脑通用的数据格式包,可以方便处理 json 文档里面的信息
import json
# 一个运算超级快速的数字相关的科学工具,用来处理数字提升效率
import numpy as np
# 一个用来对付 Tag 和其里面内容之资料形态的包,可以很方便帮我们把我们需要的信息剥离出来
from bs4 import BeautifulSoup as bs
# 建立一个头档,让对方的 server 以为是我们使用浏览器的方式登入他们网站,避免不必要的阻拦
opener = urst.build_opener()
hd = ('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:55.0) Gecko/20100101 Firefox/55.0')
opener.addheaders = [hd]
# 网址如果使用该公司提供的 API,则代入浏览器里面的 URL 位置找到的将不再是网页,而是一个 json 文件
googleSearch = 'https://www.google.com/search?ei=1m7NWePfFYaGmQG51q7IBg&hl=en&q={}\&tbm=isch&ved=0ahUKEwjjovnD7sjWAhUGQyYKHTmrC2kQuT0I7gEoAQ&start={}\&yv=2&vet=10ahUKEwjjovnD7sjWAhUGQyYKHTmrC2kQuT0I7gEoAQ.1m7NWePfFYaGmQG51q7IBg\.i&ijn=1&asearch=ichunk&async=_id:rg_s,_pms:s'
baiduSearch = 'http://image.baidu.com/search/index?tn=baiduimage&ps=1&ct=201326592&lm=-1&cl=2&nc=1&ie=utf-8&word={}'
# 定义一个函数,能够让我们输入关键词之后,借由 google 的搜寻回传一组全是图片网址组成的 list
def google_get_links(keywords):
# 人类的语言使用空格分离,但是在 URL 的规则中,空格要使用 “+” 替代
keywords = keywords.replace(' ', '+')
# 把处理好的重组关键词放入到对应的网址位置中,0表示低0页(即可)
google_url = googleSearch.format(keywords, 0)
# 使用 urllib 包打开重组好的网址,并且 “读” 里面的内容后,赋值于 data 这个自创的 object
data = opener.open(google_url).read()
# 把被赋值的 data object 转换成 json 格式的 list 信息
page = json.loads(data)
# 使用 bs4 的方法过滤 Tag 内容前的预处理,把 object 转换成 bs4 对应的属性
soup = bs(page[1][1], 'lxml', from_encoding='utf-8')
# 找出所有名为 ”img” 的标签
img_tags = soup.findAll('img')
# 找出 img 标签中国呢备注了 ‘src' 里面的内容,也就是我们要的网址
links = [target.get('src') for target in img_tags]
# 最后遍历到一个 list 里面会传给该函数
return links
# 定义一个函数,能够让我们输入关键词之后,借由 baidu 的搜寻回传一组全是图片网址组成的 list
def baidu_get_links(keywords):
keywords = keywords.replace(' ', '+')
baidu_url = baiduSearch.format(keywords)
data = opener.open(baidu_url).read()
soup = bs(data, 'lxml', from_encoding='utf-8')
img_tags = soup.findAll('script', {'type': 'text/javascript'})
# 定义一个空的 list 用来承载在回圈中被不断添加的内容
links = []
# 由于这边没有使用 API,所以找出来的东西会很杂乱,需要自行解析,自行找规则
for chaos in img_tags:
chaos_str = chaos.string
# 有的内容是 None,在判断式那边会报错,因此直接加上 try 来把这个问题避开,如果报错了,就 continue 即可
try:
if 'ObjURL' in chaos_str:
# 发现了‘ObjURL’ 后面的网址就是我们要的网址,因此用这个词把字符串分段组成新的 list
Split = chaos_str.split('objURL":"')
# 使用正则匹配后面我们期望看到的网址样貌,记得使用的是 ”懒惰模式“
target_format = re.compile('^(http)s?.*?[^"]?.*?(jpg|png|jpeg|gif)')
for chaos_split in Split:
# 同样为了达到避免报错的目的,而设置的 try / except
try:
# 把匹配成功的内容放到那个空的 list 里面
i = target_format.match(chaos_split).group()
links.append(i)
except:
continue
except:
continue
return links
# 定义一个函数用来把上面形成 list 的图片网址集合下载下来,并根据其搜寻名称放置到我们喜欢的文件夹路径中
def save_images(links_list, keywords):
# 由于在使用电脑呼叫文件的时候,不希望看到空格,因此这边使用 "_" 替代
folderName = keywords.replace(' ', '_')
# 使用 join 好处是只要输入两个谁前谁后要排起来的路径名即可,不用中间还自己加 "/" 之类的东西
directory = os.path.join(input('Enter the path to store the images: '), folderName)
# 如果那个路径里面没有这个文件夹,那就创造一个,有的话就莫不作为
if not os.path.isdir(directory):
os.mkdir(directory)
for sequence, link in enumerate(links_list):
# 使用该图片网址在 list 中的排序来命名,并且重新生成一个最终的下载状态(包含档名)
savepath = os.path.join(directory, '{:06}.png'.format(sequence))
# 如果图片文件夹里面有一些隐藏文件,会导致下载失败,那就用 try 避开,让程序继续运行下去
try:
# 用这函数下载图片网址,并且把下载下来的东西放到指定的路径里面
urst.urlretrieve(link, savepath)
except:
continue
# 如果呼叫的这些函数名字都是写在这个文档的函数,并非 import 进来的话,则运行;否则忽略
if __name__ == '__main__':
keyword = 'audi'
links_from_google = google_get_links(keyword)
# links_from_baidu = baidu_get_links(keyword)
save_images(links_from_google, keyword)
3,自己手工准备图片
我是通过这种办法,用手机一张张的拍,大概准备了300张,每个类型准备了50张左右。
在收集完成数据以后,需要一一将图片标记,标记好的信息会储存到XML文件中。XML格式如下
motorcycle
000026.png
0
338
149
3
比较重要的部分是size和bndbox两个参数,第一个标注了图片的尺寸大小,第二个标注了目标物体出现的位置。在此提供一段将图片进行标注的代码。
# 用来操控电脑相关文件属性与档案路径的模块
import os
# 用来操控图片,导入摄像头,处理色彩超级模块
import cv2
# 这是一个 matplotlib 下面的类,主要功能是制图
import matplotlib.pyplot as plt
# 这是一个 matplotlib 下面的类,提供格式小工具,这边使用的是方形选取
from matplotlib.widgets import RectangleSelector
# 一个用来处理 xml 文本的包,这边用来重新排布文本段落,使之更可视化
from lxml import etree
# 同上,只是它的引用名太长了,给个短的名字方便
import xml.etree.cElementTree as ET
# 人工输入一个我们目标的文件夹路径,让代码从此展开执行的旅程
folder_path = input("Enter the directory of the targeted folder: ")
# 一开始要全选出来的东西的名字也需要在这里 ”初始化“
Label_name = input("The object name being specified: ")
# 设置一个 list 容器,分别用来容纳之后经过鼠标产出的数据
TL_corner = []
BR_corner = []
Labels_list = []
# 定义一个 “事件“ 名为 mouse_click 的函数,在下面与 “RectangleSelector" 相连接
def mouse_click(press, release):
# 当需要对这些变量在函数里面被修改的时候,用上 global 会比较精确且保险
global TL_corner
global BR_corner
global Labels_list
# 如果第一个参数按下去的 .button 按钮是鼠标左键的话
if press.button == 1:
# 就把这个框框左上与右下的值分别贴到两个空的 list 中做保存,并且把一开始我们取好名字的 Label 也存入 list 中
TL_corner.append((int(press.xdata), int(press.ydata)))
BR_corner.append((int(release.xdata), int(release.ydata)))
Labels_list.append(Label_name)
# 如果第二个参数放开鼠标的 .button 按钮是左键的话
elif release.button == 3:
# 就把最近一次存进去 list 里面的元素给删了,并且打印字串告知
del TL_corner[-1]
del BR_corner[-1]
del Labels_list[-1]
print('-- Latest bounding box has been removed --')
# 定义一个函数,用来在中途改变我们要标记的物体
def change_label(event):
# 当需要对这些变量在函数里面被修改的时候,用上 global 会比较精确且保险
global Label_name
# 如果按下的按钮是滑鼠中间的滚轮(如果你的滑鼠没有滚轮那就 GG 了)
if event.button == 2:
# 继续让方框选择的功能开启
selectImg_RS.set_active(True)
# 重新输入定义标签名称
Label_name = input('The other object name being specified: ')
# 即便鼠标按下的功能不是滚轮,也还是要确保方框选择功能是被开启的状态
elif event.button != 2:
selectImg_RS.set_active(True)
'''
这只是个用来学习和测试的代码部分,之所以留下来就是为了深刻告诉自己:
在 matplotlib.widgets 这个模块中的 RectangleSelector 和诸多 plt.connect(‘event name’, function)
的不同之处,当时用 RectangleSelector 的时候,他所链接到的自定义 function 就可以有两个 arguments,
他们分别表示按下和放开方框的时候鼠标的坐标轴位置,而 connect 的 event 就不同。
def mouse_press(press):
global TL_corner
if press.button == 1:
TL_corner.append((int(press.xdata), int(press.ydata)))
elif press.button == 3:
print('-- Release button to remove your latest bounding box --')
else:
print('-- Please use mosue left click to select an area --')
def mouse_release(release):
global BR_corner
if release.button == 1:
BR_corner.append((int(release.xdata), int(release.ydata)))
elif release.button == 3:
del TL_corner[-1]
del BR_corner[-1]
else:
print('-- Please use mosue left click to select an area --')
拿这两个 function 做举例他们分别连接到的是 ‘button_press_event’ 和 ‘button_release_event’,
只容许他们在定义函数的时候有一个 argument 表示按下或是放开的瞬间鼠标坐标点的位置。
而那个 argument 自带的 .xdata | .ydata | .button 属性也是在 .connect 链接起来后自己产生的 attribute,
如果没有 connect,那是没有 .xdata 这类功能的。
'''
# 定义一个 xml 文件生成函数,需要方框的两个顶点坐标信息,图片放置位置,与最一开始的手动输入的目标文件位置
def xml_maker(TL_corner, BR_corner, file_path, folder_path):
# os.path 生成的 object 有一个 .name 功能打印改路径的最后一个文件名称
target_img = file_path.name
# 告知 xml 文件最后面应该存在哪个资料夹,os.path.split() 可以把最后一个文件名和前面路径分开成为一个 tuple 里面的两个不同元素
xml_save_dir = os.path.join(os.path.split(folder_path)[0],
os.path.split(folder_path)[1] + "_xml")
# 如果没有这个文件夹名字的话,创造一个该路径下的文件夹
if not os.path.isdir(xml_save_dir):
os.mkdir(xml_save_dir)
# 开始编辑 xml 文件内容,最外层的 Tag 叫 annotation
main_tag = ET.Element('annotation')
# main_tag 下面有许多子 tags,分别他们的内容要装的是对应到的文件夹名称,对应图片名称
ET.SubElement(main_tag, 'folder').text = os.path.split(folder_path)[1]
ET.SubElement(main_tag, 'filename').text = target_img
ET.SubElement(main_tag, 'segmented').text = str(0)
# 同理上面编辑步骤,把图片的尺寸资料记录于此
size_tag = ET.SubElement(main_tag, 'size')
ET.SubElement(size_tag, 'width').text = str(width)
ET.SubElement(size_tag, 'height').text = str(height)
ET.SubElement(size_tag, 'depth').text = str(depth)
# 由于 object 可能有很多个,甚至很多个 objects 要记录,这边需要迭代,把三个 list 容器重新 zip 在一起会方便许多
for La, TL, BR in zip(Labels_list, TL_corner, BR_corner):
# 同理上面编辑步骤,把 object 对应的名字等信息记录于此
object_tag = ET.SubElement(main_tag, 'object')
ET.SubElement(object_tag, 'name').text = La
ET.SubElement(object_tag, 'pose').text = 'Unspecified'
ET.SubElement(object_tag, 'truncated').text = str(0)
ET.SubElement(object_tag, 'difficult').text = str(0)
# 同理上面编辑步骤,把方框起来的坐标记录于此
bndbox_tag = ET.SubElement(object_tag, 'bndbox')
ET.SubElement(bndbox_tag, 'xmin').text = str(TL[0])
ET.SubElement(bndbox_tag, 'ymin').text = str(TL[1])
ET.SubElement(bndbox_tag, 'xmax').text = str(BR[0])
ET.SubElement(bndbox_tag, 'ymax').text = str(BR[1])
# 为了让 xml 排布能够漂亮,pretty_print=True 前面的 root 必须是对应的 object,所以做了一个转换过去然后又变回来的过程
xml_str = ET.tostring(main_tag)
root = etree.fromstring(xml_str)
xml_str = etree.tostring(root, pretty_print=True)
# 重新命名文件夹并重新整合储存路径,修改意味着先要拆开
# os.path.splitext 可以良好的把文件名和档名分成两个元素放在一个 tuple 里面
save_path = os.path.join(xml_save_dir,
str(os.path.splitext(target_img)[0] + '.xml'))
# 储存文件于该位置
with open(save_path, 'wb') as xml_file:
xml_file.write(xml_str)
# 定义一个函数,当对一张图片的事情做好了之后,跳到下一张图片时候需要做的事情
def next_image(release):
global TL_corner
global BR_corner
global Labels_list
# 如果按下的按钮是 Space 键,且方框选取功能是开着的
if release.key in [' '] and selectImg_RS.active:
# 那就呼叫刚定义好的生成 xml 函数
xml_maker(TL_corner, BR_corner, file_path, folder_path)
# 为了给自己方便看存了什么,在内容还没背归零最前先打印出来给我看看
print(TL_corner, BR_corner, Labels_list)
# 归零,并关掉该窗口
TL_corner = []
BR_corner = []
Labels_list = []
plt.close()
# 如果按的不是 Space 键,则打印下面句子
else:
print('-- Press "space" to jump to the next pic --')
# 只有当前 .py 文件呼叫的函数可以被执行,如果是 import 进来的文本里面有函数执行指令,该函数就会被挡住不执行
if __name__ == '__main__':
# 遍历每个一开始输入进去的路径里面的文件路径
for file_path in os.scandir(folder_path):
# 如果里面有些文件不符合预期,让程序报错了,用此跳开进到下一个文件
try:
# 习惯的画图手法,可以一次创造 figure 和 axis 两个 objects 并且还同时描绘了几个子窗口,非常方便
fig, ax = plt.subplots(1)
# 使用 opencv 读取图片信息,找出其长宽深度值
image = cv2.imread(file_path.path, -1)
height, width, depth = image.shape
# 并且由于在 matplotlib 显示图片是 RGB 格式,和 opencv 的BGR 顺序不同,需要转制
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 在 matpolotlib 基础上秀出图片内容
ax.imshow(image)
# 把 widgets 里面的 RectangleSelector 跟 mouse_click 做关联,给他一个名字原因纯粹是太长了,要开要关不方便
selectImg_RS = RectangleSelector(
ax, mouse_click, drawtype='box',
useblit=True, minspanx=5, minspany=5,
spancoords='pixels', interactive=True)
# 一样把其他上面设定好的函数与图片关联
plt.connect('button_press_event', change_label)
plt.connect('key_release_event', next_image)
# plt.connect('button_press_event', mouse_press)
# plt.connect('button_release_event', mouse_release)
# 这里把在图片上面做的事情 show 出来
plt.show()
# 如果报错,则直接跳到下一个循环中
except:
continue
这次的代码相对上次来说,是比较多的,做的事情也更为复杂和多元,主要代码任务排布顺序如下:
1,找到装载很多张图片档案的目标资料夹
2,把这些资料夹里面的照片一次一张的方式显示出来
3,在图片上面拉上方框选取我们肉眼判定的目标区域
4,如果区域拉错了,可以使用滑鼠右键拉方框删除最近一次拉的方框坐标点记录
5,按下滑鼠中间滚轮可以重新定义方框的名字
6,按下键盘上 “空白键” 储存那些拉好的方框坐标位置到 XML 文件中
7,重复步骤完成所有图片的标记工作
代码来源https://blog.csdn.net/Kuo_Jun_Lin/article/details/80430238
在这里使用迁移学习的方法,在别人训练好的数据的基础上,加入自己的数据集,可以快速的得到自己想要的模型。
在官网可以下载得到tiny-yolo2的权重wieight以及模型model。
https://pjreddie.com/darknet/yolo/
同时我们需要在github上下载darkfllow,他可以通过python语言调用yolo2的模型与权重
https://github.com/thtrieu/darkflow
下载完成后,需要对一些文件进行初始的配置。具体的配置和使用参考darkflow中readme文件,在此罗列一下重点。
1,cfg 文件修改
把文件用文字编译器打开后,就会发现它是一个整齐化一,且针对每层神经网络参数描述详细的文档,拉到最下面一层神经网络的显示内容后,把要训练的物体种类数量在 [region] 所属的下面为止打上,并在 filter = … 的位置输入上 5*(class + 5) 的计算结果,完事之后保存。并最后把档名些微修改成我们修改过的样子以防搞混。
这里的class指的是你预定的需要训练的类型的数量,如8种,filiter就应为65。
2,label.txt 修改标签名称
打开 darkflow 里面的该文档,把自己要取的种类名称输入上存档即可。
3,如果需要使用GPU加速,请安装CUDA。
4,darkflow完美兼容第一代yolo,可以使用tiny-yolo或者yolo进行训练,但是我在网上已经找不到第一版本的yolo的权重和模型。无论使用哪个版本,尽量使用tiny版,可以节省很多的内存和时间。目前darkflow并不支持yolo3,对tiny-yolo2的支持需要修改darkflow中某一段的代码,否则会出错(期待的数据和提供的数据相差4个单位),具体的修改方法可以在网上查找。
进入darkflow文件夹,打开命令行输入指令。(注意在此输入自己图片文件夹目录以及xml文件夹目录)
sudo flow --model cfg/tiny-yolo2.cfg --load bin/tiny-yolo2.weights --train --annotation menpai_xml --dataset menpai --epoch 800
如果是window,将 sudo命令 改为python,输入命令行。
具体的命令方法可以参考darkflow的readme文件,如打开gpu加速,中途保存模型等。
如果不出问题,此时应该已经开始训练,训练完成的模型保存在build-graph文件夹中。