小伙伴们好呀,3D 检测和分割系列文章继续更新啦~在第一篇文章中,我们带领大家了解了整个框架的大致流程。第二篇文章我们给大家解析了 MMDetection3D 中的坐标系和核心组件 Box,今天我们将带大家看看 3D 场景中的可视化组件 Visualizer,如何在多个模态数据上轻松可视化并且自由切换?为什么在可视化的时候经常出现一些莫名其妙的问题?
话不多说,接下来就让我们进入今天的正题吧~
在 MMDetection3D 的官方可视化文档中,我们已经提供了如何进行可视化的一些指令和效果,这篇文章我们将从代码层级介绍可视化的细节过程,带大家一探究竟。
可视化代码解析
在线可视化 Visualizer
MMDetection3D 基于 Open3D 构建了一个在线 Visualizer,用于在有 GUI 界面的情况下提供实时可视化结果,Open3D 提供了非常丰富的功能。相关代码位于 mmdet3d/core/visualizer/open3d_vis.py 。
MMDetection3D 目前只使用了 Open3D 的部分 API 进行可视化,同时也非常容易实现可视化功能的扩展,目前 Visualizer 支持绘制 3D 框的 add_bboxes,绘制点云分类结果的 add_seg_mask,以及统一的 show 显示方法。在这一节中我们不具体介绍 Open3D 的各个 API 的具体作用,而是带大家来看看如何使用 Open3D 的 API 完成需求,包括实现自己的自定义的可视化需求。
#---------------- mmdet3d/core/visualizer/open3d_vis.py ----------------# class Visualizer(object): r"""Online visualizer implemented with Open3d.""" def __init__(self, points, bbox3d=None, save_path=None, points_size=2, point_color=(0.5, 0.5, 0.5), bbox_color=(0, 1, 0), points_in_box_color=(1, 0, 0), rot_axis=2, center_mode='lidar_bottom', mode='xyz'): super(Visualizer, self).__init__() assert 0 <= rot_axis <= 2 # 调用 Open3D 的 API 来初始化 visualizer self.o3d_visualizer = o3d.visualization.Visualizer() self.o3d_visualizer.create_window() # 创建坐标系帧 mesh_frame = geometry.TriangleMesh.create_coordinate_frame( size=1, origin=[0, 0, 0]) self.o3d_visualizer.add_geometry(mesh_frame) # 设定点云尺寸 self.points_size = points_size # 设定点云颜色 self.point_color = point_color # 设定 3D 框颜色 self.bbox_color = bbox_color # 设定 3D 框内点云的颜色 self.points_in_box_color = points_in_box_color self.rot_axis = rot_axis self.center_mode = center_mode self.mode = mode self.seg_num = 0 # 绘制点云 if points is not None: self.pcd, self.points_colors = _draw_points( points, self.o3d_visualizer, points_size, point_color, mode) def add_bboxes(self, bbox3d, bbox_color=None, points_in_box_color=None): """Add bounding box to visualizer.""" if bbox_color is None: bbox_color = self.bbox_color if points_in_box_color is None: points_in_box_color = self.points_in_box_color _draw_bboxes(bbox3d, self.o3d_visualizer, self.points_colors, self.pcd, bbox_color, points_in_box_color, self.rot_axis, self.center_mode, self.mode) def add_seg_mask(self, seg_mask_colors): """Add segmentation mask to visualizer via per-point colorization.""" self.seg_num += 1 offset = (np.array(self.pcd.points).max(0) - np.array(self.pcd.points).min(0))[0] * 1.2 * self.seg_num mesh_frame = geometry.TriangleMesh.create_coordinate_frame( size=1, origin=[offset, 0, 0]) # create coordinate frame for seg self.o3d_visualizer.add_geometry(mesh_frame) seg_points = copy.deepcopy(seg_mask_colors) seg_points[:, 0] += offset _draw_points( seg_points, self.o3d_visualizer, self.points_size, mode='xyzrgb') def show(self, save_path=None): """Visualize the points cloud.""" self.o3d_visualizer.run() if save_path is not None: self.o3d_visualizer.capture_screen_image(save_path) self.o3d_visualizer.destroy_window() return
如上述代码所示,在检测任务中,关于 3D 框的绘制主要是通过调用 add_bboxes 方法,我们可以在绘制 3D 框的时候,通过 bbox_color 和 points_in_box_color 分别设定框的颜色以及包含在框内的点云的颜色,而其最终调用的还是 _draw_bboxes 方法。而 add_seg_masks 则是用来绘制分割结果,其中传入的 seg_mask_colors 是带颜色(rgb)信息的点云。需要注意的是,我们在绘制点云分割结果的时候,不会在原点云上进行颜色的更新,因为如果同时在原点云上绘制预测结果和真值标签往往会有重叠现象,所以我们对每一个分割结果的点云图都会沿 x 轴设置较大的偏移量,单个场景会生成多个分割结果图,而最终对每个分割结果图都会调用 _draw_points 方法进行绘制。当完成所有的绘制之后,就可以调用 show 方法启动 Visualizer,显示场景绘制结果。除此以外,我们也可以很轻松地通过给 Visualizer 添加方法实现自己的需求。
我们可以先来看看如何绘制点云:
#---------------- mmdet3d/core/visualizer/open3d_vis.py ----------------# import open3d as o3d from open3d import geometry def _draw_points(points, vis, points_size=2, point_color=(0.5, 0.5, 0.5), mode='xyz'): # 获取 Open3D Visualizer 的渲染设置,更改点云尺寸 vis.get_render_option().point_size = points_size if isinstance(points, torch.Tensor): points = points.cpu().numpy() points = points.copy() # 构建 Open3D 中提供的 gemoetry 点云类 pcd = geometry.PointCloud() # 如果传入的点云 points 只包含位置信息 xyz # 根据指定的 point_color 为每个点云赋予相同的颜色 if mode == 'xyz': pcd.points = o3d.utility.Vector3dVector(points[:, :3]) points_colors = np.tile(np.array(point_color), (points.shape[0], 1)) # 如果传入的点云 points 本身还包含颜色信息(通常是分类预测结果或者标签) # 直接从 points 获取点云的颜色信息 elif mode == 'xyzrgb': pcd.points = o3d.utility.Vector3dVector(points[:, :3]) points_colors = points[:, 3:6] # 将颜色归一化到 [0, 1] 用于 Open3D 绘制 if not ((points_colors >= 0.0) & (points_colors <= 1.0)).all(): points_colors /= 255.0 else: raise NotImplementedError # 为点云着色 pcd.colors = o3d.utility.Vector3dVector(points_colors) # 将点云加入到 Visualizer 中 vis.add_geometry(pcd) return pcd, points_colors
以上就是绘制点云的过程,当我们了解了如何绘制点云后,我们就可以实现自己的需求了,比如有社区用户希望能够给点云的强度进行染色,当然我们可以直接加在 _draw_points 方法内,但是为了演示更清晰,这里我们可以在 Visiualizer 内实现一个 render_points_intensity 方法做演示:
#---------------- mmdet3d/core/visualizer/open3d_vis.py ----------------# import open3d as o3d from open3d import geometry def render_points_intensity(self, points, vis): # 假设传入的 points 包含 x-y-z-intensity 四个维度 # 自定义一个 colormap colormap = np.array([[128, 130, 120], [235, 0, 205], [0, 215, 0], [235, 155, 0]]) / 255.0 points = points.copy() # 构建 Open3D 中提供的 gemoetry 点云类 pcd = geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(points[:, :3]) points_intensity = points[:, 3] # intensity points_colors = [colormap[int(points_intensity[i]) % colormap.shape[0]] for i in range(points_intensity.shape[0])] pcd.colors = o3d.utility.Vector3dVector(points_colors) # 根据 intensity 为点云着色 # 将点云加入到 Visualizer 中 vis.add_geometry(pcd)
还有比如在社区用户的反馈中,想在可视化时调整场景的背景颜色,我们可以很简单地添加一个 change_bg_color 方法:
def change_bg_color(self, bg_colors): """Change back ground color of Visualizer""" # 获取渲染配置 opt = self.o3d_visualizer.get_render_option() # 将场景背景颜色设为指定颜色 opt.background_color = bg_colors
接下来我们具体来看看如何在点云中绘制 3D 框 _draw_bboxes:
#---------------- mmdet3d/core/visualizer/open3d_vis.py ----------------# def _draw_bboxes(bbox3d, vis, points_colors, pcd=None, bbox_color=(0, 1, 0), points_in_box_color=(1, 0, 0), rot_axis=2, center_mode='lidar_bottom', mode='xyz'): in_box_color = np.array(points_in_box_color) for i in range(len(bbox3d)): center = bbox3d[i, 0:3] dim = bbox3d[i, 3:6] yaw = np.zeros(3) yaw[rot_axis] = -bbox3d[i, 6] # 在 Open3D 中需要将 yaw 朝向角转为旋转矩阵 rot_mat = geometry.get_rotation_matrix_from_xyz(yaw) # 底部中心点转为几何中心店 if center_mode == 'lidar_bottom': center[rot_axis] += dim[ rot_axis] / 2 elif center_mode == 'camera_bottom': center[rot_axis] -= dim[ rot_axis] / 2 box3d = geometry.OrientedBoundingBox(center, rot_mat, dim) line_set = geometry.LineSet.create_from_oriented_bounding_box(box3d) line_set.paint_uniform_color(bbox_color) # 在 Visualizer 中绘制 Box vis.add_geometry(line_set) # 更改 3D Box 中的点云颜色 if pcd is not None and mode == 'xyz': indices = box3d.get_point_indices_within_bounding_box(pcd.points) points_colors[indices] = in_box_color # 更新点云颜色 if pcd is not None: pcd.colors = o3d.utility.Vector3dVector(points_colors) vis.update_geometry(pcd)
需要注意 MMDetection3D 中在 3D 视角可视化时的 bbox3d 是 (x, y, z, x_size, y_size, z_size, yaw) 格式的,同时在 3D 视角可视化的时候都是会被转换到深度坐标系,而且 x, y, z 表示的是底部中心点位置,而并不是几何中心点位置。所以在用 Open3D 可视化的时候需要从底部中心点转换为几何中心点。
使用 MeshLab 可视化
对于 MeshLab 来说,可视化需要提供相应的 obj 文件,文件内包含点云信息、分割结果、检测结果等等。而在目前 MMDetection3D 中,我们提供下述方法,可以将模型输出结果转换为 obj 文件。
#---------------- mmdet3d/core/visualizer/show_result.py ----------------# def _write_obj(points, out_filename): """Write points into ``obj`` format for meshlab visualization.""" N = points.shape[0] fout = open(out_filename, 'w') # 保存点云的时候如果有颜色信息也会进行保存 for i in range(N): if points.shape[1] == 6: c = points[i, 3:].astype(int) fout.write( 'v %f %f %f %d %d %d\n' % (points[i, 0], points[i, 1], points[i, 2], c[0], c[1], c[2])) else: fout.write('v %f %f %f\n' % (points[i, 0], points[i, 1], points[i, 2])) fout.close() def _write_oriented_bbox(scene_bbox, out_filename): """Export oriented (around Z axis) scene bbox to meshes.""" def heading2rotmat(heading_angle): rotmat = np.zeros((3, 3)) rotmat[2, 2] = 1 cosval = np.cos(heading_angle) sinval = np.sin(heading_angle) rotmat[0:2, 0:2] = np.array([[cosval, -sinval], [sinval, cosval]]) return rotmat # 将 MMDetection3D 内部格式的 3D 框转换为 trimesh 格式 def convert_oriented_box_to_trimesh_fmt(box): ctr = box[:3] lengths = box[3:6] trns = np.eye(4) trns[0:3, 3] = ctr trns[3, 3] = 1.0 trns[0:3, 0:3] = heading2rotmat(box[6]) box_trimesh_fmt = trimesh.creation.box(lengths, trns) return box_trimesh_fmt if len(scene_bbox) == 0: scene_bbox = np.zeros((1, 7)) # 利用 trimesh 构建场景 scene = trimesh.scene.Scene() for box in scene_bbox: scene.add_geometry(convert_oriented_box_to_trimesh_fmt(box)) mesh_list = trimesh.util.concatenate(scene.dump()) # 保存为 obj 文件 trimesh.io.export.export_mesh(mesh_list, out_filename, file_type='obj') return
_write_points 和 _write_oriented_bbox 分别用来保存点云(及其分割结果)和 3D Box 为 obj 文件,在实际使用的时候只需要将生成的 obj 文件导入到 MeshLab 即可。
文章来源:【OpenMMLab】
2022-04-25 18:04