提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
【6PACK全记录】6-PACK论文学习及复现记录
一、相关函数
1.1 compute_RT_degree_cm_symmetry
此函数根据真实位姿和预测位姿,计算方向误差Rerr、平移误差Terr并打包成result输出。
输入参数:
- RT_1、RT_2:预测和实际位姿
- class_id:类别id
- handle_visibility:是否考虑马克杯的杯柄,若不考虑则mug也是旋转对称的
- synset_names:类别名称['bottle','bowl'....]
代码实现如下
def compute_RT_degree_cm_symmetry(RT_1, RT_2, class_id, handle_visibility, synset_names):
if RT_1 is None or RT_2 is None:
return 10000,10000
try:
assert np.array_equal(RT_1[3, :], RT_2[3, :])
assert np.array_equal(RT_1[3, :], np.array([0, 0, 0, 1]))
except AssertionError:
return 10000,10000
R1 = RT_1[:3, :3] / np.cbrt(np.linalg.det(RT_1[:3, :3]))#预测R矩阵,正交矩阵行列式是±1,再取立方根
T1 = RT_1[:3, 3]
R2 = RT_2[:3, :3] / np.cbrt(np.linalg.det(RT_2[:3, :3]))
T2 = RT_2[:3, 3]
if synset_names[class_id] in ['bottle', 'can', 'bowl']:#旋转对称性的类别---柱坐标
y = np.array([0, 1, 0])
y1 = R1 @ y
y2 = R2 @ y
theta = np.arccos(y1.dot(y2) / (np.linalg.norm(y1) * np.linalg.norm(y2)))
#可能有问题,调试时出现过nan
#Rerr用角度
elif synset_names[class_id] == 'mug' and handle_visibility==0:#不考虑马克杯的杯柄,则mug也是旋转对称的
y = np.array([0, 1, 0])
y1 = R1 @ y
y2 = R2 @ y
theta = np.arccos(y1.dot(y2) / (np.linalg.norm(y1) * np.linalg.norm(y2)))
elif synset_names[class_id] in ['phone', 'eggbox', 'glue']:
y_180_RT = np.diag([-1.0, 1.0, -1.0])
R = R1 @ R2.transpose()
R_rot = R1 @ y_180_RT @ R2.transpose()
theta = min(np.arccos((np.trace(R) - 1) / 2),
np.arccos((np.trace(R_rot) - 1) / 2))
else:
R = R1 @ R2.transpose()
theta = np.arccos((np.trace(R) - 1) / 2)
theta *= 180 / np.pi#转为°
shift = np.linalg.norm(T1 - T2)#平移误差
result = np.array([theta, shift])
return result
1.2 compute_3d_iou_new
函数计算并输出真实三维边界框与预测边界框之间的体积重叠百分比(小数)IoU。
IoU(交并比)相关知识参见关于目标检测中的IoU是什么
注意,该函数中嵌套定义了asymmetric_3d_iou(),asymmetric_3d_iou的实际调用是在定义它之后的if块中实现的
输入参数:
- RT_1, RT_2:预测、真实位姿
- noc_cube_1, noc_cube_2:预测、真实边界框(物体世界系坐标)
- handle_visibility:是否考虑马克杯的杯柄,若不考虑则mug也是旋转对称的
- class_name_1, class_name_2:类别名
该函数中定义了一个asymmetric_3d_iou()函数,实际的计算操作实在asymmetric_3d_iou中进行的。asymmetric_3d_iou()部分代码如下:
def asymmetric_3d_iou(RT_1, RT_2, noc_cube_1, noc_cube_2):
#根据不同RT推当前帧在相机坐标系下的边框坐标
bbox_3d_1 = transform_coordinates_3d(noc_cube_1, RT_1)#预测
bbox_3d_2 = transform_coordinates_3d(noc_cube_2, RT_2)#真实
bbox_1_max = np.amax(bbox_3d_1, axis=0)#边框中每一列最大值
bbox_1_min = np.amin(bbox_3d_1, axis=0)#边框中每一列最小值
bbox_2_max = np.amax(bbox_3d_2, axis=0)
bbox_2_min = np.amin(bbox_3d_2, axis=0)
overlap_min = np.maximum(bbox_1_min, bbox_2_min)#最小值中的大者
overlap_max = np.minimum(bbox_1_max, bbox_2_max)#最大值中的小者,则overlap确定了二者的交集
# intersections and union
if np.amin(overlap_max - overlap_min) <0:
intersections = 0
else:
intersections = np.prod(overlap_max - overlap_min)#prod:所有元素相乘
union = np.prod(bbox_1_max - bbox_1_min) + np.prod(bbox_2_max - bbox_2_min) - intersections
overlaps = intersections / union#重叠部分在并集中的占比
return overlaps
上述函数的实际调用过程如下:
symmetry_flag = False
if (class_name_1 in ['bottle', 'bowl', 'can'] and class_name_1 == class_name_2) or (class_name_1 == 'mug' and class_name_1 == class_name_2 and handle_visibility==0):
bbox_3d_2 = transform_coordinates_3d(noc_cube_2, RT_2)#真实边框
def y_rotation_matrix(theta):#只是定义,实际调用在下面
#根据角度theta构造旋转矩阵R
return np.array([[np.cos(theta), 0, np.sin(theta), 0],
[0, 1, 0 , 0],
[-np.sin(theta), 0, np.cos(theta), 0],
[0, 0, 0 , 1]])
n = 20#2pi分20份
max_iou = 0#最后输出的占比
for i in range(n):
rotated_RT_1 = RT_1@y_rotation_matrix(2*math.pi*i/float(n))
#将预测的RT做小幅度旋转,为了容错
max_iou = max(max_iou,
asymmetric_3d_iou(rotated_RT_1, RT_2, noc_cube_1, noc_cube_2))
else:
max_iou = asymmetric_3d_iou(RT_1, RT_2, noc_cube_1, noc_cube_2)
return max_iou
与直接调用asymmetric_3d_iou()相比,这种设置的意义在于给预测的位姿RT做了20次小幅度调整,取20次中体积重叠比最大的一个,容错率更大
1.3 transform_coordinates_3
该代码通过给定的位姿RT和物体世界系坐标,推算相机系的坐标。代码实现如下:
def transform_coordinates_3d(coordinates, RT):
"""
Input:
coordinates: [3, N]
RT: [4, 4]
Return
new_coordinates: [3, N]
"""
assert coordinates.shape[0] == 3
coordinates = np.vstack([coordinates, np.ones((1, coordinates.shape[1]), dtype=np.float32)])#【4,8】最后一行全是1
#列不变,按行拼接,只为了满足矩阵乘的尺寸条件,拼接的内容不重要
new_coordinates = RT @ coordinates
new_coordinates = new_coordinates[:3, :]/new_coordinates[3, :]
return new_coordinates
二、main()
参数定义:
def main():
all_score = []#每个类别+overall的5°5指标
all_score25 = []#每个类别+overall的IoU25指标
all_rot_err = []#每个类别+overall的Rerr指标
all_trans_err = []#每个类别+overall的Terr指标
以上4个参数对应论文中的指标表格,每一项前6组是各类别指标,最后一组是所有评估次数、所有类别的平均指标,如下图所示:
各指标说明如下:
以下占比均为类别中满足该指标条件的帧数在该类别总帧数中的比例
指标 | 说明 |
---|---|
5°5cm | 满足方向误差<5°,平移误差<5 cm的帧百分比 |
IoU25 | 预测与地面真实三维边界框之间的体积重叠百分比大于25% |
Rerr | 方向误差的平均值(以度为单位) |
Terr | 平移误差的平均值(以厘米为单位) |
此后代码从外向内可以分为4个循环:评估次数循环(由pred_list = [1,...]
列表决定循环次数,取TEST_list值文件评估)、类别循环、实例循环、帧循环(实例出现过的帧)。
由各指标的说明可以看到,指标统计主要在帧循环中进行。故以下重点注解帧循环,其余循环简述。
2.1 评估次数循环
for exp in pred_list:
print(exp)#第几次评估的结果
all_score.append([])
all_score25.append([])
all_rot_err.append([])
all_trans_err.append([])
pred_data_dir = pred_data + str(exp)#评估结果地址
score = 0#overalll 5°5指标(平均)
score_25 = 0#overalll IoU25指标(平均)
rot_err = 0#overalll Rerr指标(平均)
trans_err = 0#overalll Terr指标(平均)
该部分定义指标变量,为后续输出做准备。然后进入类别循环。
2.2 类别循环
#--------------类别循环---------
for cls_idx in [1,2,3,4,5,6]:
cls_num = 0#该类别涉及的图片帧数
cls_test_num = 0
cls_in_5_5 = 0
cls_iou_25 = 0
cls_path = os.path.join(data_dir, "data_list/real_val", str(cls_idx))#该类别测试的实例名所在地址
cls_rot = []
cls_trans = []
model_list = glob.glob(os.path.join(cls_path, "*"))#所有实例名list
其中cls_in_5_5、cls_iou_25、cls_rot、cls_trans分别为该类别满足5°5cm、IoU25指标的帧数,和各帧的Rerr、Terr。
cls_test_num参数根据后续分析应该是实际计算了各指标的类别帧数,但没有实际作用。
此后进入实例循环,在实例循环结束后,将统计该类别的指标并记录如下:
#--------实例循环结束---------
all_score[-1].append(cls_in_5_5/cls_num)#当前类别5°5指标
all_score25[-1].append(cls_iou_25/cls_num)#当前类别IoU25指标
all_rot_err[-1].append(np.mean(cls_rot))#当前类别Rerr指标
all_trans_err[-1].append(np.mean(cls_trans))##当前类别Terr指标
score = score + (cls_in_5_5/cls_num)/6#overalll 5°5指标(平均)
score_25 = score_25 + (cls_iou_25/cls_num)/6
rot_err = rot_err + np.mean(cls_rot)/6
trans_err = trans_err + np.mean(cls_trans)/6
#---------类别循环结束--------
可以看到5°5cm、IoU指标是百分比形式,而Rerr、Terr是以均值形式存储的。不带"all_"前缀的指标是所有循环的均值,将在所有循环结束后并入"all_"前缀指标。
2.3 实例循环
#--------------实例循环----------
for model_path in model_list:
scene_his = ""#此前处理的视频
model_num = 0
scene_num = 0#当前视频的帧数
scene_in = 0#当前视频中满足5°5cm条件的帧数
model_name = model_path.split("/")[-1]#实例名
print(model_name)
if not model_name in score_dict:
score_dict[model_name] = {}
list_path = os.path.join(model_path, "list.txt")#实例文件夹中的list文件,存该实例出现过的图片的地址
这里有一个问题:model_num 从后面的代码 pred_path = os.path.join(pred_data_dir, "temp_" + str(cls_idx), model_name + "_" + scene + "_" + str(model_num) + "_pose.txt")
来看是用来组成预测位姿文件地址的,但对于不同的视频scene,model_num没有复位的操作。考虑在if scene != scene_his:
代码块中添加model_num =0
但在实际运行时,发现eval.py在保存时同时记录了整体的视频帧数而没有将不同视频分开处理,这里的model_num是这个数而非视频的帧序号,故无需修改
此后进入帧循环。
2.4 帧循环
with open(list_path, 'r') as list_file:
#-------------帧循环----------
for img_path in list_file:
img_path = os.path.join(data_dir, "data", img_path)#实例出现过的图片的地址
img_path = img_path.replace("\n", "")
scene = img_path.split("/")[-2]#取自视频的id
#nocs_gt_path地址中文件存储实例实际的RT
nocs_gt_path = os.path.join(data_dir, "data", "gts", "real_test", "results_real_test_" + scene + "_" + img_path.split("/")[-1] + ".pkl")
if not os.path.exists(nocs_gt_path):
continue
cls_num = cls_num + 1
该部分代码确定处理帧的地址img_path、该帧中所有实例的真实位姿文件地址nocs_gt_path、该帧取自的视频id scene。并将类别总帧数cls_num加1。
#保存不同视频的相关信息(后续如果有错误,可能在此修改)
if scene != scene_his:#换了一个视频处理,可能是初始,也可能是1个视频处理完了
if scene_his != "":
if not scene_his in score_dict[model_name]:
score_dict[model_name][scene_his] = {'temp':0,'score':0}
if scene_in/scene_num > score_dict[model_name][scene_his]['score']:
score_dict[model_name][scene_his]['score'] = scene_in/scene_num
score_dict[model_name][scene_his]['temp'] = exp
scene_his = scene
scene_num = 0
scene_in = 0
该部分保存了不同视频的信息,当scene与此前处理的视频id scene_his不同时,说明来自新的视频,需要将scene_his视频5°5cm指标存储在score_dict中。
#为读取实例预测的位姿做准备
pred_path = os.path.join(pred_data_dir, "temp_" + str(cls_idx), model_name + "_" + scene + "_" + str(model_num) + "_pose.txt")
#eval.py得到的位姿RT以及误差score的地址
model_num = model_num + 1
scene_num = scene_num + 1
if not os.path.exists(pred_path):
print(pred_path)
continue
#为读取gts文件中实例真实位姿做准备
obj_path = img_path + "_meta.txt"#图片中出现的实例列表
ins_id = -1#实例在图片中的掩码id
num_idx = 0#目标实例在图片中的idx
with open(obj_path, "r") as obj_f:
for line in obj_f:
if int(line.split(" ")[1]) == cls_idx and line.split(" ")[-1].replace("\n","") == model_name:
ins_id = int(line.split(" ")[0])#实例在图片中的掩码idx
break
num_idx = num_idx + 1
if ins_id == -1:
continue
该部分代码为读取真实位姿和预测位姿做准备。得到预测位姿文件的地址pred_path,目标实例在nocs_gt_path文件中的idx。
#读该实例实际位姿
with open(nocs_gt_path, 'rb') as f:
result = cPickle.load(f)#字典{RT:,img_path}是该图片帧中每个实例的位姿RT
gt_pose = result['gt_RTs'][num_idx]#【4,4】
gt_pose[:3,3] = gt_pose[:3,3] * 1000#对T调整
#读模型预测的位姿
with open(pred_path, "r") as pred_f:#eval.py结果
pred_pose = []
for i in range(3):
pred_pose.append([])
new_line = pred_f.readline()
for j in range(3):
pred_pose[i].append(float(new_line.split(" ")[j]))
new_line = pred_f.readline()
for i in range(3):
pred_pose[i].append(float(new_line.split(" ")[i]))
pred_pose.append([0.0,0.0,0.0,1.0])#【4,4】
z_180_RT = np.zeros((4, 4), dtype=np.float32)
z_180_RT[:3, :3] = np.diag([-1, -1, 1])
z_180_RT[3, 3] = 1#对角是-1,-1,1,1
pred_pose = z_180_RT @ pred_pose#矩阵乘
gt_pose = np.array(gt_pose)
pred_pose = np.array(pred_pose)
该部分读取实例真实位姿gt_pose和预测位姿pred_pose,均为4*4的矩阵,矩阵最后一行为[0,0,0,1]。至此,可以开始通过两个位姿来统计指标。
#-------开始统计各指标值------
result = compute_RT_degree_cm_symmetry(pred_pose, gt_pose, cls_idx, 1, synset_names)
#result--Rerr、Terr
bbox = np.loadtxt(data_dir + "/model_scales/" + model_name + ".txt").transpose()#8个边框点【3,8】
miou = compute_3d_iou_new(gt_pose, pred_pose, bbox, bbox, 1, synset_names[cls_idx], synset_names[cls_idx])
#IoU预测与地面真实三维边界框之间的体积重叠百分比
cls_test_num = cls_test_num + 1
#实际计算了各指标的类别帧数
if miou > 0.25 and result[0] < 360:
cls_rot.append(result[0])#类别Rerr
if miou > 0.25:
cls_trans.append(result[1])#类别Terr
if miou > 0.25:
cls_iou_25 = cls_iou_25 + 1#Iou>25%的类别帧数加1
if result[0] < 5 and result[1] < 50:
scene_in = scene_in + 1#视频内满足5°5cm条件的帧数
cls_in_5_5 = cls_in_5_5 + 1#5°5类别帧数加1
该部分代码完成指标统计, result = compute_RT_degree_cm_symmetry(pred_pose, gt_pose, cls_idx, 1, synset_names)
调用compute_RT_degree_cm_symmetry()函数得到该帧方向误差和平移误差result=[Rerr,Terr]。
miou = compute_3d_iou_new(gt_pose, pred_pose, bbox, bbox, 1, synset_names[cls_idx], synset_names[cls_idx])
调用compute_3d_iou_new()函数,计算真实三维边界框与预测边框之间的体积重叠百分比,得到的mious是一个小数。
此后的几段if
语句通过mious、result统计满足各指标条件的帧数。
帧循环结束后,需要将最后一次操作的视频的相关信息保存,故在帧循环外,又重写了一遍下述代码
#-------帧循环结束-----------
if not scene_his in score_dict[model_name]:
score_dict[model_name][scene_his] = {'temp':0,'score':0}#补充最后一个视频的字典
if scene_in/scene_num > score_dict[model_name][scene_his]['score']:
score_dict[model_name][scene_his]['score'] = scene_in/scene_num#该视频中满足5°5的帧数占比
score_dict[model_name][scene_his]['temp'] = exp
2.5 输出说明
#---------类别循环结束--------
#输出此评估次数所有类别的平均指标(每次评估输出1组)
print("********************************************************")
print("5cm 5degree:",score*100)
print("IoU 25: ",score_25*100)
print("rot error: ",rot_err)
print("tran error: ",trans_err/10)
all_score[-1].append(score)
all_score25[-1].append(score_25)
all_rot_err[-1].append(rot_err)
all_trans_err[-1].append(trans_err)
该部分在属于评估次数循环,故每一次评估,都会输出1组。输出内容为6个类别的4个指标的平均值(按指标分组),输出样例:
为了便于查看各类别的指标值,在这部分后添加了如下代码:
#输出每个类别的指标,便于绘表
print("********************************************************")
print(exp," Class")
print("5cm 5degree:",np.array(all_score[0][:-1])*100)
print("IoU 25: ",np.array(all_score25[0][:-1])*100)
print("rot error: ",all_rot_err[0][:-1])
print("tran error: ",np.array(all_trans_err[0][:-1])/10)
输出样例:
#----------评估次数循环结束-------
#输出平均指标(是所有测试的平均,只输出1组)
print("********************************************************")
print("Mean 5cm 5degree:",np.mean(np.array(all_score)*100,0)[-1])
print("Mean IoU 25: ",np.mean(np.array(all_score25)*100,0)[-1])
print("Mean rot error: ",np.mean(np.array(all_rot_err),0)[-1])
print("Mean tran error: ",np.mean(np.array(all_trans_err)/10,0)[-1])
#print(score_dict)
print("********************************************************")
该部分代码在所有循环结束后,故只输出1组。输出内容为所有评估次数、所有类别的各指标的均值。
输出样例:
三、问题:
根据github中的提问:issue21确定问题产生原因是部分*_meta.txt文件中有图片中实际不存在的实例,为解决这个问题,修改benchmark.py的代码,实现删除_meta文件中多余的(没有在掩码中出现过)的实例行。
修改代码如下:
#为读取gts文件中实例真实位姿做准备
obj_path = img_path + "_meta.txt"#图片中出现的实例列表
mask_path=img_path+"_mask.png"
mask_im=cv2.imread(mask_path)[:,:,2]#读取mask.png的各掩码id
mask_im=np.array(mask_im)
inst_ids=np.unique(mask_im)
ins_id = -1#实例在图片中的掩码id
num_idx = 0#目标实例在图片中的idx
with open(obj_path, "r") as obj_f:
for line in obj_f:
if int(line.split(" ")[1]) == cls_idx and line.split(" ")[-1].replace("\n","") == model_name:
ins_id = int(line.split(" ")[0])#实例在图片中的掩码idx
break
if int(line.split(" ")[0]) in inst_ids:
num_idx=num_idx+1
#num_idx = num_idx + 1
if ins_id == -1:
continue
解决思路:我们的数据集中*_mask.png图实际是RGB图,其中背景均为[255,255,255]纯白;n个实例的近黑色掩码并不为[0,0,0],而是[1,1,1]、[2,2,2]…[n,n,n],且像素取值与*_meta.txt文件中的实例id是对应的。
利用这个信息,可以在计数num_idx时,判断*_meta.txt的某一行是不是真的在图片中有对应,如果有,再累加num_idx,否则跳过改行。