何为 YOLO(You Only Look Once)
YOLO [点击] 充满青春暗示的另一面,更是一个强大物体检测算法的简称,其算法结合了速度与准确度,近期更推出了第三版的 model 供大家参考,大幅度提升了前一版的性能。作者为一位资深 computer scientist: Joseph Chet Redmon 精通数据科学,编程等工作,与另外四名伙伴一同开发了此模型。
与以往的重复定位不同图片区域,并多次加权算出最高的得分区域来衡量该物体的属性与位置不同,YOLO 采用了一次性的神经网络层,直接平行分析图片中每个区域的各种可能,并在可能性最大的区域画出一个方框后标记方框内的物体对应的属性。这样的流程大幅提升准确度和计算速度,击败诸多先前模型的 performance。更详细的描述可以参考此篇文章 [点击]。
我作为一个计算机小白出身的光学工程师,学习之路走到今天能用上大神推行出来的物体检测算法来应用到自己的电脑上,已经是无比激动的一件事情了。从 Python 代码的编写学习,到模块包应用如:os, re, bs4, math, urllib, cv2, numpy, moviepy, ... 等等,再跳到现在的综合考验环节,做出自己设定结果的瞬间,只想感谢强大的万维网提供如此之完备的资源能够让一个人独立完成所有工作。一方面为了加深自己对此领域的记忆,一方面也希望自己淌过的坑可以说出来和大家分享,让同为这方面的爱好者有个前车之鉴,但是本人资历尚浅,要是描述中有何不妥也衷心感谢大神们的回馈!
实时物体检测的主要三环节
- 搜集数据
面对成千上万的物体种类与名称,必须预先搜集我们期望机器了解的种类的图像数据,通常需要上千张类似的图片作为资料库,面对庞大的量我们将会选择使用简单爬虫的方法,利用常见的搜寻引擎如 Google 或是 Baidu 作为主要手段。 - 标记数据
搜集好数据后,接下来的步骤就是手把手的教导机器 “什么是什么” 环节。面对每张涵盖了相关信息内容的图片,我们将基于这些图片生成一个 xml 文档来记录相关物体的坐标信息,并以此 xml 文档与对应图片作为输入要件,进入到下一个步骤。 - 训练模型
下载好相关的 YOLO 文件(它们都是检测算法里面重要的部件),并在 Terminal 或是 CMD 里面告诉计算机前面两个环节生成好的文件所在路径,最后开始让计算机去根据我们搜集来的数据训练,等到 Loss Value 降低到一个极限的时候,基本上就可以认定它被训练好了,并可以作为一个更新的模型辨认出我们希望它辨认的物体。
Data Collecting 搜集数据
于实战中使用到的 Python module 有:(模块引用名: 使用文档)
- urllib: https://docs.python.org/3/library/urllib.html
- re: https://docs.python.org/3/library/re.html
- os: https://docs.python.org/3/library/os.html
- json: https://docs.python.org/3/library/json.html
- numpy: https://docs.scipy.org/doc/numpy-1.11.0/numpy-user-1.11.0.pdf
- bs4: https://media.readthedocs.org/pdf/beautiful-soup-4/latest/beautiful-soup-4.pdf
p.s. 更多记录会逐一完备并添加链接于此,更为深入的介绍个人使用每个包的心得与常见功能
The codes are written below in practice:
# 一个用来调用网路资源的包,包含解析网址,设定头档
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)
设计代码的时候,python 环境中的我们同样倾向于模块化的功能编写,这样不仅写出来的代码逻辑性强,如果模块的名字取得好,那写出来的代码就可以说是超级易懂的了。整体代码步骤如下:
- 引入需要用到的模块
- 为了防止网站反爬虫机制,做一个头档(hd)去伪装自己像是个浏览器
- 使用 “图片” 搜寻引擎的 API 作为网址(如果直接去图片搜寻的网页把其 URL 拷贝也是可以,只是效率低下,原因稍后说明)
- 定义第一个功能函数 google_get_links(keyword) ,拿一个关键词作为输入,以 google 搜寻作为手段输出一个装满相关图片网址的 list,同样的逻辑适用于函数 baidu_get_links(keyword)
- 定义第三个功能函数 save_images(links_list, keyword) 用来下载前面函数找下来一箩筐的图片网址,把这些图片网址下载成图片存到本地指定的新建资料夹位置,并以 keyword 关键词作为新建资料夹的命名
一模一样的关键词,一摸一样的搜寻引擎,在没有 API 支持下找出来的东西就会东缺西漏的令人不舒服... 并且判断是也更为复杂才能够达到预期的目的。
重新细究代码
如果我们仔细观察输入关键词后的 URL 会发现原本空格隔开的部分都用上了 “+” 替代,因此最一开始就要对人类的输入关键词做预处理,把空格全部改成 “+” 的形式,接下来把改好的词对应放入 URL 中,方法就是用 {} 作为插入位置的标记,使用 XXX.format(name1, name2, ...) 一一插入顺序位置。
重新组装完毕的 URL 就可以被放到加好头档的 urllib 模块功能函数 urlopen() 或是 build_opener().open() 中开始爬虫,回传 data 物件即为一大大大串的 binary string,如果是经由 API 网址的结果,得出来的 string 会充满规律,一切我们期望得到的图片网址都乖乖的安放在 <img src='the target link'> 中,只要使用 bs4 里面的 BeautifulSoup 功能,就可以简单迅速的找出所有匹配结果,并回传成一个 list 格式给这个函数。
但是如果没有 API 就麻烦了,首先得自己经由浏览器解析 HTML 的编码,但事后我们会发现浏览器显示的编码和 urllib 抓取下来的编码尚有些出入,我们需要的图片网址都存在,但是归属存放的标签名称则不同,我这边使用的方法是把抓取下来的 data 转换成字符串,用 split() 切成片段重新组成 list,并用 re.complle 正则表达式去匹配我要的网址格式,最后汇集成为一个完整的 list。
最后储存图片阶段,我们可以用 os.path.join(a, b) 把两个结果合并在一起成为一个 “路径该有的样子” 而不需要我们自己手动去添加 “/” 这类的东西。检查该路径中是否已经有存在同一个文件夹,如果没有的话则用 mkdir 新创一个。并且我们下载如此之多的图片,就要依照下载顺序编号命名,enumerate 作为回圈的话可以输出每一个循环的回合数作为第一个值 (如代码),使用 urllib.request.urlretrieve() 来下载该图片,并放到创建好的资料夹和命名规则。
最后的最后,if __name == "__main__": 这段的解释是让 python 程序可以顺利的执行当前文件下被呼叫的 functions,把那些在不同模块里面(被 import 引用进来的)同名的 functions 在执行的时候屏蔽掉,更多关于 if __name == "__main__": 的详细解释可以参考 [点击1],[点击2]。