前言
以下内容仅为个人在学习人工智能中所记录的笔记,先将目标识别算法yolo系列的整理出来分享给大家,供大家学习参考。
本文仅对YOLOV3代码中关键部分进行了注释,未掌握基础代码的铁汁可以自己百度一下。
若文中内容有误,希望大家批评指正。
资料下载
YOLOV3论文下载地址:YOLOv3:An Incremental Improvement
回顾
YOLO V1:【YOLO系列】YOLO V1论文思想详解
YOLO V2:【YOLO系列】YOLO V2论文思想详解
YOLO V3:【YOLO系列】 YOLOv3论文思想详解
项目地址
YOLOV3 keras版本:下载地址
YOLOV3 Tensorflow版本:下载地址
YOLOV3 Pytorch版本:下载地址
Gitee仓库
YOLOV3 各版本:yolov3各版本
本文主要基于keras版本进行讲解
话不多说,直接上代码
一、yolo.py脚本代码详解
yolo.py脚本主要用于评估输入的图像,输出检测的目标,并在图像中绘制检测出的目标与置信度。
1、设置默认参数
包括模型文件、Anchor Box、类别文件、检测阈值、IOU阈值、图像大小以及使用的gpu数量
class YOLO(object):
_defaults = {
"model_path": 'model_data/yolo.h5', # 训练好的模型文件路径
"anchors_path": 'model_data/yolo_anchors.txt', # 聚类生成的Anchor Box文件路径
"classes_path": 'model_data/coco_classes.txt', # coco数据集的类别文件路径
"score": 0.3, # 目标检测阈值
"iou": 0.45, # iou阈值
"model_image_size": (416, 416), # 输入图像的大小
"gpu_num": 1, # 使用的gpu数量
}
2、设置classmethod装饰器
用于外部调用获取相关信息
# 设置classmethod装饰器,用于获取_defaults中的值
@classmethod
def get_defaults(cls, n):
if n in cls._defaults:
return cls._defaults[n]
else:
return "Unrecognized attribute name '" + n + "'"
3、初始化YOLO类参数
包括class_names,anchor,创建计算图,调用generate()方法获取boxes,score,classes
# 初始化类方法,获取class_names,anchor,session,boxes,score,classes。
def __init__(self, **kwargs):
self.__dict__.update(self._defaults) # set up default values
self.__dict__.update(kwargs) # and update with user overrides
self.class_names = self._get_class() # 获取类别的名称
self.anchors = self._get_anchors() # 获取Anchors大小
self.sess = K.get_session() # 建立的session计算图
self.boxes, self.scores, self.classes = self.generate()
4、获取类别与Anchors
def _get_class(self):
# os.path.expanduser()用于将路径字符串中的波浪线(~)扩展为用户的主目录,波浪线(~)一般在liunx中较多
classes_path = os.path.expanduser(self.classes_path)
with open(classes_path) as f:
class_names = f.readlines()
class_names = [c.strip() for c in class_names]
return class_names
def _get_anchors(self):
anchors_path = os.path.expanduser(self.anchors_path)
with open(anchors_path) as f:
anchors = f.readline()
anchors = [float(x) for x in anchors.split(',')]
return np.array(anchors).reshape(-1, 2)
5、generate()函数输出图片目标框、置信度、类别
(1)加载训练好的model文件;
(2)为所有的类别生成一个边框的颜色;
(3)创建输入图片tensor;
(4)调用评估函数输出检测图片目标框、置信度、类别。
def generate(self):
# 获取model的路径
model_path = os.path.expanduser(self.model_path)
# 判断model是否以h5结尾
assert model_path.endswith('.h5'), 'Keras model or weights must be a .h5 file.'
# Load model, or construct model and load weights.
# num_anchors = 9,yolov3有9个先验框
num_anchors = len(self.anchors)
# num_classes = 80,coco集一共80类
num_classes = len(self.class_names)
# 判断是否为tiny版本,如果是,则加载tiny model
is_tiny_version = num_anchors == 6 # default setting
try:
self.yolo_model = load_model(model_path, compile=False)
except:
self.yolo_model = tiny_yolo_body(Input(shape=(None, None, 3)), num_anchors//2, num_classes) \
if is_tiny_version else yolo_body(Input(shape=(None, None, 3)), num_anchors//3, num_classes)
self.yolo_model.load_weights(self.model_path) # make sure model, anchors and classes match
else:
# output_shape[-1]:输出维度的最后一维。 -> (?,13,13,255)->255
# 255 = (9/3)*(80+5). 9/3:每层特征图对应3个anchor box 80:80个类别 5:4+1,框的4个值+1个置信度
assert self.yolo_model.layers[-1].output_shape[-1] == \
num_anchors/len(self.yolo_model.output) * (num_classes + 5), \
'Mismatch between model and given anchor and class sizes'
print('{} model, anchors, and classes loaded.'.format(model_path))
# 为所有的类别生成一个边框的颜色。[h,s,v]
# h(色调):x/len(self.class_names) s(饱和度):1.0 v(明亮):1.0
# 对于80种coco目标,确定每一种目标框的绘制颜色,即:将(x/80, 1.0, 1.0)的颜色转换为RGB格式,并随机调整颜色以便于肉眼识别,
# 其中:一个1.0表示饱和度,一个1.0表示亮度
hsv_tuples = [(x / len(self.class_names), 1., 1.)
for x in range(len(self.class_names))]
# hsv转换为rgb
self.colors = list(map(lambda x: colorsys.hsv_to_rgb(*x), hsv_tuples))
self.colors = list(
# hsv取值范围在[0,1],而RBG取值范围在[0,255],所以乘上255
map(lambda x: (int(x[0] * 255), int(x[1] * 255), int(x[2] * 255)),
self.colors))
# 产生随机种子,固定种子为一致的颜色
np.random.seed(10101) # Fixed seed for consistent colors across runs.
# 打乱,调整颜色,避免相近颜色来装饰相邻的类
np.random.shuffle(self.colors) # Shuffle colors to decorrelate adjacent classes.
# 重置种子为默认
np.random.seed(None) # Reset seed to default.
# Generate output tensor targets for filtered bounding boxes.
# K.placeholder:keras中的占位符 相当于分配空间
# 这里是给需要检测的图片预留的,生成一个tensor,输入来自后面detect_image()函数
self.input_image_shape = K.placeholder(shape=(2, ))
# 若GPU个数大于等于2,调用multi_gpu_model()
if self.gpu_num >= 2:
self.yolo_model = multi_gpu_model(self.yolo_model, gpus=self.gpu_num)
# yolo_eval(): yolo评估函数
boxes, scores, classes = yolo_eval(self.yolo_model.output, self.anchors,
len(self.class_names), self.input_image_shape,
score_threshold=self.score, iou_threshold=self.iou)
return boxes, scores, classes
6、detect_image()函数预测并绘制目标框
(1)图片尺寸处理:将输入的图片按最长边确定一个缩放比例,然后按比例缩放(采样方法:BICUBIC)图片,再将缩放后的图片粘贴到一个用“绝对灰”R128-G128-B128填充的416x416新图片上,缩放后图片以外的部分保留为灰色;
(2)归一化图片数值,再添加一个维度生成(bitch, w, h, c)格式,用于model的输入层,调用计算图计算图片目标框、置信度、类别;
(3)使用Pillow库绘制边框,设置边框宽度,绘制边框和类别字体,将检测出来的所有目标框在图片中绘制出来,输出图片
def detect_image(self, image):
start = timer() # 定时器
if self.model_image_size != (None, None):
# 要求进行检测的图片尺寸是32的倍数,因为在Darknet网络中,执行了5次步长为2的卷积操作,即
# 图片的默认尺寸是416*416,因为在最底层中的特征图大小是13*13,所以13*32=416
assert self.model_image_size[0] % 32 == 0, 'Multiples of 32 required'
assert self.model_image_size[1] % 32 == 0, 'Multiples of 32 required'
# 调用letterbox_image()函数,即:将输入的图片按最长边确定一个比例,然后按比例缩放(采样方法:BICUBIC)图片,
# 再生成一个用“绝对灰”R128-G128-B128填充的416x416新图片后将缩放后的输入图片粘贴上去,粘贴不到的部分保留为灰色
boxed_image = letterbox_image(image, tuple(reversed(self.model_image_size)))
else:
new_image_size = (image.width - (image.width % 32),
image.height - (image.height % 32))
boxed_image = letterbox_image(image, new_image_size)
image_data = np.array(boxed_image, dtype='float32')
print(image_data.shape) # (416,416,3)
# 将缩放后图片的数值除以255,做归一化
image_data /= 255.
# 在图片前面添加一个维度 -> (1,416,416,3) 满足网络的输入格式 -> (bitch, w, h, c)
image_data = np.expand_dims(image_data, 0) # Add batch dimension.
# 计算boxes,scores,classes,这是使用的是之前建立的session()计算图
# 即调用generate()函数,将feed_dict中的图像尺寸传递给generate()函数中的placeholder
# 图片做为model的输入
out_boxes, out_scores, out_classes = self.sess.run(
[self.boxes, self.scores, self.classes],
feed_dict={
self.yolo_model.input: image_data, # 图像数据
self.input_image_shape: [image.size[1], image.size[0]], # 图像尺寸416x416
K.learning_phase(): 0 # 学习模式: 0:测试模型;1:训练模式
})
print('Found {} boxes for {}'.format(len(out_boxes), 'img'))
# 使用Pillow库绘制边框,设置边框宽度,绘制边框和类别字体
# 设置字体
font = ImageFont.truetype(font='font/FiraMono-Medium.otf',
size=np.floor(3e-2 * image.size[1] + 0.5).astype('int32'))
# 设置目标框线条的宽度
thickness = (image.size[0] + image.size[1]) // 300
# 对于c个目标类别中的每个目标框i,调用Pillow画图
for i, c in reversed(list(enumerate(out_classes))):
# 目标类别的名字
predicted_class = self.class_names[c]
# 框
box = out_boxes[i]
# 置信度
score = out_scores[i]
# 标签:类别名称+置信度
label = '{} {:.2f}'.format(predicted_class, score)
# 加载输入的原始图片
draw = ImageDraw.Draw(image)
# 返回标签文字label按照font字体与大小的宽和高(多少个pixels)
label_size = draw.textsize(label, font)
top, left, bottom, right = box
# 目标框的上、左两个坐标小数点后一位向下取整
top = max(0, np.floor(top + 0.5).astype('int32'))
left = max(0, np.floor(left + 0.5).astype('int32'))
# 目标框的下、右两个坐标小数点后一位向下取整,与图片的尺寸相比,取最小值
bottom = min(image.size[1], np.floor(bottom + 0.5).astype('int32'))
right = min(image.size[0], np.floor(right + 0.5).astype('int32'))
print(label, (left, top), (right, bottom))
# 确定标签(label)起始点位置
if top - label_size[1] >= 0:
text_origin = np.array([left, top - label_size[1]])
else:
text_origin = np.array([left, top + 1])
# My kingdom for a good redistributable image drawing library.
# 绘制目标框,线条宽度为thickness
for i in range(thickness):
draw.rectangle(
[left + i, top + i, right - i, bottom - i],
outline=self.colors[c])
# 画标签框
# 绘制一个矩形框,填充颜色作为文字背景
draw.rectangle(
[tuple(text_origin), tuple(text_origin + label_size)],
fill=self.colors[c])
# 填写标签内容
draw.text(text_origin, label, fill=(0, 0, 0), font=font)
del draw
# 结束计时
end = timer()
print(end - start)
return image
7、detect_video()函数用于视频检测
(1)打开视频文件,获取视频视频编解码器、视频的帧率、宽度与高度;
(2)从视频文件中读取每一帧进行检测;
(3)将文本(FPS)添加到图像(result)上,包括文本内容(text),文本起始位置(org),字体类型(fontFace),字体大小(fontScale),字体颜色(红色),文本线的粗细;
(4)将检测完成的图片写入out中,生成新的视频。
def detect_video(yolo, video_path, output_path=""):
import cv2
# 打开视频
vid = cv2.VideoCapture(video_path)
# 判断视频文件是否已成功打开
if not vid.isOpened():
raise IOError("Couldn't open webcam or video")
video_FourCC = int(vid.get(cv2.CAP_PROP_FOURCC)) # 标识视频编解码器
video_fps = vid.get(cv2.CAP_PROP_FPS) # 获取视频的帧率
video_size = (int(vid.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(vid.get(cv2.CAP_PROP_FRAME_HEIGHT))) # 获取视频的宽度与高度
isOutput = True if output_path != "" else False
if isOutput:
print("!!! TYPE:", type(output_path), type(video_FourCC), type(video_fps), type(video_size))
out = cv2.VideoWriter(output_path, video_FourCC, video_fps, video_size) # 创建一个新的视频文件
accum_time = 0
curr_fps = 0
fps = "FPS: ??"
prev_time = timer()
while True:
return_value, frame = vid.read() # 从视频文件中读取一帧,返回两个元素,第一个为布尔值,判断是否成功读取了帧,第二个元素为读取的帧本身,为一个数组
image = Image.fromarray(frame) # 生成图片
image = yolo.detect_image(image) # 检测图片
result = np.asarray(image)
# 计算当前图片检测时间,累计检测时间
curr_time = timer()
exec_time = curr_time - prev_time
prev_time = curr_time
accum_time = accum_time + exec_time
curr_fps = curr_fps + 1
if accum_time > 1:
accum_time = accum_time - 1
fps = "FPS: " + str(curr_fps)
curr_fps = 0
# 将文本(FPS)添加到图像(result)上,包括文本内容(text),文本起始位置(org),字体类型(fontFace),字体大小(fontScale),字体颜色(红色),文本线的粗细。
cv2.putText(result, text=fps, org=(3, 15), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.50, color=(255, 0, 0), thickness=2)
cv2.namedWindow("result", cv2.WINDOW_NORMAL)
cv2.imshow("result", result)
# 将检测完成的图片写入out中,生成新的视频
if isOutput:
out.write(result)
# cv2.waitKey(1) 等待1毫秒键盘输入,返回输入值的ASCII值
# 0xFF掩码操作,用于确保只获取低 8 位(即一个字节)的数值,
# cv2.waitKey(1) & 0xFF判断完后,再判断输出结果是否等于 ord('q')
if cv2.waitKey(1) & 0xFF == ord('q'):
break
yolo.close_session()