使用blender进行点云可视化
最近做了一些点云可视化的工作,感觉应该可以经常复用,特此记录一下相关的一些内容。由于使用blender进行可视化需要借助额外的插件,其仅支持ply格式文件,因此本文将从ply格式文件的生成和处理以及配色选择搭配开始介绍,最后再介绍如何使用blender进行点云可视化。
ply文件生成
ply格式
PLY文件格式是Stanford大学开发的一套三维模型数据格式。可以存放顶点,面片,或其他的一种或者多种。举例读取一种简单的,只包含了顶点数据。数据格式如下所示:
ply
format binary_little_endian 1.0 # 编码格式
comment Created by Open3D # 使用的创建工具
element vertex 1000 # 顶点元素数量
property double x # 下列皆为顶点属性
property double y
property double z
property uchar red
property uchar green
property uchar blue
end_header # 数据头结束符 后接二进制数据
数据格式转换
理论上,按照这个格式可以自己通过编辑文本的方式生成ply文件,但是为了使用和维护的方便,我们选择使用Open3D
库进行处理.
从数据格式看出,对于点云数据,我们只需要获得其x,y,z
坐标以及rgb
颜色值即可。由于我们本次拿到的初始文件格式为x,y,z,label
的形式,只需要将label转化为相应的rgb
值即可。
这里,由于label数量较多,人工选择色彩容易出现易混淆的情况,这个问题在stackoverflow上已经有很多讨论了,这里我们直接从GitHub上找到了一个自动生成n种区分度较大的颜色的项目,为n种标签生成区分度较大的配色方案,代码如下:
# get_n_colors.py
import colorsys
import random
import matplotlib.pyplot as plt
# hls格式色彩生成算法
def get_n_hls_colors(num):
hls_colors = []
i = 0
step = 360.0 / num
while i < 360:
h = i
s = 90 + random.random() * 10
l = 50 + random.random() * 10
_hlsc = [h / 360.0, l / 100.0, s / 100.0]
hls_colors.append(_hlsc)
i += step
return hls_colors
# generate colors for ply file
def ncolors(num, rgb=False):
"""
num: points number
rgb: hls color by Default
return: hls or rgb color in [0,1]
"""
if num < 1:
return rgb_colors
hls_colors = get_n_hls_colors(num)
if rgb:
return [colorsys.hls_to_rgb(h, l, s) for h, l, s in hls_colors]
else:
return hls_colors
if __name__ == '__main__':
n = 13
rgb_colors = ncolors(n, rgb=True)
col_map = {}
for i in range(len(rgb_colors)):
col_map[i] = [x for x in rgb_colors[i]]
print(col_map) # 输出得到的color map
x = [x for x in range(n)]
y = [1]*n
# 使用plt简单查看颜色显示效果
plt.bar(x, y, color=rgb_colors) # 这里只能正确显示rgb颜色!
plt.show()
其结果如下:

需要注意的是,这里返回的颜色值都在[0,1]区间内,当时没注意到这一点,增加了很多调试的时间。但是这也是符合后续ply文件需求的(颜色值需要在[0,1]区间内)。
格式转换与ply文件生成
这一步其实相对简单,只需要根据上述生成的标签和颜色的对应关系,使用Open3D
提供的一些api直接操作就可以了,代码如下:
from asyncore import read
import open3d as o3d
import numpy as np
import os
col_map = {0: [0.968598945752023, 0.21265177717633077, 0.21265177717633077], 1: [0.9593424395567822, 0.43938701079007103, 0.04942043921503769], 2: [0.9564083671463547, 0.8441574631810681, 0.17065203938934836], 3: [0.7119784414524474, 0.9707467808994562, 0.06505759283492496], 4: [0.4216596845499031, 0.9746740070101805, 0.20045395556579249], 5: [0.09410395585610232, 0.9975270445459243, 0.22316439709750563], 6: [0.08875664925141424, 0.955616406724503, 0.5841050820931795], 7: [0.22495255248804136, 0.9708864249338678, 0.9708864249338685], 8: [0.15853756200060765, 0.6307409064043225, 0.9848934147071097], 9: [0.08291138196093573, 0.20910827660750841, 0.9662896444869518], 10: [0.3989335698651475, 0.17449579837215634, 0.9600279985976244], 11: [0.7194960807027336, 0.07306822754967035, 0.9780672219639588], 12: [0.9692772940469393, 0.05250565523033324, 0.8383099170731375], 13: [0.9881871645219139, 0.04214938209626784, 0.4475941459929724]}
def txt2ply(txt_path, ply_path, label=False):
"""
Convert txt file to ply file.
label = True: the last column is label: x y z label
label = False: the last column is color: x y z r g b
"""
with open(txt_path, 'r') as f:
lines = f.readlines()
points = []
colors = []
if label:
for line in lines:
line = line.strip().split()
points.append([float(line[0]), float(line[1]), float(line[2])])
colors.append(col_map[int(line[3])])
#colors.append(col_map[int(float(line[3]))])
else:
for line in lines:
line = line.strip().split()
points.append([float(line[0]), float(line[1]), float(line[2])])
colors.append([float(line[3])/255, float(line[4])/255, float(line[5])/255])
# for line in lines:
# line = line.strip().split()
# points.append([float(line[0]), float(line[1]), float(line[2])])
# colors.append(col_map[int(float(line[3]))])
# print(int(float(line[3])))
# print(colors)
points = np.array(points)
mean_x, mean_y, mean_z = np.mean(points, axis=0)
# move to center
points[:, 0] -= mean_x
points[:, 1] -= mean_y
points[:, 2] -= mean_z
# print(points)
colors = np.array(colors)
# print(colors)
write_ply(ply_path, points, colors)
def write_ply(ply_path, points, colors):
"""
Write ply file.
"""
pcd = o3d.geometry.PointCloud()
# print(colors)
pcd.points = o3d.utility.Vector3dVector(points)
pcd.colors = o3d.utility.Vector3dVector(colors)
o3d.io.write_point_cloud(ply_path, pcd)
def read_ply(ply_path):
"""
Read ply file.
"""
pcd = o3d.io.read_point_cloud(ply_path)
return pcd
def generate_testData(num=1000):
points = np.random.randint(0, 10, size=(num, 3))
labels = np.random.randint(0, 13, size=num)
data = np.hstack((points, labels.reshape(-1, 1)))
with open('testData.txt', 'w') as f:
for line in data:
f.write(' '.join(map(str, line)) + '\n')
if __name__ == '__main__':
data_path = 'data'
save_path = 'ply'
if not os.path.exists(save_path):
os.mkdir(save_path)
for file in os.listdir(data_path):
txt2ply(os.path.join(data_path, file), os.path.join(save_path, file[:-4]+'.ply'), label=False)
为了能够在中心展示点云数据,在24行处将点云进行了平移操作;blender中的点云坐标数值不能太大,一般10左右就可以。使用Open3D
进行初步可视化结果如下:

这里,ply文件的转换就完成了,下面就进行blender的可视化操作
blender可视化
由于我们想要达到一个点云旋转的效果,``Open3d`只能静态展示,因此需要借用blender这类专业3D建模软件。首先我们将介绍如何使用blender可视化一个点云,接着我们再介绍如何实现旋转的效果,最后说明一下如何导出视频.
点云可视化-bpy插件
使用blender进行点云可视化需要借助一个插件bpy,插件的功能和参数非常多,这里我们只需要用到几个比较简单的功能即可
安装
安装主要参考这篇博文,整体过程比较简单。首先需要将bpy插件从GitHub上下载下来,然后在菜单的“编辑->偏好设置->插件->安装”完成,选择需要安装的脚本文件即可,对于bpy插件即选择space_view3d_point_cloud_visualizer.py这个文件即可。然后在blender的右侧菜单就能看到该插件了。
数据导入
首先新建一个blender项目,删除项目初始的cube

然后新建一个空物体

单击新建的空物体,在右侧插件中选择要可视化的ply文件,再点击“自由线”即可绘制出点云

可以调整参数对点云的点大小,透明度等效果进行调节

旋转效果生成
第一步我们已经得到了点云的静态可视化结果,下一步我们就要让点云旋转起来。说是点云旋转,但由于运动是相对的,我们可以通过旋转相机来实现相同的效果。下面我们就来设置相机旋转,主要参考教程的内容。
首先切换到顶视图(菜单栏:视图->视图->顶视图),然后添加一个圆形曲线(菜单栏: 添加->曲线->圆环),按“S”键调整圆环大小,使相机顶点处于圆环之上

调整到前视图,将圆环移动到和相机端点相同高度(快捷键“G”),然后为相机添加“跟随路径”约束(在右侧菜单栏),目标就选择刚才设置的曲线,并同时点击“动画路径”按钮。此时相机会偏移到较远的位置,找到相机之后进行坐标归零操作(alt+G),操作完成后效果如图,此时相机已经可以跟随圆环选择,但是视角不正确

接下来就是调整视角的操作。首先将鼠标移动到窗口右上角,点击并拖动可以新建一个工作窗口,其中一个窗口进入相机的活动视角(右键相机:设置活动摄像机)。再为相机添加一个标准约束,目标就选择空物体即可,这样就完成了初步的设置,再调整一下相机成像窗口的大小即可。最终效果如下图所示,根据这种方法还可以调节速度,定义其他更复杂的轨迹,但是目前展示没有用到,因此先到这一步即可。

视频导出和生成
由于blender无法渲染空物体,因此视频的导出需要借助插件本身来完成。点击插件的“渲染->animation”,即可生成相机视角的帧序列。此时生成的png图像默认为透明背景,导出视频后为黑色背景,因此我们先将其转换为jpg格式,即默认为白色背景:for i in *.png; do sips -s format jpeg $i --out ${i%.*}.jpg;done;
然后,我们借助ffmpeg
工具即可将所有帧生成一个完整的视频,命令为:ffmpeg -threads 8 -i pcv_render_%03d.jpg output.mp4
,最后效果如下: