先前我们使用的是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):
#首先是判断当前的展示类型,是否是CATEGORY
if self.openGLWidget.display == DISPLAY.CATEGORY:
#根据点云类别颜色生成蒙版,初始全为True
mask = np.ones(self.openGLWidget.categorys.shape, dtype=bool)
#遍历类别复选框,根据索引号与self.openGLWidget.categorys中的点云类别值做匹配,从而选择这些点云是否可见
for index in range(self.label_listWidget.count()):#self.label_listWidget.count()的值为8
item = 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] = False
self.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
#获得最终的mask
self.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] = False
self.openGLWidget.instance_display_state_dict[label_instance] = False
else:
self.openGLWidget.instance_display_state_dict[label_instance] = True
self.openGLWidget.mask = mask
self.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())) ** 2
pickpoint_radius = self.pickpoint_radius * self.ortho_change_scale
mask = d < pickpoint_radius**2
if not any(mask):
return np.array([])
mask1 = self.mask.copy()
mask1[mask1==True] = mask
points = self.pointcloud.xyz[mask1]
index = np.argmin(points[:, 0] * vector.x() + points[:, 1] * vector.y() + points[:, 2] * vector.z())
point = points[index] # 取最近的点
return point
效果如下: