一、读取数据文件
1、设计点类
class Point:
__slots__ = ('idx', 'x', 'y', 'z')
def __init__(self, idx, x, y, z):
self.idx = idx # 存储点的序号(从1开始)
self.x = x
self.y = y
self.z = z
2、静默读取
不将大量点数据显示到 Qt 界面,避免渲染卡顿。
(1)初始化路径
file_path = None
(2)打开文件
def open_file(self):
global file_path
path, _ = QFileDialog.getOpenFileName(self, "打开文件", "", "文本文件(*.txt)")
file_path = path
self.original_text_area.setText("已正确读取并将点数据静默后台存储。")
self.result_text_area.setText("等待数据处理....")
self.status_label.setText("已打开数据文件。")
(3)逐行解析点数据
points = []
with open(file_path, "r", encoding="utf-8") as f:
for idx, line in enumerate(f, start=1):
part = line.strip().split(" ")
p = Point(int(idx), float(part[0]), float(part[1]), float(part[2]))
points.append(p)
二、算法实现

去噪实现
采用哈希思想,计算每个点的格网索引,以格网元组为键、该格网内的点列表为值,存入字典:
my_dict = {} # key为元组(网格索引),value为列表(该网格内的点)
for p in points:
i = int((p.x - x_min) / c)
j = int((p.y - y_min) / c)
k = int((p.z - z_min) / c)
m_key = (i, j, k)
if m_key not in my_dict:
my_dict[m_key] = []
my_dict[m_key].append(p)
进行 k 近邻点搜索时,先计算目标点所在格网,再通过字典查询相邻 27 个格网的候选点,时间复杂度 O(n)。
错误解法(O(n²))
for p in points:
i, j, k = p.in_grid
flag = []
for i_ in [-1, 0, 1]:
for j_ in [-1, 0, 1]:
for k_ in [-1, 0, 1]:
new_grid = (i + i_, j + j_, k + k_)
flag.append(new_grid)
for px in points: # 遍历所有点,O(n²)
ix, jx, kx = px.in_grid
if (ix, jx, kx) in flag:
p.neighbor_.append(px)
未利用格网索引的哈希思想,导致双重循环,时间复杂度 O(n²)。
三、源代码
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import math
file_path = None
class Point:
__slots__ = ('idx', 'x', 'y', 'z')
def __init__(self, idx, x, y, z):
self.idx = idx # 存储点的序号(从1开始)
self.x = x
self.y = y
self.z = z
def calculate_distance(p1, p2):
return math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 + (p1.z - p2.z) ** 2)
# 计算点p的候选点和邻近点
def calculate_neighbor(p, x_min, y_min, z_min, c, my_dict):
x_lst = [-1, 0, 1]
y_lst = [-1, 0, 1]
z_lst = [-1, 0, 1]
candidate = []
x = p.x
y = p.y
z = p.z
i = int((x - x_min) / c)
j = int((y - y_min) / c)
k = int((z - z_min) / c)
for xx in x_lst:
for yy in y_lst:
for zz in z_lst:
neighbor_ijk = (i + xx, j + yy, k + zz)
if neighbor_ijk not in my_dict:
continue
candidate.extend(my_dict[neighbor_ijk])
# 计算p与候选点的欧氏距离,排序后取前6个作为邻近点
candidate.sort(key = lambda p1: calculate_distance(p1, p))
neighbor_points = candidate[1:7] # 取前六个点作为邻近点(注意:排除自身点)
idx_max = max([p.idx for p in neighbor_points])
return len(candidate) - 1, idx_max, neighbor_points # 返回点p的候选点总数和其六个邻近点中序号的最大值
class App(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("基于统计滤波的点云去噪")
self.setGeometry(100,100,850,600)
self._init_ui()
def _init_ui(self):
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu("文件")
file_menu.addAction("📂打开",self.open_file)
file_menu.addAction("🗃️保存",self.save_file)
file_menu.addSeparator()
file_menu.addAction("❌关闭",self.close)
menu_bar.addMenu("运行").addAction("⛷️运行算法",self.process_data)
menu_bar.addMenu("显示").addAction("🖥️显示结果", self.process_data)
toolbar = self.addToolBar("工具栏")
toolbar.addAction("📂打开",self.open_file)
toolbar.addAction("⛷️运行算法",self.process_data)
self.status_label = QLabel("✅就绪")
self.statusBar().addPermanentWidget(self.status_label, 1)
self.statusBar().setStyleSheet("background:#e8f4f0;")
tab = QWidget()
layout = QVBoxLayout(tab)
layout.setContentsMargins(5,5,5,5)
splitter = QSplitter(Qt.Horizontal)
splitter.addWidget(self._make_panel("原始数据"))
splitter.addWidget(self._make_panel("运行结果"))
splitter.setSizes([425,425])
layout.addWidget(splitter)
notebook = QTabWidget()
notebook.addTab(tab,"数据处理")
self.setCentralWidget(notebook)
self.original_text_area = self.panels[0]
self.result_text_area = self.panels[1]
def _make_panel(self,title):
frame = QFrame()
frame.setFrameShape(QFrame.StyledPanel)
layout = QVBoxLayout(frame)
layout.addWidget(QLabel(title))
text = QTextEdit()
text.setStyleSheet("font-family:Consolas;font-size:10pt;")
layout.addWidget(text)
if not hasattr(self,'panels'):
self.panels = []
self.panels.append(text)
return frame
def open_file(self):
global file_path
path,_ = QFileDialog.getOpenFileName(self, "打开文件", "", "文本文件(*.txt)")
file_path = path
self.original_text_area.setText("✅已正确读取并将点数据静默后台存储。")
self.result_text_area.setText("⌛️等待数据处理....")
self.status_label.setText(f"✅已打开数据文件。")
def save_file(self):
path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "文本文件(*.txt)")
with open(path,"w",encoding="utf-8") as f:
f.write(self.result_text_area.toPlainText())
self.status_label.setText(f"✅已保存结果至:{path}")
def process_data(self):
try:
processed_lines = []
# 一、读取数据文件
points = []
with open(file_path, "r", encoding="utf-8") as f:
for idx, line in enumerate(f, start = 1):
part = line.strip().split(" ")
p = Point(int(idx), float(part[0]), float(part[1]), float(part[2]))
points.append(p)
processed_lines.append(f"1、点P1的x坐标为:{points[0].x:.3f}")
processed_lines.append(f"2、点P6的y坐标为:{points[5].y:.3f}")
processed_lines.append(f"3、点P789的z坐标为:{points[788].z:.3f}")
# 二、程序算法
# 1、数据统计与初始化
n = len(points)
processed_lines.append(f"4、原始点云的总点数:{n}")
# 2、去噪实现
# 2.1格网划分与点云分配
c = 3
x_max = max([p.x for p in points])
y_max = max([p.y for p in points])
z_max = max([p.z for p in points])
x_min = min([p.x for p in points])
y_min = min([p.y for p in points])
z_min = min([p.z for p in points])
x_max1 = int((x_max - x_min) / c + 1) * c + x_min
y_max1 = int((y_max - y_min) / c + 1) * c + y_min
z_max1 = int((z_max - z_min) / c + 1) * c + z_min
cnt = 0
for p in points:
x = p.x
y = p.y
z = p.z
i = int((x-x_min) / c)
j = int((y-y_min) / c)
k = int((z-z_min) / c)
if i == 0 and j == 0 and k == 0:
cnt += 1
p1_i = int((points[0].x - x_min) / c)
p6_j = int((points[5].y - y_min) / c)
processed_lines.append(f"5、点云数据x最大值xmax:{x_max:.3f}")
processed_lines.append(f"6、点云数据y最大值ymax:{y_max:.3f}")
processed_lines.append(f"7、点云数据z最大值zmax:{z_max:.3f}")
processed_lines.append(f"8、格网xyz最小最大值xmax1:{x_max1:.3f}")
processed_lines.append(f"9、格网xyz最小最大值ymax1:{y_max1:.3f}")
processed_lines.append(f"10、格网xyz最小最大值zmax1:{z_max1:.3f}")
processed_lines.append(f"11、网格(0,0,0)内的点个数:{cnt}")
processed_lines.append(f"12、点P1的网格索引(i,j,k)中的i分量(第一分量):{p1_i}")
processed_lines.append(f"13、点P6的网格索引(i,j,k)中的j分量(第二分量):{p6_j}")
# 2.2k邻近点搜索(k=6)
# (1)存储每个网格包括哪些点
my_dict = {} # key为元组,表示网格。value为列表,表示在该网格的点类。
for p in points:
x = p.x
y = p.y
z = p.z
i = int((x - x_min) / c)
j = int((y - y_min) / c)
k = int((z - z_min) / c)
m_key = (i, j, k)
if m_key not in my_dict:
my_dict[m_key] = []
my_dict[m_key].append(p)
# (2)计算点P1的候选点总数,点P6的候选点总数,点P1的6个邻近点序号中最大值,点P6的六个邻近点序号中最大值
p1_candidate_points_cnt, p1_max_idx,_ = calculate_neighbor(points[0], x_min, y_min, z_min, c, my_dict)
p6_candidate_points_cnt, p6_max_idx,_ = calculate_neighbor(points[5], x_min, y_min, z_min, c, my_dict)
processed_lines.append(f"14、点P1的候选点总数:{p1_candidate_points_cnt}")
processed_lines.append(f"15、点P6的候选点总数:{p6_candidate_points_cnt}")
processed_lines.append(f"16、点P1的6个邻近点序号中最大值:{p1_max_idx}")
processed_lines.append(f"17、点P6的6个邻近点序号中最大值:{p6_max_idx}")
# 2.3统计特征计算
all_u = []
all_cc = []
# (1)对于每个点,找到其k个邻近点Ni,k=6
for p in points:
_, _, neighbor_points = calculate_neighbor(p, x_min, y_min, z_min, c, my_dict)
# (2)计算邻域点的平均距离u和标准差cc
u = sum(calculate_distance(neighbor_point, p) for neighbor_point in neighbor_points) / 6
cc = math.sqrt(sum((calculate_distance(neighbor_point, p)-u)**2 for neighbor_point in neighbor_points) / 6)
all_u.append(u)
all_cc.append(cc)
if p.idx == 1:
processed_lines.append(f"18、点P1的邻域平均距离:{u:.3f}")
processed_lines.append(f"19、点P1的邻域距离标准差:{cc:.3f}")
if p.idx == 6:
processed_lines.append(f"20、点P6的邻域平均距离:{u:.3f}")
processed_lines.append(f"21、点P6的邻域距离标准差:{cc:.3f}")
# (3)计算所有点的平均距离的均值u和标准差cc
all_u_mean = sum(all_u) / n
all_cc_mean = sum(all_cc) / n
processed_lines.append(f"22、全局平均距离均值u:{all_u_mean:.3f}")
processed_lines.append(f"23、全局距离标准差:{all_cc_mean:.3f}")
# 2.4去噪结果
flag_lst = [0] * len(all_u)
flag = all_u_mean + 2 * all_cc_mean
for i, ui in enumerate(all_u):
if ui > flag:
flag_lst[i] = 1 # 噪声点标记为1
noised_point_cnt = sum(flag_lst)
remain_cnt = len(all_u) - noised_point_cnt
processed_lines.append(f"24、点P1是否为噪声点:{flag_lst[0]}")
processed_lines.append(f"25、点P6是否为噪声点:{flag_lst[5]}")
processed_lines.append(f"26、去噪的噪声点总数:{noised_point_cnt}")
processed_lines.append(f"27、去噪后保留的点云总数:{remain_cnt}")
self.result_text_area.setText("\n".join(processed_lines))
self.status_label.setText("✅数据处理成功!")
except Exception as e:
self.status_label.setText(f"❌数据处理失败:{e}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = App()
window.show()
sys.exit(app.exec_())

四、个人理解
这道题应该学会:格网索引哈希优化技巧、点云去噪思想。