在Open3D
中,通过read
方法,我们可以读取不同格式的点云数据,那么,在不使用Open3D
的相关接口时,我们就需要自己重写文件读入、加载、渲染展示方法,效果如下:
点云文件读入
首先,我们要绑定事件:
main.py
中启动点云读入线程
def point_cloud_read_thread_start(self, file_path):if file_path.endswith('.las') or file_path.endswith('.ply') or file_path.endswith('.txt'):self.point_cloud_read_thread.set_file_path(file_path) # 传递文件名self.point_cloud_read_thread.start() # 线程读取文件else:QtWidgets.QMessageBox.warning(self, 'Warning', "Only support file endwith '.txt', '.ply', '.las'")
在pointcloud.py
文件中我们可以看到,其继承了QThread
,并重写了run
方法,当我们的线程启动时,其会执行run
方法的内容,
from multiprocessing import Poolclass PointCloudReadThread(QThread):message = pyqtSignal(str, int)tag = pyqtSignal(bool)def __init__(self):super(PointCloudReadThread, self).__init__()self.file_path = Noneself.callback = Noneself.pointcloud = Nonedef run(self):#在状态栏加载这段话self.message.emit("Open file | Loading ...", 10000000) # 一直显示pool = Pool()#一个线程池,其大小为20 <multiprocessing.pool.Pool state=RUN pool_size=20>#pool.apply_async:这是 Pool 类的一个方法,用于异步地执行一个函数。与 pool.apply 不同,apply_async 是非阻塞的,这意味着它会立即返回一个 AsyncResult 对象(在您的例子中是 p),而不会等待函数执行完成。其余的则是需要指定的传入参数,这里其要异步执行的是read方法,p = pool.apply_async(func=self.read, args=(self.file_path,), callback=self.callback)pool.close()#关闭线程池pool.join()self.pointcloud = p.get()self.message.emit("Open file | Load point cloud finished.", 1000)self.tag.emit(True)#read方法,根据传入的文件个数,选择不同的文件加载方法,这里我们使用的是ply格式的文件@staticmethoddef read(file_path:str):if file_path.endswith('.las'):xyz, rgb, size, offset = las_read(file_path)elif file_path.endswith('.ply'):xyz, rgb, size, offset = ply_read(file_path)elif file_path.endswith('.txt'):xyz, rgb, size, offset = txt_read(file_path)else:return Nonepointcloud = PointCloud(file_path, xyz, rgb, size, offset)return pointcloud
ply
文件读取代码如下:
def ply_read(file_path):#PlyData是提供的一种PLY文件格式读取接口ply_data = PlyData.read(file_path)#PlyData的值如下:((PlyElement('vertex', (PlyProperty('x', 'double'), PlyProperty('y', 'double'), PlyProperty('z', 'double'), PlyProperty('red', 'uchar'), PlyProperty('green', 'uchar'), PlyProperty('blue', 'uchar')), count=1178312, comments=[]),), text=False, byte_order='<', comments=['Created by Open3D'], obj_info=[])if 'vertex' not in ply_data:return np.array([]), np.array([]), np.array([0, 0, 0]), np.array([0, 0, 0])#下面这段代码则将ply_data的内容读取处理#ply_data['vertex']['x']#Out[5]: memmap([ 7.90625, 7.65625, 8. , ..., 34.78125, 34.71875, 30.1875 ])vertices = np.vstack((ply_data['vertex']['x'],ply_data['vertex']['y'],ply_data['vertex']['z'])).transpose()if 'red' in ply_data['vertex']:colors = np.vstack((ply_data['vertex']['red'],ply_data['vertex']['green'],ply_data['vertex']['blue'])).transpose()else:colors = np.ones(vertices.shape)vertices = vertices.astype(np.float32)#接下来这个操作便是计算一些点云属性,入最值,大小、偏移等xmin, ymin, zmin = min(vertices[:, 0]), min(vertices[:, 1]), min(vertices[:, 2])xmax, ymax, zmax = max(vertices[:, 0]), max(vertices[:, 1]), max(vertices[:, 2])size = np.array((xmax - xmin, ymax - ymin, zmax - zmin))#偏移值即xyz的最小值offset = np.array([xmin, ymin, zmin])vertices -= offsetcolors = colors.astype(np.float32)#颜色归一化colors = colors / 255return vertices, colors, size, offset
使用PlyData
读取的ply
文件内容如下:
将其转换为numpy
格式查看,我们看到其内容如下,即表示xyz
、rgb
。
生成点云对象
将点云信息获得后,生成点云对象(这个PointCloud
是我们自己定义的)
#生成点云对象
pointcloud = PointCloud(file_path, xyz, rgb, size, offset)class PointCloud:def __init__(self, file_path:str, xyz, rgb, size, offset):self.file_path:str = file_pathself.xyz:np.ndarray = xyz if xyz.dtype == np.float32 else xyz.astype(np.float32)self.offset:np.ndarray = offsetself.size:np.ndarray = sizeself.num_point = self.xyz.shape[0]self.rgb:np.ndarray = rgb if rgb.dtype == np.float32 else rgb.astype(np.float32)def __str__(self):return "<PointCloud num_point: {} | size: ({:.2f}, {:.2f}, {:.2f}) | offset: ({:.2f}, {:.2f}, {:.2f}) >".format(self.num_point, self.size[0], self.size[1], self.size[2], self.offset[0], self.offset[1], self.offset[2])
点云渲染生成
最终,我们也就得到了点云文件,接下来将送消息通知,调用point_cloud_read_thread_finished
方法,这个方法主要是初始化一些点云信息,用于将来保存,在这里面,真正在屏幕上生成点云图像的是self.openGLWidget.load_vertices(pointcloud, categorys, instances)
方法
#线程绑
self.point_cloud_read_thread.tag.connect(self.point_cloud_read_thread_finished)
def point_cloud_read_thread_finished(self, tag:bool):if tag:#self.tag.emit(True)这个tag是在这里pointcloud = self.point_cloud_read_thread.pointcloud#获得我们生成的PointCloud对象if pointcloud is None:return#label_file = '.'.join(self.current_file.split('.')[:-1]) + '.json'#当我们点击保存时,会生成json的label标签文件,目录与我们的点云文件相同,该文件设置为仅可读模式categorys = Noneinstances = None#判断这个文件是否存在,继续写入,这个是在第二次打开时才会生效,然后判断点云文件与先前的点云是否相同,并且加载先前我们对点云的分类信息if os.path.exists(label_file):with open(label_file, 'r') as f:datas = load(f)info = datas.get('info', '')if info == 'Laiease label file.':categorys = datas.get('categorys', [])instances = datas.get('instances', [])categorys = np.array(categorys, dtype=np.int16)instances = np.array(instances, dtype=np.int16)if categorys.shape[0] != pointcloud.xyz.shape[0] or instances.shape[0] != pointcloud.xyz.shape[0]:QtWidgets.QMessageBox.warning(self, 'Warning', 'Point cloud size does not match label size!')if categorys.shape[0] != pointcloud.xyz.shape[0]:categorys = Noneif instances.shape[0] != pointcloud.xyz.shape[0]:instances = Noneif pointcloud.num_point < 1:return#展示点云self.openGLWidget.load_vertices(pointcloud, categorys, instances)#侧边标签显示点云信息self.label_num_point.setText('{}'.format(pointcloud.num_point))self.label_size_x.setText('{:.2f}'.format(pointcloud.size[0]))self.label_size_y.setText('{:.2f}'.format(pointcloud.size[1]))self.label_size_z.setText('{:.2f}'.format(pointcloud.size[2]))self.label_offset_x.setText('{:.2f}'.format(pointcloud.offset[0]))self.label_offset_y.setText('{:.2f}'.format(pointcloud.offset[1]))self.label_offset_z.setText('{:.2f}'.format(pointcloud.offset[2]))self.setWindowTitle(pointcloud.file_path)#开启点云框选、取消框选按钮self.actionPick.setEnabled(True)self.actionCachePick.setEnabled(True)
展示点云调用的是load_vertices(pointcloud, categorys, instances)
方法,该方法将计算点云的一些初始化参数
def load_vertices(self, pointcloud, categorys:np.ndarray=None, instances:np.ndarray=None):self.reset()#清空先前的信息self.pointcloud = pointcloud#这里是对视角进行转换,如平移旋转等self.vertex_transform.setTranslation(-pointcloud.size[0]/2, -pointcloud.size[1]/2, -pointcloud.size[2]/2)#添加掩膜,用于根据标签展示点云self.mask = np.ones(pointcloud.num_point, dtype=bool)self.category_display_state_dict = {}self.instance_display_state_dict = {}#这里会读取categorys的内容,即在point_cloud_read_thread_finished方法中加载的json里面的类别信息self.categorys = categorys if categorys is not None else np.zeros(pointcloud.num_point, dtype=np.int16)#这个是不同展示模式,设计了rgb、categorys以及instances三种模式self.instances = instances if categorys is not None else np.zeros(pointcloud.num_point, dtype=np.int16)#计算缩放系数,这个值用于后面屏幕点击坐标转换self.ortho_change_scale = max(pointcloud.size[0] / (self.height() / 5 * 4),pointcloud.size[1] / (self.height() / 5 * 4))self.current_vertices = self.pointcloud.xyz#这个是要通过vob渲染展示到屏幕的点云self.current_colors = self.pointcloud.rgbself.init_vertex_vao()#渲染展示self.resizeGL(self.width(), self.height())self.update()
视角转换
在点云标注过程中,我们经常要进行点云视角切换,如前视角、后视角等。
视角切换代码如下,我们以左视角为例:
首先,需要绑定视角切换按钮和事件
self.actionLeft_view.triggered.connect(self.openGLWidget.set_left_view)
在openglwidget.py
文件中,定义了set_left_view
方法
self.vertex_transform = Transform()self.circle_transform = Transform()self.axes_transform = Transform()self.keep_transform = Transform()self.projection = QMatrix4x4()self.camera = Camera()def set_left_view(self):self.vertex_transform.setRotation(QQuaternion.fromAxisAndAngle(QVector3D(1, 0, 0), 270))self.circle_transform.setRotation(QQuaternion.fromAxisAndAngle(QVector3D(1, 0, 0), 270))self.axes_transform.setRotation(QQuaternion.fromAxisAndAngle(QVector3D(1, 0, 0), 270))self.vertex_transform.rotate(QVector3D(0, 1, 0), 90)self.circle_transform.rotate(QVector3D(0, 1, 0), 90)self.axes_transform.rotate(QVector3D(0, 1, 0), 90)self.update()
Transform()
类的定义如下:
from PyQt5.QtGui import QMatrix4x4, QVector3D, QQuaternion
#视角转换class Transform(object):def __init__(self):self.m_translation = QVector3D()self.m_scale = QVector3D(1, 1, 1)self.m_rotation = QQuaternion()self.m_world = QMatrix4x4()self.localforward = QVector3D(0.0, 0.0, 1.0)self.localup = QVector3D(0.0, 1.0, 0.0)self.localright = QVector3D(1.0, 0.0, 0.0)def forward(self):return self.m_rotation.rotatedVector(self.localforward)def up(self):return self.m_rotation.rotatedVector(self.localup)def right(self):return self.m_rotation.rotatedVector(self.localright)def translate(self, dx, dy, dz):self.m_translation += QVector3D(dx, dy, dz)def scale(self, sx, sy, sz):self.m_scale *= QVector3D(sx, sy, sz)def rotate(self, axis, angle):dr = QQuaternion.fromAxisAndAngle(axis, angle)self.m_rotation = dr * self.m_rotationself.m_translation = dr.rotatedVector(self.m_translation)def rotate_in_place(self, axis, angle):dr = QQuaternion.fromAxisAndAngle(axis, angle)self.m_rotation = dr * self.m_rotationdef setTranslation(self, dx, dy, dz):self.m_translation = QVector3D(dx, dy, dz)def setTranslationwithRotate(self, dx, dy, dz):self.m_translation = self.m_rotation.rotatedVector(QVector3D(dx, dy, dz))def setScale(self, sx, sy, sz):self.m_scale = QVector3D(sx, sy, sz)def setRotation(self, r:QQuaternion):dr = r * self.m_rotation.inverted()self.m_translation = dr.rotatedVector(self.m_translation)self.m_rotation = rdef toMatrix(self):self.m_world.setToIdentity()self.m_world.translate(self.m_translation)self.m_world.scale(self.m_scale)self.m_world.rotate(self.m_rotation)return self.m_world
当然,这里的视角切换是切换到固定视角,当我们的鼠标在拖动时,点云也会随着切换视角,此时,其实现方法与之类似:
def mouse_rotate(self, xoffset, yoffset):# 点云旋转self.vertex_transform.rotate(self.vertex_transform.localup, xoffset * 0.5)self.vertex_transform.rotate(self.vertex_transform.localright, yoffset * 0.5)# 坐标旋转self.circle_transform.rotate_in_place(self.circle_transform.localup, xoffset * 0.5)self.circle_transform.rotate_in_place(self.circle_transform.localright, yoffset * 0.5)self.axes_transform.rotate_in_place(self.axes_transform.localup, xoffset * 0.5)self.axes_transform.rotate_in_place(self.axes_transform.localright, yoffset * 0.5)self.update()
为了将坐标从一个坐标系变换到另一个坐标系,需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么: