这里主要是utils下的部分文件的补充注释
config.py
from pprint import pprint#打印出来更美观
class Config:
#data
voc_data_dir='/home/wrc/yuyijie/KITTI/VOCdevkit/VOC2007'
min_size=600
max_size=1000
num_works=8
test_num_works=8
rpn_sigma=3.
roi_sigma=1.
# for optimizer
wd=0.0005
lr_decay=0.1
lr=1e-3
#vis
env='faster-rcnn'
port=8097#visdom 端口
plot_every=40
#preset
data='voc'
pretrained_model='vgg16'
epoch=14
use_adam=False
use_chainer=False
use_drop=False
#debug
debug_file='/tmp/debugf'
test_num=10000
#model
load_path=None
caffe_pretrain=False
caffe_pretrain_path='checkpoints/vgg16_caffe.pth'
def _parse(self,kwargs):#解析并设置用户设定的参数
state_dict=self._state_dict()#读取Config类所有参数dict{para_name:para_value}
for k,v in kwargs.items():#遍历用户传来的dict
if k not in state_dict:
raise ValueError('Unknow option:"--%s"'%k)
setattr(self,k,v)#设置参数
print('=============user config=========')
pprint(self._state_dict())#打印参数
print('=============end=========')
def _state_dict(self):
return {k:getattr(self,k) for k ,_ in Config.__dict__.items() if not k.startswith('_')}
#字典解析,字典解析,Config.__dict__.items() 取出类中所有的函数、全局变量以及一些内置的属性
# 前面我们设定的都是全局变量(键值对:比如min_size = 600),没有函数,而系统内置属性都是_打头的,
# 所以我们要not k.startswith('_') 返回结果dict{para_name0:para_value0,para_name1:para_value1,....}
opt=Config()#创建config对象
array_tool.py
import torch as t
import numpy as np
def tonumpy(data):#将数据转化为Numpy
if isinstance(data,np.ndarray):#如果数据是numpy类型 直接返回
return data
if isinstance(data,t.Tensor):
return data.detach().cpu().numpy()#将变量从图中分离(使得数据独立,以后你再如何操作都不会对图,对模型产生影响)
#如果是gpu类型还要转化为cpu,再转化为numpy
def totensor(data,cuda=True):#将数据转化为Tensor或者cuda
if isinstance(data,np.ndarray):
tensor=t.from_numpy(data)
if isinstance(data,t.Tensor):
tensor=data.detach() #隔离变量
if cuda:
tensor=tensor.cuda()
return tensor
def scalar(data):#取出数据的值
if isinstance(data,np.ndarray):
return data.reshape(1)[0]#如果是numpy类型(必须为1个数据 几维都行) 取出这个数据的值
if isinstance(data,t.Tensor):
return data.item()#如果是tensor类型 调用pytorch常用的item方法 取出tensor的值
eval_tool.py
from __future__ import division
from collections import defaultdict
#这里的defaultdict(function_factory)构建的是一个类似dictionary的对象,
# 其中keys的值,自行确定赋值,但是values的类型,是function_factory的类实例,而且具有默认值。
# 比如default(int)则创建一个类似dictionary对象,里面任何的values都是int的实例,
# 而且就算是一个不存在的key, d[key] 也有一个默认值,这个默认值是int()的默认值0.
import itertools
import numpy as np
import six
from model.utils.bbox_tools import bbox_iou
def eval_detection_voc(pred_bboxes,pred_labels,pred_scores,gt_bboxes,gt_labels,gt_difficults=None,
iou_thresh=0.5,use_07_metric=False):
#根据PASCAL VOC evaluation
#所有参数都是list
# test_num张图片(图片数据来自测试数据testdata)的预测框,标签,分数,和真实的框,标签和分数。所有参数都是list
# len(list)=opt.test_num(default=10000)
# pred_boxes: [(A, 4), (B, 4), (C, 4)....共test_num个]
# 输入源gt_数据
# 经过train.predict函数预测出的结果框
# pred_labels[(A,), (B,), (C,)...共test_num个]
# pred_scores同pred_labels
# A, B, C, D是由nms决定的个数,即预测的框个数,不确定。
# gt_bboxes:[(a, 4), (b, 4)....共test_num个]
# a b...是每张图片标注真实框的个数
# gt_labels与gt_difficults同理
prec,rec=calc_detection_voc_prec_rec(#计算每个label类别的准确率和召回率
pred_bboxes,pred_labels,pred_scores,gt_bboxes,gt_labels,gt_difficults,iou_thresh=iou_thresh
)
ap=calc_detection_voc_ap(prec,rec,use_07_metric=use_07_metric)#根据prec和rec计算map和ap
return {'ap':ap,'map':np.nanmean(ap)}
def calc_detection_voc_prec_rec(pred_bboxes,pred_labels,pred_scores,gt_bboxes,gt_labels,gt_difficults=None,iou_thresh=0.5):
pred_bboxes=iter(pred_bboxes)#生成迭代器
pred_labels=iter(pred_labels)#生成迭代器
pred_scores=iter(pred_scores)#生成迭代器
gt_bboxes=iter(gt_bboxes)#生成迭代器
gt_labels=iter(gt_labels)#生成迭代器
if gt_difficults is None:
gt_difficults=itertools.repeat(None)#itertools.repeat生成一个重复的迭代器 None是每次迭代获得的数值
else:
gt_difficults=iter(gt_difficults)
n_pos=defaultdict(int)#defaultdict当key不存在时,dict[key]=default(int)=0 default(list)=[]
score=defaultdict(list)
match=defaultdict(list)
for pred_bbox,pred_label,pred_score,gt_bbox,gt_label,gt_difficult in six.moves.zip(pred_bboxes,pred_labels,
pred_scores,gt_bboxes,gt_labels,gt_difficults):
if gt_difficults is None:
gt_difficults=np.zeros(gt_bbox.shape[0],dtype=bool)#全部设置为非difficult
#遍历一边图片中,所有出现的label
for l in np.unique(np.concatenate((pred_label,gt_label)).astype(int)):
#拼接后返回无重复的从小到大排序的一维numpy 如[2,3,4,5,6]
# 并遍历这个一维数组,即遍历这张图片出现过的标签数字(gt_label+pred_label)
#>>> np.unique([1, 1, 2, 2, 3, 3])
# array([1, 2, 3])
# >>> a = np.array([[1, 1], [2, 3]])
# >>> np.unique(a)
# array([1, 2, 3])
pred_mask_l=pred_label==l#//广播pred_mask_l=[eg. T,F,T,T,F,F,F,T..] 所有预测label中等于L的为T 否则F
pred_bbox_l=pred_bbox[pred_mask_l]#选出label=L的所有pre_box
pred_score_l=pred_scores[pred_mask_l]#label=L 对应所有pre_score
#sort by score
order=pred_score_l.argsort()[::-1]#获得score降序排序索引
pred_bbox_l=pred_bbox_l[order]
pred_score_l=pred_score_l[order]
gt_mask_l=gt_label==l#同理
gt_bbox_l=gt_bbox[gt_mask_l]
gt_difficult_l=gt_difficult[gt_mask_l]
n_pos[l]+=np.logical_not(gt_difficult_l).sum()#对T,F取反求和,统计出difficult=0的个数
score[l].extend(pred_score_l)#score={l:predscore_l,...} extend是针对defaultdict中是list的情况
if len(pred_bbox_l)==0:#没有预测的label=L的box,即真实label有L,我们全没有预测到
continue#跳过这张图片 此时没有对match字典操作,之前score[l].extend操作也为空 保持了match和score的形状一致
if len(gt_bbox_l)==0:#没有真实的label=L的情况 即预测中有L 真实中没有 全都预测错了
match[l].extend((0,)*pred_bbox_l.shape[0])#match{L:[0,0,0...n_pred_box个0]}
# VOC evaluation follows integer typed bounding boxes.
# 作者给的注释是follows integer typed bounding boxes
# 但是只改变了ymax, xmax的值,重要的是这样做并不能转化为整数
# pred_bbox和gt_bbox只
# 参与了IOU计算且后面没有参与其他计算
pred_bbox_l=pred_bbox_l.copy()
pred_bbox_l[:,2:]+=1#ymax,xmax+=1
gt_bbox_l=gt_bbox_l.copy()
gt_bbox_l[:,2:]+=1
iou=bbox_iou(pred_bbox_l,gt_bbox_l)#计算两个box的iou
gt_index=iou.argmax(axis=1)#有len(pred_bbox_l)个索引,第i个索引值n表示gt_box[n]与pred_box[i]iou最大
#比如 4个pred_box 3 个gtbox
# gt0 gt1 gt2 最大对应用*表示 这里的gt_index输出将是[1,0,0,2] 第0个索引值1 表示gt1和pred_box[0]iou最大
# A *
# B *
# C *
# D *
#要注意 这里的gt_index是会重复的 因为同一个gt会与很多pred_box拥有最大iou
# #计算iou 将会是(N,K)纬度的输出,如果所有tl都大于br的话
gt_index[iou.max(axis=1)<iou_thresh]=-1
#这里则是在上面的基础上把小于阈值的gt_index设置为-1
#将gt_box与pred_box iou<thresh的索引值置为-1
# 即针对每个pred_bbox,与每个gt_bbox IOU的最大值 如果最大值小于阀值则置为-1
# 即我们预测的这个box效果并不理想 后续会将index=-1的 matchlabel=0
del iou
selec=np.zeros(gt_bbox_l.shape[0],dtype=bool)
for gt_idx in gt_index:#遍历gt_index索引值
if gt_index>=0:#即iou满足条件的bbox
if gt_difficult_l[gt_idx]:#对应的gt_difficult=1即困难标记
match[l].append(-1)#match[l] 后面追加一个-1
else:
#这里的做法可能有个问题 就是比如gt1同时根B,C两个prebox拥有超过阈值的iou这时候的排序就很关键 ,要先按照分数来进行排序
if not selec[gt_idx]:#没有被选国,select[idx]=0的时候
match[l].append(1)
else:#对应的gt_box已经被选国一次,即已经和前面的某个pre_box iou最大
match[l].append(0)
selec[gt_idx]=True#一个gt被选过之后,则要设置为True
else:#不满足iou>thresh
match[l].append(0)#追加0 很重复匹配的结果一样
#我们注意到上面为每个pred_box都打了label 0, 1, -1
#len(match[l]) = len(score[l]) = len(pred_bbox_l)
for iter_ in ( # 上面的 six.moves.zip遍历会在某一iter遍历到头后停止,由于pred_bboxes等是全局iter对象,
#我们此时继续调用next取下一数据,如果有任一数据不为None,那么说明他们的len是不相等的 有悖常理,数据错误
pred_bboxes,pred_labels,pred_scores,gt_bboxes,gt_labels,gt_difficult):
if next(iter_,None) is not None:#next(iter_,None)表示调用next 如果已经遍历到了头 不抛出异常而是返回None
raise ValueError("Length of input iterables need to be same")
n_fg_class=max(n_pos.keys())+1#有n_fg_class个类
prec=[None]*n_fg_class
rec=[None]*n_fg_class
for l in n_pos.keys():#遍历所有label
score_l=np.array(score[l])
match_l=np.array(match[l],dtype=np.int8)
order=score_l.argsort()[::-1]
match_l=match_l[order]#对应match按照score由大到小排序
tp=np.cumsum(match_l==1)#统计累计match_1=1的个数
# 比如 match_l =[1,1,1,0,1,1] np.cumsum(match)=[1,2,3,3,4,5]
# tp=[1,2,3,3,4,5] fp=[0,0,0,1,1,1] 长度是所有predbox的总数 prec[l]=[1,1,1,0.75,0.8,0.833]
fp=np.cumsum(match_l==0)
prec[l]=tp/(tp+fp)
if n_pos[l]>0:#如果n_pos[l]=0 那么rec[l]=None
rec[l]=tp/n_pos[l]#这里干嘛不用所有的gt[l]的总数
return prec,rec
def calc_detection_voc_ap(prec,rec,use_07_metric=False):
n_fg_class=len(prec)
ap=np.empty(n_fg_class)
for l in six.moves.range(n_fg_class):#遍历每个label
if prec[l] is None or rec[l] is None:#如果为NOne 则ap设置为np.nan
ap[l]=np.nan
continue
if use_07_metric:
# 11 point metric
ap[l]=0
for t in np.arange(0.,1.1,0.1):#t=0 0.1 0.2...1.0
if np.sum(rec[l]>=t)==0:#这个标签召回率没有大于阈值的
p=0
else:
p=np.max(np.nan_to_num(prec[l])[rec[l]>=t])#p=(rec>=t时,对应index:prec中的最大值) np.nan_to_num是为了
#让None=0一边计算
ap[l]+=p/11
else:
mpre=np.concatenate(([0],np.nan_to_num(prec[l]),[0]))#头尾添加0
mrec=np.concatenate(([0],rec[l],[1]))#头添加0 尾添加1
mpre=np.maximum.accumulate(mpre[::-1])[::-1]#获得从小到大的累计最大值
#>>> np.add.accumulate([2, 3, 5])
# array([ 2, 5, 10])
# >>> np.multiply.accumulate([2, 3, 5])
# array([ 2, 6, 30])
# 我们知道
# 我们是按score由高到低排序的
# 而且我们给box打了label
# 0, 1, -1
# score高时1的概率会大,所以pre是累计降序的
# 而rec是累积升序的,那么此时将pre倒序再maxuim.ac
# 获得累积最大值,再倒序后
# 从小到大排序的累积最大值
i=np.where(mrec[1:]!=mrec[:-1])[0]#差位比较,看哪里改变了recall的值,记录index(x轴)
#and sum (\Delta recall)*prec
ap[l]=np.sum((mrec[i+1]-mrec[i])*mrec[i+1])#差值*mpre_max的值,(x轴之差*ymax)
return ap
vis_tool.py
import time
import numpy as np
import matplotlib
import torch as t
import visdom
matplotlib.use('Agg')
from matplotlib import pyplot as plot
# from data.voc_dataset import VOC_BBOX_LABEL_NAMES
VOC_BBOX_LABEL_NAMES = (
'fly',
'bike',
'bird',
'boat',
'pin',
'bus',
'c',
'cat',
'chair',
'cow',
'table',
'dog',
'horse',
'moto',
'p',
'plant',
'shep',
'sofa',
'train',
'tv',
)
def vis_image(img,ax=None):#img是经过你标准化的0-255的rgb图像 ax是传来的matplotlib.axes,Axis对象,告诉我们画在那里
if ax is None:#如果没有这个对象 创建一个(1,1,1)的
fig=plot.figure()
ax=fig.add_subplot(1,1,1)
#CHW-》HWC
img=img.transpose(1,2,0)
ax.imshow(img.astype(np.uint8))#matplotlib 支持0-1的图像显示,也支持0-255的图像显示,转成uint8是为了告诉它我们的图像是0-255的
return ax #返回axis对象后续使用,vis_box会继续调用,在此图片基础上画框等
def vis_bbox(img,bbox,label=None,score=None,ax=None):
label_names=list(VOC_BBOX_LABEL_NAMES)+['bg']
if label is not None and not len(bbox)==len(label):#框个数不等于标签个数
raise ValueError('The length of label must be same as that of bbox')
if score is not None and not len(bbox)==len(score):#score个数不等于标签个数
raise ValueError('The length of score must be same as that of bbox')
ax=vis_image(img,ax=ax)
if len(bbox)==0:#没有框的话
return ax
for i,bb in enumerate(bbox):#遍历一张图中所有的框
xy=(bb[1],bb[0])#左上角
height=bb[2]-bb[0]
width=bb[3]-bb[1]
ax.add_patch(plot.Rectangle(xy,width,height,fill=False,edgecolor='red',linewidth=2))#画矩形
caption=list()
if label is not None and label_names is not None:
lb=label[i]#此框对应的label
if not (-1<=lb<len(label_names)):#label名字超过边界
raise ValueError('No corresponding name is given')
caption.append(label_names[lb])#物体名字加入caption list
if score is not None:
sc=score[i] #找到此框对应分数
caption.append('{:.2f}'.format(sc))#加入caption list
if len(caption)>0:
ax.text(bb[1],bb[0],':'.join(caption),style='italic',bbox={'facecolor':'white','alpha':0.5,'pad':0})#bbox是框住字体的框的一些参数
return ax
def fig2data(fig):#将matplotlib的figure图像 转化为RGBA形式的NUmpy返回
fig.canvas.draw()#画图,渲染 fig.canvas.draw()重绘所有内容。这是你的瓶颈。就你而言,你不需要重新绘制轴边界,刻度标签等等
w,h=fig.canvas.get_width_height()#得到figure的w,h
buf=np.fromstring(fig.canvas.tostring_argb(),dtype=np.uint8)#得到numpy形式的argb图像
buf.shape=(w,h,4)#(w,h,argb)
buf=np.roll(buf,3,axis=2)#(w,h,rgba) 在第二轴水平向右滚动3个距离
return buf.reshape(h,w,4)#(h,w,4)
def fig4vis(fig):#将matplotlib的figure图像转化为pytorch 常用的3通道RGB CHW 0-1图像
ax=fig.get_figure()
img_data=fig2data(ax).astype(np.int32)
plot.close()
return img_data[:,:,:3].transpose((2,0,1))/255
#(H,W,RGBA)-》(H,W,RGB)-》(RGB,H,W)即CHW/255 -》0-1
def visdom_bbox(*args,**kwargs):#我们要使用的函数 包含上面所有的函数调用,传入img box score 画成一张图像 以numpy的形式返回
fig=vis_bbox(*args,**kwargs)
data=fig4vis(fig)
return data
class Visualizer(object): #Visdom可视化部分
def __init__(self, env='default', **kwargs):
self.vis = visdom.Visdom(env=env, use_incoming_socket=False, **kwargs) #visdom对象
self._vis_kw = kwargs #参数
self.index = {}
self.log_text = ''
def reinit(self, env='default', **kwargs):
#可能由于用户要改变kwargs初始化参数 而重新初始化visdom对象
self.vis = visdom.Visdom(env=env, **kwargs)
return self
def plot_many(self, d):#plot
#参数 d: dict (name,value) i.e. ('loss',0.11)
for k, v in d.items():
if v is not None:
self.plot(k, v) #调用下面的plot函数 画折线图(各种损失折线图,训练map折线图)
def img_many(self, d): #参数 d: dict (name,value) i.e. ('loss',0.11)
for k, v in d.items():
self.img(k, v) #调用下面的img函数 画图
def plot(self, name, y, **kwargs): #画一条直线 title=name
x = self.index.get(name, 0)
self.vis.line(Y=np.array([y]), X=np.array([x]),
win=name,
opts=dict(title=name),
update=None if x == 0 else 'append',
**kwargs
)
self.index[name] = x + 1
def img(self, name, img_, **kwargs): #画图片 title=name
self.vis.images(t.Tensor(img_).cpu().numpy(),
win=name,
opts=dict(title=name),
**kwargs
)
def log(self, info, win='log_text'): #打印log日志 时间+info
self.log_text += ('[{time}] {info} <br>'.format(
time=time.strftime('%m%d_%H%M%S'), \
info=info))
self.vis.text(self.log_text, win)
def __getattr__(self, name): #按name获得一个属性
return getattr(self.vis, name)
def state_dict(self): #返回现有参数 dict
return {
'index': self.index,
'vis_kw': self._vis_kw,
'log_text': self.log_text,
'env': self.vis.env
}
def load_state_dict(self, d): #参数 d: dict (name,value) i.e. ('loss',0.11) 初始化参数
self.vis = visdom.Visdom(env=d.get('env', self.vis.env), **(self.d.get('vis_kw')))
self.log_text = d.get('log_text', '')
self.index = d.get('index', dict())
return self
不熟悉的numpy,pytorch,python操作
data.detach()
将tensor变量从图中分离(使得数据独立,以后你再如何操作都不会对图,对模型产生影响)
data.numpy()
tensor2numpy
t.from_numpy(data)
numpy2tensor
data.reshape(1)[0]
如果是numpy类型(必须为1个数据 几维都行) 取出这个数据的值
data.item()
如果是tensor类型 调用pytorch常用的item方法 取出tensor的值
from collections import defaultdict
这里的defaultdict(function_factory)构建的是一个类似dictionary的对象,
其中keys的值,自行确定赋值,但是values的类型,是function_factory的类实例,而且具有默认值。 比如default(int)则创建一个类似dictionary对象,里面任何的values都是int的实例,
而且就算是一个不存在的key, d[key] 也有一个默认值,这个默认值是int()的默认值0.
这在计算map等eval指标的时候非常有用
比如
if not selec[gt_idx]:#没有被选国,select[idx]=0的时候 match[l].append(1)
就能把所有的1都放到l对应的list中 而且即使没有对应的值,该list也会存在只是为空
{‘8’:[1,1,1,1]}这种形式
np.unique
#拼接后返回无重复的从小到大排序的一维numpy 如[2,3,4,5,6]
# 并遍历这个一维数组,即遍历这张图片出现过的标签数字(gt_label+pred_label)
#>>> np.unique([1, 1, 2, 2, 3, 3])
# array([1, 2, 3])
# >>> a = np.array([[1, 1], [2, 3]])
# >>> np.unique(a)
# array([1, 2, 3])
iter(pred_bboxes)#生成迭代器
把一个list可以变成迭代器,相比直接用list去做循环能节省很多内存
迭代器的节省内存的原因:
一个是生成器函数,外表看上去像是一个函数,但是没有用return语句一次性的返回整个结果对象列表,取而代之的是使用yield语句一次返回一个结果。另一个是生成器表达式,类似于上一小节的列表解析,但是方括号换成了圆括号,他们返回按需产生的一个结果对象,而不是构建一个结果列表。
生成器函数他通过yield关键字返回一个值后,还能从其退出的地方继续运行,因此可以随时间产生一系列的值。他们自动实现了迭代协议,并且可以出现在迭代环境中。运行的过程是这样的:生成器函数返回一个迭代器,for循环等迭代环境对这个迭代器不断调用next函数,不断的运行到下一个yield语句,逐一取得每一个返回值,直到没有yield语句可以运行,最终引发StopIteration异常。
np.logical_not()
输入一组bool型的数据
对T,F取反
np.cumsum(match_l==1)#统计累计match_1=1的个数
cumsum(a, axis=None, dtype=None, out=None):
Axis along which the cumulative sum is computed. The default
(None) is to compute the cumsum over the flattened array.
Return the cumulative sum of the elements along a given axis.
比如 match_l =[1,1,1,0,1,1] np.cumsum(match==1)=[1,2,3,3,4,5]
tp=[1,2,3,3,4,5] fp=[0,0,0,1,1,1] 长度是所有predbox的总数 prec[l]=[1,1,1,0.75,0.8,0.833]
Examples
--------
>>> a = np.array([[1,2,3], [4,5,6]])
>>> a
array([[1, 2, 3],
[4, 5, 6]])
>>> np.cumsum(a)
array([ 1, 3, 6, 10, 15, 21])
>>> np.cumsum(a, dtype=float) # specifies type of output value(s)
array([ 1., 3., 6., 10., 15., 21.])
>>> np.cumsum(a,axis=0) # sum over rows for each of the 3 columns
array([[1, 2, 3],
[5, 7, 9]])
>>> np.cumsum(a,axis=1) # sum over columns for each of the 2 rows
array([[ 1, 3, 6],
[ 4, 9, 15]])
"""
np.nan_to_num
Replace NaN with zero and infinity with large finite numbers
--------
>>> np.nan_to_num(np.inf)
1.7976931348623157e+308
>>> np.nan_to_num(-np.inf)
-1.7976931348623157e+308
>>> np.nan_to_num(np.nan)
0.0
>>> x = np.array([np.inf, -np.inf, np.nan, -128, 128])
>>> np.nan_to_num(x)
array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000, # may vary
-1.28000000e+002, 1.28000000e+002])
>>> np.nan_to_num(x, nan=-9999, posinf=33333333, neginf=33333333)
array([ 3.3333333e+07, 3.3333333e+07, -9.9990000e+03,
-1.2800000e+02, 1.2800000e+02])
>>> y = np.array([complex(np.inf, np.nan), np.nan, complex(np.nan, np.inf)])
array([ 1.79769313e+308, -1.79769313e+308, 0.00000000e+000, # may vary
-1.28000000e+002, 1.28000000e+002])
>>> np.nan_to_num(y)
array([ 1.79769313e+308 +0.00000000e+000j, # may vary
0.00000000e+000 +0.00000000e+000j,
0.00000000e+000 +1.79769313e+308j])
>>> np.nan_to_num(y, nan=111111, posinf=222222)
array([222222.+111111.j, 111111. +0.j, 111111.+222222.j])
"""
np.maximum.accumulate(mpre[::-1])[::-1]
获得从小到大的累计最大值