1、前言

上篇博客:教你用Python快速开发3D点云数据处理上位机(1),主要给大家介绍了了一些点云常用的处理工具,因为目前图像数据处理相对成熟,而三维点云数据处理资料相对较少,所以上篇博客中主要将一些3D点云处理工具进行了总结。

下面我们主要讲一下上位机的程序的开发过程,由于教你用Python快速开发3D点云数据处理上位机(1)这篇博客中的更新后版本的代码量太大,我们将其中的3D点云显示程序提取出来进行介绍(我们使用的3D点云显示工具为pyqtgraph==0.10.0,开发上位机用的工具为PyQt5==5.13.0)。

2、上位机程序

1、使用PyQt5搭载的界面框架,PyQt5安装配置教程可参考链接:Pycharm安装PyQt5(详细教程)。习惯使用VS Code的读者可以参考:python界面编程:VScode+pyqt+pyqt integration配置备忘

2、使用qtdesigner.exe拖拽生成操作界面(所见即所得的界面生成工具),这里我们默认保存为PyQt_Form.ui格式,生成的操作界面如图1所示。

图1 3D点云数据处理上位机主界面

绘制好界面之后,接下来我们将PyQt_Form.ui格式编译为PyQt_Form.py格式(python文件形式,编译工具为上述链接中提到的pyuic.exe工具),PyQt_Form.py格式的代码如下所示。

# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'e:\Study_self\python_lesson_ybsteer\pyqtgraph_3dlidar\PyQt_Form.ui'
# Created by: PyQt5 UI code generator 5.13.0
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(837, 487)
        self.gridLayout = QtWidgets.QGridLayout(Form)
        self.gridLayout.setObjectName("gridLayout")
        self.stackedWidget = QtWidgets.QStackedWidget(Form)
        self.stackedWidget.setMaximumSize(QtCore.QSize(120, 16777215))
        self.stackedWidget.setObjectName("stackedWidget")
        self.page_5 = QtWidgets.QWidget()
        self.page_5.setObjectName("page_5")
        self.gridLayout_3 = QtWidgets.QGridLayout(self.page_5)
        self.gridLayout_3.setObjectName("gridLayout_3")
        self.frame = QtWidgets.QFrame(self.page_5)
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame.setObjectName("frame")
        self.pushButton_2 = QtWidgets.QPushButton(self.frame)
        self.pushButton_2.setGeometry(QtCore.QRect(10, 120, 75, 41))
        font = QtGui.QFont()
        font.setPointSize(10)
        font.setBold(True)
        font.setWeight(75)
        self.pushButton_2.setFont(font)
        self.pushButton_2.setObjectName("pushButton_2")
        self.pushButton_3 = QtWidgets.QPushButton(self.frame)
        self.pushButton_3.setGeometry(QtCore.QRect(10, 70, 75, 41))
        font = QtGui.QFont()
        font.setPointSize(10)
        font.setBold(True)
        font.setWeight(75)
        self.pushButton_3.setFont(font)
        self.pushButton_3.setObjectName("pushButton_3")
        self.pushButton = QtWidgets.QPushButton(self.frame)
        self.pushButton.setGeometry(QtCore.QRect(10, 20, 75, 41))
        font = QtGui.QFont()
        font.setPointSize(10)
        font.setBold(True)
        font.setWeight(75)
        self.pushButton.setFont(font)
        self.pushButton.setObjectName("pushButton")
        self.gridLayout_3.addWidget(self.frame, 0, 0, 1, 1)
        self.stackedWidget.addWidget(self.page_5)
        self.page_6 = QtWidgets.QWidget()
        self.page_6.setObjectName("page_6")
        self.stackedWidget.addWidget(self.page_6)
        self.gridLayout.addWidget(self.stackedWidget, 0, 1, 1, 1)
        self.stackedWidget_2 = QtWidgets.QStackedWidget(Form)
        self.stackedWidget_2.setObjectName("stackedWidget_2")
        self.page_7 = QtWidgets.QWidget()
        self.page_7.setObjectName("page_7")
        self.gridLayout_2 = QtWidgets.QGridLayout(self.page_7)
        self.gridLayout_2.setObjectName("gridLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.gridLayout_2.addLayout(self.verticalLayout, 0, 0, 1, 1)
        self.label = QtWidgets.QLabel(self.page_7)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
        self.label.setSizePolicy(sizePolicy)
        self.label.setMaximumSize(QtCore.QSize(16777215, 50))
        font = QtGui.QFont()
        font.setFamily("Times New Roman")
        font.setPointSize(15)
        font.setBold(True)
        font.setWeight(75)
        self.label.setFont(font)
        self.label.setText("")
        self.label.setScaledContents(False)
        self.label.setAlignment(QtCore.Qt.AlignCenter)
        self.label.setObjectName("label")
        self.gridLayout_2.addWidget(self.label, 1, 0, 1, 1)
        self.stackedWidget_2.addWidget(self.page_7)
        self.page_8 = QtWidgets.QWidget()
        self.page_8.setObjectName("page_8")
        self.stackedWidget_2.addWidget(self.page_8)
        self.gridLayout.addWidget(self.stackedWidget_2, 0, 0, 1, 1)

        self.retranslateUi(Form)
        self.pushButton.clicked.connect(Form.pointcloud_single_frame_open)
        self.pushButton_2.clicked.connect(Form.pointclouud_target_recognition_algorithm)
        self.pushButton_3.clicked.connect(Form.pointcloud_mutil_frame_open)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "PointCloud"))
        self.pushButton_2.setText(_translate("Form", "识别算法"))
        self.pushButton_3.setText(_translate("Form", "多帧点云"))
        self.pushButton.setText(_translate("Form", "单帧点云"))

3、我们创建好上位机操作界面之后,接下来就可以开始着手去写3D点云读取以及显示的程序了。

这里我们创建kitti_display.py文件,然后将下列程序复制到kitti_display.py文件中。

# -*- coding: utf-8 -*-
import sys,os
if hasattr(sys, 'frozen'):
    os.environ['PATH'] = sys._MEIPASS + ";" + os.environ['PATH']
# 加载PyQt的函数库
from PyQt5 import QtWidgets
from PyQt_Form import Ui_Form  # 加载绘制的UI界面
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QLabel, QInputDialog, QLineEdit # 弹出提示窗
from PyQt5.QtGui import QImage, QPixmap, QPainter, QPen, QGuiApplication    # 二维图片数据显示
from PyQt5.QtGui import *
from PyQt5.QtCore import QTimer, QThread, Qt
import qdarkstyle
import numpy as np  # 数据处理的库 numpy
import math
import pandas as pd  # 数据处理的库 Pandas
import inspect
import ctypes
import time
# from multiprocessing import Process, Queue, Pool
import subprocess
import threading
import pickle
import pyqtgraph.opengl as gl
import pyqtgraph as pg  # 二维曲线绘图
import shutil
# 三维点云函数库
import initExample  # 点云显示需要调用的文件
from PIL import Image, ImageQt
from subfunction import xb_yb_solve

class MyPyQT_Form(QtWidgets.QWidget, Ui_Form):  # 系统主界面
    def __init__(self):
        super(MyPyQT_Form, self).__init__()
        self.timer_camera = QTimer(self)
        self.setupUi(self)
        # pixmap = QPixmap("F:\Study_self\python test\PYQT_template\kaiji.jpg")         # 按指定路径找到图片,注意路径必须用双引号包围,不能用单引号
        # self.label.setPixmap(pixmap)                                                  # 在label上显示图片
        # self.label.setScaledContents(True)                                            # 让图片自适应label大小
        self.initUI()
        self.initFuntion()

    def initUI(self):
        # TODO:初始化将所有的界面,全部置为首页显示
        self.stackedWidget.setCurrentIndex(0)
        self.stackedWidget_2.setCurrentIndex(0)

        self.programe_threading_quit()
        self.programe_timer_quit()

    def initFuntion(self):
        # self.image_dir = os.path.join(os.getcwd(), "image")
        # TODO:定义点云的openGL显示窗口
        opengl_weight = gl.GLViewWidget()
        opengl_weight.opts['distance'] = 20
        self.verticalLayout.addWidget(opengl_weight)
        opengl_weight.setWindowTitle('pyqtgraph example: GLScatterPlotItem')
        gl_glgrideitem = gl.GLGridItem()  # 添加网格
        opengl_weight.addItem(gl_glgrideitem)
        gl_axis = gl.GLAxisItem()   # 添加xyz坐标轴
        opengl_weight.addItem(gl_axis)
        self.sp2 = gl.GLScatterPlotItem(pos=None, color=(1, 1, 1, 1), size=2)  # 不带有任何颜色的白点
        phase = 0.
        opengl_weight.addItem(self.sp2)


        # TODO:定义3D包围框
        self.c_name_list = []
        for i in range(100):
            c_name_var = "c_name" + "_" + str(i)
            self.c_name_list.append(c_name_var)
            # print("i", i, len(c_name_list))
        for j in range(100):
            # print("j", j, c_name_list[5])
            self.c_name_list[j] = gl.GLBoxItem()
            # self.c_name_list[j].setColor(255)
            self.c_name_list[j].setSize(float(0), float(0), float(0))
            opengl_weight.addItem(self.c_name_list[j])
   
    def pointcloud_single_frame_open(self):
        print("pointcloud_open is loading...")
        self.programe_threading_quit()
        self.programe_timer_quit()

        try:
            fileName_choose, filetype = QFileDialog.getOpenFileName(self,
                                                                    "选取文件",
                                                                    os.getcwd(),  # 起始路径
                                                                    "All Files (*);;Text Files (*.txt)")  # 设置文件扩展名过滤,用双分号间
        except:
            fileName_choose = ""
            QMessageBox.critical(self, "waring", "系统未检测到任何文件")
        if fileName_choose.split(".")[-1] == 'bin':
            try:
                path = fileName_choose
                points_bin = np.fromfile(path, dtype=np.float32).reshape(-1, 4)
                points_bin_sig = points_bin[:, 0:3]
                pc_inte = points_bin[:, 2]
                pc_color = self.pointcloud_inte_to_rgb(pc_inte)
                self.sp2.setData(pos=points_bin_sig, color=pc_color)
                try:
                    projected_map = self.pointcloud_range_image_pointcloud(points_bin_sig, 17, 360 / 1024.,
                                                                None)  # 深度图的形状大小为(17, 1024)
                    depth_image_array = self.MatrixToImage(projected_map)  # 将深度图转换为PIL的Image类型
                    # depth_image_array.show()  # 使用PIL直接显示深度图
                    depth_image = ImageQt.toqpixmap(depth_image_array)
                    self.label.setPixmap(depth_image)
                    self.label.setScaledContents(True)
                except:
                    print("深度图显示错误")
            except:
                QMessageBox.critical(self, "waring", "文件打开失败,请选择正确的Bin文件!!")

        elif fileName_choose.split(".")[-1] == 'pcd':
            try:
                points_list = []
                # 读取pcd文件,从pcd的第12行开始是三维点
                ff = open(fileName_choose)
                for line in ff.readlines()[11:len(ff.readlines()) - 1]:
                    strs = line.split(' ')
                    points_list.append([strs[0], strs[1], strs[2], strs[3].strip()])
                # strip()是用来去除换行符,把三维点写入txt文件
                points_pcd = np.array(points_list, dtype=np.float32)
                if points_pcd.shape[-1] > 3:
                    points_pcd = points_pcd[:, 0:4]
                else:
                    len_points = len(points_pcd)
                    col_last_point = np.ones(len_points, dtype=np.float32)
                    points_pcd = np.column_stack((points_pcd, col_last_point))
                points_pcd_sig = points_pcd[:, 0:3]
                pc_inte = points_pcd[:, 2]
                pc_color = self.pointcloud_inte_to_rgb(pc_inte)
                self.sp2.setData(pos=points_pcd_sig, color=pc_color)
            except:
                try:
                    path = fileName_choose
                    points_pcd_bina = np.fromfile(path, dtype=np.float32).reshape(-1, 4)
                    points_pcd_sig_bina = points_pcd_bina[:, 0:3]
                    pc_inte = points_pcd_bina[:, 2]
                    pc_color = self.pointcloud_inte_to_rgb(pc_inte)
                    self.sp2.setData(pos=points_pcd_sig_bina, color=pc_color)
                except:
                    QMessageBox.critical(self, "waring", "文件打开失败,请选择正确的PCD文件!!")

        elif fileName_choose.split(".")[-1] == 'txt':
            try:
                points_list = np.loadtxt(fileName_choose)
                points_txt = np.array(points_list, dtype=np.float32)
                if points_txt.shape[-1] > 3:
                    points_txt = points_txt[:, 0:4]
                else:
                    len_points = len(points_txt)
                    col_last_point = np.ones(len_points, dtype=np.float32)
                    points_txt = np.column_stack((points_txt, col_last_point))
                points_txt_sig = points_txt[:, 0:3]
                pc_inte = points_txt[:, 2]
                pc_color = self.pointcloud_inte_to_rgb(pc_inte)
                self.sp2.setData(pos=points_txt_sig, color=pc_color)
            except:
                QMessageBox.critical(self, "waring", "文件打开失败,请选择正确的txt文件!!")

        elif fileName_choose.split(".")[-1] == 'npy':
            try:
                points_list = np.load(fileName_choose)
                points_npy = np.array(points_list, dtype=np.float32)
                points_npy_sig = points_npy[:, 0:3]
                pc_inte = points_npy[:, 2]
                pc_color = self.pointcloud_inte_to_rgb(pc_inte)
                self.sp2.setData(pos=points_npy_sig, color=pc_color)
            except:
                QMessageBox.critical(self, "waring", "文件打开失败,请选择正确的npy文件!!")

        elif fileName_choose.split(".")[-1] == 'csv':
            try:
                points_list = pd.read_csv(fileName_choose)
                points_csv = np.array(points_list, dtype=np.float32)
                if points_csv.shape[-1] > 3:
                    points_csv = points_csv[:, 0:4]
                else:
                    len_points = len(points_csv)
                    col_last_point = np.ones(len_points, dtype=np.float32)
                    points_csv = np.column_stack((points_csv, col_last_point))
                points_csv_sig = points_csv[:, 0:3]
                pc_inte = points_csv[:, 2]
                pc_color = self.pointcloud_inte_to_rgb(pc_inte)
                self.sp2.setData(pos=points_csv_sig, color=pc_color)
            except:
                QMessageBox.critical(self, "waring", "文件打开失败,请选择正确的csv文件!!")
        else:
            QMessageBox.critical(self, "waring", "不支持的文件类型")

    def pointcloud_mutil_frame_open(self):
        """
        1、首先提示点云、标签文件的放置格式
        2、检测/velodyne文件夹路径,检测不到则退出
        3、检测到/velodyne文件夹路径之后,检测/pred文件夹路径
        4、如果检测不到/pred文件夹路径,提示是否运行?如果是,则只显示点云不显示3D包围框
        :return:
        """
        self.programe_threading_quit()
        self.programe_timer_quit()
        try:
            reply_points = QtWidgets.QMessageBox.question(self,
                                                '提示',
                                                "请确定文件格式为:\n |-pointcloud_data_dir \n |--velodyne \n |---000000.bin \n |--pred \n |---000000.txt ",
                                                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
                                                QtWidgets.QMessageBox.No)
            if reply_points == QtWidgets.QMessageBox.Yes:
                self.pointcloud_data_dirs = QFileDialog.getExistingDirectory(None, "选择文件夹路径", os.getcwd())   # 选择的是data_dir的文件路径
                # for parent, dirs, files in os.walk(self.pointcloud_data_dirs):
                #     self.file_name_list = []
                #     for file in files:
                #         self.file_name_list.append(file)
                self.pointcloud_data_dir_path= os.path.join(self.pointcloud_data_dirs, "velodyne")
                self.pointcloud_labels_dir_path = os.path.join(self.pointcloud_data_dirs, "pred")
                if os.path.exists(self.pointcloud_data_dir_path):
                    self.pointcloud_data_files_list = os.listdir(self.pointcloud_data_dir_path)
                    if os.path.exists(self.pointcloud_labels_dir_path):
                        self.pointcloud_labels_files_list = os.listdir(self.pointcloud_labels_dir_path)
                        self.mutil_pointcloud_signNum = 0
                        self.timer_pointcloud_mutil_pointcloud = QTimer(self)
                        self.timer_pointcloud_mutil_pointcloud.timeout.connect(self.pointcloud_mutil_pointcloud_update)
                        self.timer_pointcloud_mutil_pointcloud.start(50)  # 设置时间间隔 50 ms
                    else:
                        reply_labels = QtWidgets.QMessageBox.question(self,
                                                            '提示',
                                                            "请确定文件格式为:\n |-pointcloud_data_dir \n |--velodyne \n |---000000.bin \n |--pred \n |---000000.txt ",
                                                            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
                                                            QtWidgets.QMessageBox.No)
                        if reply_labels == QtWidgets.QMessageBox.Yes:
                            self.mutil_pointcloud_signNum = 0
                            self.timer_pointcloud_mutil_pointcloud = QTimer(self)
                            self.timer_pointcloud_mutil_pointcloud.timeout.connect(self.pointcloud_mutil_pointcloud_update)
                            self.timer_pointcloud_mutil_pointcloud.start(50)  # 设置时间间隔 50 ms
                else:
                    QMessageBox.critical(self, "warning", "选择的文件目录未检测到/velodyne文件夹")
            else:
                print("选择了退出,没有打开任何文件夹")
        except:
            self.pointcloud_data_dirs = ""
            QMessageBox.critical(self, "waring", "文件路径读取失败 2")

    def pointcloud_mutil_pointcloud_update(self):
        """
        多帧点云数据显示
        点云动态更新
        :return:
        """
        try:
            poincloud_data_file_path = os.path.join(self.pointcloud_data_dir_path, self.pointcloud_data_files_list[self.mutil_pointcloud_signNum])
            points_read = np.fromfile(poincloud_data_file_path, dtype=np.float32).reshape(-1, 4)    # 只能读取KITTI格式的标准bin格式点云
            points = points_read[:, 0:3]            # 读取点云的x,y,z
            pc_inte = points_read[:, 3]             # 点云的颜色显示,使用第4列强度值
            pc_color = self.pointcloud_inte_to_rgb(pc_inte)    # 将强度值转化为特定的颜色显示格式==》(1,1,1,1)
            self.sp2.setData(pos=points, color=pc_color)    # setData(pos=xx)可以实现动态刷新
            try:
                label_file = self.pointcloud_data_files_list[self.mutil_pointcloud_signNum].split(".")[0] + ".txt"
                label_file_frame = os.path.join(self.pointcloud_labels_dir_path, label_file)
                with open(label_file_frame, 'r') as f:
                    labels = f.readlines()
                    for u in range(100):
                        self.c_name_list[u].setSize(float(0), float(0), float(0))
                        self.c_name_list[u].resetTransform()
                bbox_multi_sign = 0
                for line in labels:
                    line = line.split()
                    lab, _, _, _, _, _, _, _, h, w, l, x, y, z, rot, _ = line
                    h, w, l, x, y, z, rot = map(float, [h, w, l, x, y, z, rot])
                    if lab != 'DontCare':
                        # print("lab", lab)
                        corners_3d = np.vstack([0, -h / 2, 0])  # (3, 8)
                        # transform the 3d bbox from object coordiante to camera_0 coordinate
                        R = np.array([[np.cos(rot), 0, np.sin(rot)],
                                    [0, 1, 0],
                                    [-np.sin(rot), 0, np.cos(rot)]])
                        corners_3d = np.dot(R, corners_3d).T + np.array([x, y, z])
                        # transform the 3d bbox from camera_0 coordinate to velodyne coordinate
                        corners_3d = corners_3d[:, [2, 0, 1]] * np.array([[1, -1, -1]])
                        corners_x = float(corners_3d[0, 0])
                        corners_y = float(corners_3d[0, 1])
                        corners_z = float(corners_3d[0, 2])
                        z_rotation_angle = (-rot * (180 / 3.14)) + 360
                        # print("-rot", -rot * (180/3.14))
                        if lab == "Car":
                            self.c_name_list[bbox_multi_sign].setColor(255)   # 汽车颜色为黄色
                        elif lab == "Pedestrian":
                            self.c_name_list[bbox_multi_sign].setColor(250)   # 行人颜色为蓝色
                        else:
                            self.c_name_list[bbox_multi_sign].setColor(0)     # 其余物体颜色为红色
                        self.c_name_list[bbox_multi_sign].setSize(float(w), float(l), float(h))         # 设置3D包围框的大小
                        self.c_name_list[bbox_multi_sign].translate(corners_x, corners_y, corners_z)    # 3D包围框的空间位置(不是中心点,是左下角的坐标位置)
                        self.c_name_list[bbox_multi_sign].rotate(z_rotation_angle, 0, 0, 1, locals())   #  3D包围框的左下角现在在中心点位置,绕左下角坐标轴旋转,代替中心点旋转
                        x_, y_ = xb_yb_solve(rot, float(w / 2), float(l / 2))                           # 上面用左下角代替中心点旋转,这一步替换回去,计算左下角坐标位置
                        self.c_name_list[bbox_multi_sign].translate(-x_, -y_, -float(h / 2))            # 替换回去,让3D包围框中心在刚才的左下角坐标位置
                        bbox_multi_sign = bbox_multi_sign + 1      # 设置标志位,求一帧点云中有多少个3Dbbox
            except:
                print("查找不到对应的label标签文件 \n 请检查pred文件夹是否存在 \n 请检查pointcloud处理程序是否加载")
            self.mutil_pointcloud_signNum = self.mutil_pointcloud_signNum + 1   # 一共多少帧点云

            try:  # TODO:动态显示深度图
                projected_map = self.pointcloud_range_image_pointcloud(points, 17, 360 / 1024., None)  # 深度图的形状大小为(17, 1024)
                depth_image_array = self.MatrixToImage(projected_map)  # 将深度图转换为PIL的Image类型
                # depth_image_array.show()  # 使用PIL直接显示深度图
                # print("depth image array:shape", depth_image_array)

                depth_image = ImageQt.toqpixmap(depth_image_array)
                self.label.setPixmap(depth_image)
                self.label.setScaledContents(True)

            except:
                print("深度图显示错误")
        except:
            self.mutil_pointcloud_signNum = 0       # 设置循环播放

    def pointclouud_target_recognition_algorithm(self):
        print("pointcloud_target_recognition_algorithm is loading...")
        QMessageBox.Critical(self, "warning", "功能开发中...")
        
    def pointcloud_inte_to_rgb(self, pc_inte):
        """
        :激光雷达的颜色信息
        :param pc_inte:
        :return:
        """
        minimum, maximum = np.min(pc_inte), np.max(pc_inte)
        try:
            if minimum == maximum:
                ratio = 2 * (pc_inte + minimum) / (maximum + minimum)
                b = (np.maximum((1 - ratio), 0))
                r = (np.maximum((ratio - 1), 0))
                g = 1 - b - r
            else:
                ratio = 2 * (pc_inte - minimum) / (maximum - minimum)
                b = (np.maximum((1 - ratio), 0))
                r = (np.maximum((ratio - 1), 0))
                g = 1 - b - r
        except:
            ratio = 2 * (pc_inte + minimum) / (maximum + minimum)
            b = (np.maximum((1 - ratio), 0))
            r = (np.maximum((ratio - 1), 0))
            g = 1 - b - r
        return np.stack([r, g, b, np.ones_like(r)]).transpose()


    def pointcloud_range_image_pointcloud(self, points, v_line=17, h_res=360 / 1024., ground_indices=None):
        """
        点云转化为深度图
        打开一帧或者多帧点云,深度图跟着进行动态刷新
        """
        # range image size, depends on your sensor, i.e., VLP-16: 16x1800, OS1-64: 64x1024
        image_rows_full = 17
        image_cols = 1025
        max_range = 80.0
        min_range = 1.0
        ang_res_x = 360.0 / float(image_cols)  # horizontal resolution
        ang_res_y = 32.5 / float(image_rows_full - 1)  # vertical resolution
        ang_start_y = 16.6  # bottom beam angle
        x, y, z = points[:, 0], points[:, 1], points[:, 2]
        vertical_angle = np.arctan2(z, np.sqrt(x * x + y * y)) * 180.0 / np.pi
        relative_vertical_angle = vertical_angle + ang_start_y
        rowId = np.int_(np.round_(relative_vertical_angle / ang_res_y))
        horitontal_angle = np.arctan2(x, y) * 180.0 / np.pi
        colId = -np.int_((horitontal_angle - 90.0) / ang_res_x) + image_cols / 2;
        shift_ids = np.where(colId >= image_cols)
        colId[shift_ids] = colId[shift_ids] - image_cols
        thisRange = np.sqrt(x * x + y * y + z * z)
        thisRange[thisRange > max_range] = 0
        thisRange[thisRange < min_range] = 0
        thisRange2 = thisRange.copy()
        if ground_indices is not None:
            thisRange2[ground_indices] = 0
        range_image = np.zeros((image_rows_full, image_cols))

        # range_image[np.int_(rowId), np.int_(colId), 0] = x
        # range_image[np.int_(rowId), np.int_(colId), 1] = y
        # range_image[np.int_(rowId), np.int_(colId), 2] = z
        # range_image[np.int_(rowId), np.int_(colId), 3] = thisRange
        # range_image[np.int_(rowId), np.int_(colId), 4] = thisRange2
        range_image[np.int_(rowId), np.int_(colId)] = x
        return range_image

    def MatrixToImage(self, depth_image_data):
        """
        转化后的深度图转化为能够显示的图像格式
        :param depth_image_data:
        :return: depth_image_array
        """
        depth_image_data = depth_image_data * 255
        depth_image_array = Image.fromarray(depth_image_data.astype(np.uint8))
        return depth_image_array

    def programe_threading_quit(self):
        try: 
            self.stop_thread(self.threading_camera_image_face_info)
        except: 
            pass    

    def programe_timer_quit(self):
        try:
            self.timer_pointcloud_mutil_pointcloud.stop()
        except:
            pass

        try:
            self.label.setText("Depth Map Point Cloud")
            self.label.setAlignment(Qt.AlignCenter)

        except:
            pass
        
    def _async_raise(self, tid, exctype):
        """raises the exception, performs cleanup if needed"""
        tid = ctypes.c_long(tid)
        if not inspect.isclass(exctype):
            exctype = type(exctype)
        res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
        if res == 0:
            raise ValueError("invalid thread id")
        elif res != 1:
            # """if it returns a number greater than one, you're in trouble,
            # and you should call it again with exc=NULL to revert the effect"""
            ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
            raise SystemError("PyThreadState_SetAsyncExc failed")

    def stop_thread(self, thread):
        self._async_raise(thread.ident, SystemExit)

    def closeEvent(self, event):
        """
        参考:https://www.cnblogs.com/foreverlin/p/10881346.html
        对MainWindow的函数closeEvent进行重构
        退出软件时结束所有进程
        :param event:
        :return:
        """
        reply = QtWidgets.QMessageBox.question(self,
                                               '本程序',
                                               "是否要退出程序?",
                                               QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
                                               QtWidgets.QMessageBox.No)
        if reply == QtWidgets.QMessageBox.Yes:
            event.accept()
            self.programe_threading_quit()
            self.programe_timer_quit()
            os._exit(0)
        else:
            event.ignore()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setStyleSheet(qdarkstyle.load_stylesheet())     # 使用开源插件优化界面

    my_pyqt_form = MyPyQT_Form()
    my_pyqt_form.show()

    sys.exit(app.exec_())

4、创建好上述程序之后,我们运行python kitti_display.py,便可以正常启动该程序了。我们设定上位机图像界面刷新时间为50 ms,3D点云显示效果如图2(a)和图2(b)所示,从图中可以看出,我们上位机显示界面中不但包含3D点云数据显示,而且还将当前帧3D点云转化为深度图并进行显示(部分程序待优化,欢迎大家评论交流~)。

图2(a)单帧点云数据显示

图2(b)多帧点云数据显示效果(实际为动态显示)