先前我们使用的是Open3D进行点云加载与展示,但由于Open3D更侧重于点云处理,其缺少一些相关的GUI控件,因此采用PyQt进行开发,同时使用OpenGL进行3D渲染,那么具体要如何实现呢?
复选框类别选中点云展示
如何开发根据点云类别展示点云的功能呢,效果如下:
当我们的复选框被取消时,相应的点云会不显示
在main.py
文件中,其代码如下:
首先需要给复选框绑定事件
check_box.stateChanged.connect(functools.partial(self.point_cloud_visible))
self.openGLWidget.categorys
是点云的类别,其值如下:
接下来的事件处理都是围绕着self.openGLWidget.categorys
展开的,其值代表着每个点云所属的类别
事件处理逻辑如下:
def point_cloud_visible(self):#首先是判断当前的展示类型,是否是CATEGORYif self.openGLWidget.display == DISPLAY.CATEGORY:#根据点云类别颜色生成蒙版,初始全为Truemask = np.ones(self.openGLWidget.categorys.shape, dtype=bool)#遍历类别复选框,根据索引号与self.openGLWidget.categorys中的点云类别值做匹配,从而选择这些点云是否可见for index in range(self.label_listWidget.count()):#self.label_listWidget.count()的值为8item = self.label_listWidget.item(index)widget = self.label_listWidget.itemWidget(item)check_box = widget.findChild(QtWidgets.QCheckBox, 'check_box')#如果当前的索引下被选中if not check_box.isChecked():#根据索引index使对应的mask变为Fale,即不可见mask[self.openGLWidget.categorys==index] = Falseself.openGLWidget.category_display_state_dict[index] = False#其值{0: True, 1:False, 2: True, 3: True, 4: True, 5: True, 6: True, 7: True}else:self.openGLWidget.category_display_state_dict[index] = True#获得最终的maskself.openGLWidget.mask = mask#根据mask更改当前点云,包含坐标和颜色self.openGLWidget.current_vertices = self.openGLWidget.pointcloud.xyz[self.openGLWidget.mask]self.openGLWidget.current_colors = self.openGLWidget.category_color[self.openGLWidget.mask]elif self.openGLWidget.display == DISPLAY.INSTANCE:mask = np.ones(self.openGLWidget.instances.shape, dtype=bool)for index in range(self.label_listWidget.count()):item = self.label_listWidget.item(index)widget = self.label_listWidget.itemWidget(item)label_instance = widget.findChild(QtWidgets.QLabel, 'label_instance')label_instance = int(label_instance.text())check_box = widget.findChild(QtWidgets.QCheckBox, 'check_box')if not check_box.isChecked():mask[self.openGLWidget.instances==label_instance] = Falseself.openGLWidget.instance_display_state_dict[label_instance] = Falseelse:self.openGLWidget.instance_display_state_dict[label_instance] = Trueself.openGLWidget.mask = maskself.openGLWidget.current_vertices = self.openGLWidget.pointcloud.xyz[self.openGLWidget.mask]self.openGLWidget.current_colors = self.openGLWidget.instance_color[self.openGLWidget.mask]#重新渲染点云self.openGLWidget.init_vertex_vao()self.openGLWidget.update()
最终效果如下:
点云渲染加速
每当我们执行了某个操作,最后要想对点云有效果,最后都需要调用一段代码:
self.openGLWidget.init_vertex_vao()
self.openGLWidget.update()
其中init_vertex_vao()
是我们自定义的,即初始化点云(每次点云发生修改都需要进行初始化,这里的初始化可以认为是重写)而update函数则是OpenGL
的更新函数
opengl_widget.py
中的init_vertex_vao
函数定义代码如下:
这段代码主要是将一些数据加载到缓冲区,用以提升渲染效率,这里我们需要了解一些基础概念:
VBO vertex buffer object
顶点缓冲对象 :VBO
是CPU
和GPU
传递信息的桥梁,我们把数据存入VBO
是在CPU
上操作,VBO
会自动将数据送至GPU
。送至GPU
不需要任何人为操作。VAO vertex array object
顶点数组对象:VBO
将顶点数据传送至GPU
只是一堆数字,要怎么向GPU
解释它们呢,就需要VAO
,其内包含的是顶点数据的格式信息EBO element(index) buffer object
索引缓冲对象
def init_vertex_vao(self):self.vertex_vao = glGenVertexArrays(1)#请求生成 2 个新的缓冲区对象名称(VBO)vbos = glGenBuffers(2)#将特定的缓冲区对象名称(或称为“缓冲区标识符”即vbos[0])与特定的缓冲区目标绑定在一起,GL_ARRAY_BUFFER 是一个枚举值,指定了缓冲区的目标类型。在这个例子中,它表示该缓冲区将用作顶点属性数据(如顶点位置、颜色、纹理坐标等)的存储,即该段代码的含义是绑定glBindBuffer(GL_ARRAY_BUFFER, vbos[0])#加载是数据,glBufferData 或 glBufferSubData 函数来上传数据到缓冲区glBufferData(GL_ARRAY_BUFFER, self.current_vertices.nbytes, self.current_vertices, GL_STATIC_DRAW)glBindBuffer(GL_ARRAY_BUFFER, 0)#这段代码首先将vbos[1]绑定到GL_ARRAY_BUFFER目标上,然后使用glBufferData函数上传self.current_vertices数组中的数据到该缓冲区对象。self.current_colors.nbytes指定了数据的大小(以字节为单位),GL_STATIC_DRAW表示数据的使用模式(即数据很少更改,且会被多次绘制)。最后,将GL_ARRAY_BUFFER的绑定解除(绑定到0)。glBindBuffer(GL_ARRAY_BUFFER, vbos[1])glBufferData(GL_ARRAY_BUFFER, self.current_colors.nbytes, self.current_colors, GL_STATIC_DRAW)glBindBuffer(GL_ARRAY_BUFFER, 0)glBindVertexArray(self.vertex_vao)glBindBuffer(GL_ARRAY_BUFFER, vbos[0])glEnableVertexAttribArray(0)#glVertexAttribPointer函数的参数指定了顶点属性的位置、大小、类型、是否归一化、步长以及偏移量。glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, self.current_vertices.itemsize * 3, ctypes.c_void_p(0))#ctypes.c_void_p(0)用于指定顶点属性数据的偏移量。在这个例子中,由于数据是紧密打包的,且从缓冲区的开始位置读取,所以偏移量为0。glBindBuffer(GL_ARRAY_BUFFER, 0)glBindBuffer(GL_ARRAY_BUFFER, vbos[1])glEnableVertexAttribArray(1)glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, self.current_colors.itemsize * 3, ctypes.c_void_p(0))glBindBuffer(GL_ARRAY_BUFFER, 0)#将VAO的绑定解除。glBindVertexArray(0)
点云拾取
在点云标注中,获取点云坐标是一个十分重要的功能。
首先,双击鼠标触发事件,这里,获取点云坐标的功能是将该点作为坐标中心点
def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent):#获取点击的坐标x, y = event.pos().x(), self.height() - event.pos().y()if self.pointcloud is None:return#计算当前点的点云位置point = self.pickpoint(x, y)print(point)# 双击点移动到坐标中心if point.size:self.vertex_transform.setTranslationwithRotate(-point[0], -point[1], -point[2])self.update()
计算点云坐标的代码实现如下:
def pickpoint(self, x, y):if self.pointcloud is None:return np.array([])point1 = QVector3D(x, y, 0).unproject(self.camera.toMatrix() * self.vertex_transform.toMatrix(),self.projection,QtCore.QRect(0, 0, self.width(), self.height()))point2 = QVector3D(x, y, 1).unproject(self.camera.toMatrix() * self.vertex_transform.toMatrix(),self.projection,QtCore.QRect(0, 0, self.width(), self.height()))vector = (point2 - point1) # 直线向量vector.normalize()# 点到直线(点向式)的距离t = (vector.x() * (self.current_vertices[:, 0] - point1.x()) +vector.y() * (self.current_vertices[:, 1] - point1.y()) +vector.z() * (self.current_vertices[:, 2] - point1.z())) / (vector.x() ** 2 + vector.y() ** 2 + vector.z() ** 2)d = (self.current_vertices[:, 0] - (vector.x() * t + point1.x())) ** 2 + \(self.current_vertices[:, 1] - (vector.y() * t + point1.y())) ** 2 + \(self.current_vertices[:, 2] - (vector.z() * t + point1.z())) ** 2pickpoint_radius = self.pickpoint_radius * self.ortho_change_scalemask = d < pickpoint_radius**2if not any(mask):return np.array([])mask1 = self.mask.copy()mask1[mask1==True] = maskpoints = self.pointcloud.xyz[mask1]index = np.argmin(points[:, 0] * vector.x() + points[:, 1] * vector.y() + points[:, 2] * vector.z())point = points[index] # 取最近的点return point
效果如下: