使用tiny-yolov2训练自己的图片数据

本文参考https://blog.csdn.net/Kuo_Jun_Lin/article/details/80428653的方法,通过手机拍摄建立了300张的图片数据库,通过tiny-yolo2训练后,能够准确识别门牌号,效果如下。
使用tiny-yolov2训练自己的图片数据_第1张图片
使用tiny-yolov2训练自己的图片数据_第2张图片

数据收集

一,首先需要收集足够数量的图片数据,一般有三种方法:
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
  
  
    human
    Unspecified
    0
    0
    
      111.93951612903226
      10.352419354838723
      166.11491935483872
      118.02177419354837
    
  
  
    wheel
    Unspecified
    0
    0
    
      173.27016129032256
      82.58629032258065
      227.10483870967744
      144.25766129032257
    
  
  
    wheel
    Unspecified
    0
    0
    
      47.88306451612903
      73.7274193548387
      113.98387096774195
      138.46532258064514
    
  

比较重要的部分是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文件夹中。

你可能感兴趣的:(yolo,python,darkflow,tensorflow)