import tkinter as tk
from tkinter import ttk, filedialog
import open3d as o3d
import numpy as np
from scipy.optimize import least_squares
class EnhancedShapeAnalyzer:
def __init__(self):
self.pcd = None
self.raw_pcd = None
self.transform = np.eye(4)
self.results = {"type": "unknown", "confidence": 0.0}
self.camera_params = {
"focal_length": 500, # 像素单位
"sensor_width": 36, # 毫米
"max_offset": 0.2, # 最大中心偏移(米)
"depth_base": 1000 # 基准深度(毫米)
}
def load_data(self, folder_path):
"""加载并预处理数据"""
try:
# 加载原始数据
self.raw_pcd = o3d.io.read_point_cloud(f"{folder_path}/scan.ply")
transform_data = np.load(f"{folder_path}/transform.npz")
self.transform = transform_data['matrix']
# 应用变换并中心化
self.pcd = self.raw_pcd.transform(np.linalg.inv(self.transform))
self._center_normalization()
return True
except Exception as e:
print(f"数据加载失败: {str(e)}")
return False
def _center_normalization(self):
"""将点云中心对齐到坐标系原点"""
points = np.asarray(self.pcd.points)
current_center = np.mean(points, axis=0)
self.pcd.translate(-current_center)
print(f"中心化偏移量: {current_center}")
def _perspective_correction(self, raw_size, depth):
"""透视尺寸校正"""
depth_mm = depth * 1000 # 转换为毫米
scale_factor = self.camera_params["focal_length"] / \
(self.camera_params["focal_length"] +
(depth_mm - self.camera_params["depth_base"]) / self.camera_params["sensor_width"])
return raw_size * scale_factor
def analyze_shape(self):
"""执行形状分析流程"""
if not self._preprocess():
return "预处理失败"
self.results = {"type": "unknown", "confidence": 0.0}
# 并行检测所有形状
candidates = []
if sphere_conf := self._detect_sphere():
candidates.append(("sphere", self.results.copy()))
if cylinder_conf := self._detect_cylinder():
candidates.append(("cylinder", self.results.copy()))
if cuboid_conf := self._detect_cuboid():
candidates.append(("cuboid", self.results.copy()))
# 选择最佳结果
valid_candidates = [c for c in candidates if c[1]["confidence"] >= 0.5]
if valid_candidates:
best = max(valid_candidates, key=lambda x: x[1]["confidence"])
self.results = best[1]
return best[0]
return "unknown"
def _preprocess(self):
"""数据预处理流程"""
try:
# 多阶段降采样
self.pcd = self.pcd.voxel_down_sample(0.005)
self.pcd = self.pcd.voxel_down_sample(0.003)
# 统计滤波
self.pcd, _ = self.pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=1.5)
return True
except Exception as e:
print(f"预处理失败: {str(e)}")
return False
def _detect_sphere(self):
"""精密球体检测"""
try:
# 分割平面点云
_, inliers = self.pcd.segment_plane(0.01, 3, 1000)
sphere_pcd = self.pcd.select_by_index(inliers, invert=True)
points = np.asarray(sphere_pcd.points)
if len(points) < 100:
return 0.0
# 最小二乘优化
def sphere_loss(params):
cx, cy, cz, r = params
return np.linalg.norm(points - [cx, cy, cz], axis=1) - r
init_center = np.mean(points, axis=0)
init_radius = np.mean(np.linalg.norm(points - init_center, axis=1))
opt = least_squares(sphere_loss, [*init_center, init_radius], loss='soft_l1')
# 中心验证
if not self._validate_center(opt.x[:3]):
print("球体中心超出允许范围")
return 0.0
# 透视校正
corrected_radius = self._perspective_correction(opt.x[3], opt.x[2])
# 置信度计算
residuals = sphere_loss(opt.x)
inlier_ratio = np.sum(np.abs(residuals) < 0.02) / len(residuals)
center_score = 1 - np.linalg.norm(opt.x[:3]) / self.camera_params["max_offset"]
confidence = 0.6 * inlier_ratio + 0.4 * center_score
if confidence > self.results["confidence"]:
self.results = {
"type": "sphere",
"radius": float(corrected_radius),
"center": opt.x[:3].tolist(),
"confidence": float(confidence),
"_points": points
}
return confidence
except Exception as e:
print(f"球体检测异常: {str(e)}")
return 0.0
def _detect_cylinder(self):
"""精密圆柱检测"""
try:
# 分割平面点云
_, inliers = self.pcd.segment_plane(0.01, 3, 1000)
cyl_pcd = self.pcd.select_by_index(inliers, invert=True)
points = np.asarray(cyl_pcd.points)
if len(points) < 100:
return 0.0
# PCA分析主方向
cov = np.cov(points.T)
_, vecs = np.linalg.eigh(cov)
axis = vecs[:, 2]
# 非线性优化
def cylinder_loss(params):
a, b, c, x0, y0, z0, r = params
dir_vec = np.array([a, b, c])
dir_vec /= np.linalg.norm(dir_vec) + 1e-8
pts_vec = points - [x0, y0, z0]
cross = np.cross(pts_vec, dir_vec)
return np.linalg.norm(cross, axis=1) - r
init_center = np.mean(points, axis=0)
opt = least_squares(cylinder_loss, [*axis, *init_center, 0.1], loss='soft_l1')
# 参数提取
axis = opt.x[:3] / (np.linalg.norm(opt.x[:3]) + 1e-8)
proj = np.dot(points - opt.x[3:6], axis)
height = np.ptp(proj)
center = opt.x[3:6] + axis * (np.min(proj) + height / 2)
# 中心验证
if not self._validate_center(center):
print("圆柱中心超出允许范围")
return 0.0
# 透视校正
corrected_radius = self._perspective_correction(opt.x[6], center[2])
corrected_height = self._perspective_correction(height, center[2])
# 置信度计算
residuals = cylinder_loss(opt.x)
inlier_ratio = np.sum(np.abs(residuals) < 0.015) / len(residuals)
center_score = 1 - np.linalg.norm(center) / self.camera_params["max_offset"]
confidence = 0.5 * inlier_ratio + 0.3 * center_score + 0.2 * (corrected_height / (2 * corrected_radius))
if confidence > self.results["confidence"]:
self.results = {
"type": "cylinder",
"radius": float(corrected_radius),
"height": float(corrected_height),
"axis": axis.tolist(),
"center": center.tolist(),
"confidence": float(confidence),
"_points": points
}
return confidence
except Exception as e:
print(f"圆柱检测异常: {str(e)}")
return 0.0
def _detect_cuboid(self):
"""精密长方体检测"""
try:
# 多平面检测
planes = []
residual_pcd = self.pcd
for _ in range(3):
plane, inliers = residual_pcd.segment_plane(0.02, 3, 2000)
if len(inliers) < 500:
break
planes.append(plane[:3])
residual_pcd = residual_pcd.select_by_index(inliers, invert=True)
if len(planes) < 3:
return 0.0
# 正交化处理
u, v, w = self._gram_schmidt(planes[0], planes[1], planes[2])
points = np.asarray(self.pcd.points)
# 计算几何参数
proj_u = np.dot(points, u)
proj_v = np.dot(points, v)
proj_w = np.dot(points, w)
dims = [np.ptp(proj_u), np.ptp(proj_v), np.ptp(proj_w)]
center_3d = np.dot([np.mean(proj_u), np.mean(proj_v), np.mean(proj_w)], [u, v, w])
# 中心验证
if not self._validate_center(center_3d):
print("长方体中心超出允许范围")
return 0.0
# 透视校正
corrected_dims = [self._perspective_correction(d, center_3d[2]) for d in dims]
# 置信度计算
ortho_score = 1 - (abs(np.dot(u, v)) + abs(np.dot(u, w)) + abs(np.dot(v, w))) / 3
center_score = 1 - np.linalg.norm(center_3d) / self.camera_params["max_offset"]
confidence = 0.6 * ortho_score + 0.4 * center_score
if confidence > self.results["confidence"]:
self.results = {
"type": "cuboid",
"dimensions": corrected_dims,
"center": center_3d.tolist(),
"confidence": float(confidence),
"_planes": planes
}
return confidence
except Exception as e:
print(f"长方体检测异常: {str(e)}")
return 0.0
def _validate_center(self, center):
"""验证中心位置有效性"""
return np.linalg.norm(center) < self.camera_params["max_offset"]
def _gram_schmidt(self, u, v, w):
"""Gram-Schmidt正交化"""
u = u / (np.linalg.norm(u) + 1e-8)
v = v - np.dot(v, u) * u
v = v / (np.linalg.norm(v) + 1e-8)
w = w - np.dot(w, u) * u - np.dot(w, v) * v
w = w / (np.linalg.norm(w) + 1e-8)
return u, v, w
class AnalysisGUI(tk.Tk):
def __init__(self):
super().__init__()
self.title("三维精密分析系统 v4.0")
self.geometry("1280x960")
self.analyzer = EnhancedShapeAnalyzer()
self._init_ui()
def _init_ui(self):
main_frame = ttk.Frame(self)
main_frame.pack(fill="both", expand=True, padx=20, pady=20)
# 文件选择区
file_frame = ttk.LabelFrame(main_frame, text="数据加载")
file_frame.pack(fill="x", pady=10)
self.path_entry = ttk.Entry(file_frame, width=80)
self.path_entry.pack(side="left", expand=True, padx=5)
ttk.Button(file_frame, text="浏览...", command=self._browse).pack(side="left")
# 控制区
ctrl_frame = ttk.Frame(main_frame)
ctrl_frame.pack(fill="x", pady=10)
ttk.Button(ctrl_frame, text="开始分析", command=self._analyze).pack(side="left", padx=5)
ttk.Button(ctrl_frame, text="原始点云", command=lambda: self._show_pcd(self.analyzer.raw_pcd)).pack(side="left")
ttk.Button(ctrl_frame, text="处理点云", command=lambda: self._show_pcd(self.analyzer.pcd)).pack(side="left")
ttk.Button(ctrl_frame, text="显示结果", command=self._show_result).pack(side="left")
# 结果显示区
result_frame = ttk.LabelFrame(main_frame, text="分析结果")
result_frame.pack(fill="both", expand=True)
self.result_text = tk.Text(result_frame, wrap="word", font=("Consolas", 12))
scrollbar = ttk.Scrollbar(result_frame, orient="vertical", command=self.result_text.yview)
self.result_text.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side="right", fill="y")
self.result_text.pack(fill="both", expand=True, padx=10, pady=10)
# 状态栏
self.status = ttk.Label(main_frame, relief="sunken")
self.status.pack(fill="x", pady=5)
def _browse(self):
path = filedialog.askdirectory()
if path:
self.path_entry.delete(0, tk.END)
self.path_entry.insert(0, path)
def _analyze(self):
self.status.config(text="分析中...")
self.result_text.config(state="normal")
self.result_text.delete(1.0, tk.END)
if not self.analyzer.load_data(self.path_entry.get()):
self._update_result("数据加载失败")
return
shape_type = self.analyzer.analyze_shape()
res = self.analyzer.results
output = [
"=== 三维精密分析结果 ===",
f"识别类型: {shape_type.upper()}",
f"综合置信度: {res.get('confidence', 0):.1%}",
f"中心坐标: {np.round(res.get('center', [0, 0, 0]), 3).tolist()}"
]
if shape_type == "sphere":
output.extend([
f"\n球体半径: {res['radius']:.3f} 米",
f"表面点数: {len(res.get('_points', []))}"
])
elif shape_type == "cylinder":
output.extend([
f"\n圆柱半径: {res['radius']:.3f} 米",
f"圆柱高度: {res['height']:.3f} 米",
f"轴向向量: {np.round(res.get('axis', [0, 0, 0]), 3).tolist()}"
])
elif shape_type == "cuboid":
output.extend([
"\n长方体尺寸:",
f"X: {res['dimensions'][0]:.3f} 米",
f"Y: {res['dimensions'][1]:.3f} 米",
f"Z: {res['dimensions'][2]:.3f} 米"
])
self.result_text.insert(tk.END, "\n".join(output))
self.result_text.config(state="disabled")
self.status.config(text=f"分析完成 - 置信度: {res.get('confidence', 0):.1%}")
def _show_pcd(self, pcd):
"""显示点云可视化"""
try:
if pcd.is_empty():
return
vis = o3d.visualization.Visualizer()
vis.create_window(window_name="点云视图", width=800, height=600)
# 克隆点云以避免修改原始数据
cloned_pcd = o3d.geometry.PointCloud(pcd)
cloned_pcd.paint_uniform_color([0.6, 0.6, 0.6])
vis.add_geometry(cloned_pcd)
self._add_center_marker(vis)
vis.run()
vis.destroy_window()
except Exception as e:
print(f"可视化错误: {str(e)}")
def _show_result(self):
"""显示分析结果的可视化"""
try:
res = self.analyzer.results
if res["confidence"] < 0.5:
self._update_result("无有效分析结果")
return
vis = o3d.visualization.Visualizer()
vis.create_window(window_name="三维分析结果", width=1000, height=800)
# 显示处理后的点云
base_pcd = self.analyzer.pcd.paint_uniform_color([0.8, 0.8, 0.8])
vis.add_geometry(base_pcd)
# 显示检测结果
if res["type"] == "sphere":
self._show_sphere_result(vis, res)
elif res["type"] == "cylinder":
self._show_cylinder_result(vis, res)
elif res["type"] == "cuboid":
self._show_cuboid_result(vis, res)
# 添加中心标记和坐标系
self._add_center_marker(vis)
coord_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.5)
vis.add_geometry(coord_frame)
vis.run()
vis.destroy_window()
except Exception as e:
print(f"结果可视化失败: {str(e)}")
def _show_sphere_result(self, vis, res):
"""显示球体检测结果"""
sphere = o3d.geometry.TriangleMesh.create_sphere(res["radius"])
sphere.translate(res["center"])
sphere.paint_uniform_color([1, 0, 0])
vis.add_geometry(sphere)
# 显示特征点
inlier_pcd = o3d.geometry.PointCloud()
inlier_pcd.points = o3d.utility.Vector3dVector(res["_points"])
inlier_pcd.paint_uniform_color([1, 0.5, 0])
vis.add_geometry(inlier_pcd)
def _show_cylinder_result(self, vis, res):
"""显示圆柱检测结果"""
cylinder = o3d.geometry.TriangleMesh.create_cylinder(
radius=res["radius"],
height=res["height"]
)
# 计算旋转矩阵
axis = np.array(res["axis"])
z_axis = np.array([0, 0, 1])
rotation = self._get_rotation_matrix(axis, z_axis)
cylinder.rotate(rotation)
# 调整位置到底面中心
cylinder.translate(np.array(res["center"]) - axis * res["height"] / 2)
cylinder.paint_uniform_color([0, 0.8, 0])
vis.add_geometry(cylinder)
def _show_cuboid_result(self, vis, res):
"""显示长方体检测结果"""
obb = o3d.geometry.OrientedBoundingBox.create_from_points(
o3d.utility.Vector3dVector(np.asarray(self.analyzer.pcd.points))
)
obb.color = [0, 0, 1]
vis.add_geometry(obb)
def _add_center_marker(self, vis):
"""添加中心标记"""
center_sphere = o3d.geometry.TriangleMesh.create_sphere(radius=0.03)
center_sphere.paint_uniform_color([1, 1, 0]) # 黄色
vis.add_geometry(center_sphere)
def _get_rotation_matrix(self, from_vec, to_vec):
"""计算两个向量间的旋转矩阵"""
from_vec = from_vec / np.linalg.norm(from_vec)
to_vec = to_vec / np.linalg.norm(to_vec)
v = np.cross(from_vec, to_vec)
c = np.dot(from_vec, to_vec)
kmat = np.array([[0, -v[2], v[1]],
[v[2], 0, -v[0]],
[-v[1], v[0], 0]])
return np.eye(3) + kmat + kmat.dot(kmat) * (1 / (1 + c))
def _update_result(self, msg):
"""更新结果显示"""
self.result_text.config(state="normal")
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, msg)
self.result_text.config(state="disabled")
self.status.config(text="操作完成")
if __name__ == "__main__":
app = AnalysisGUI()
app.mainloop()
在运行时代码总是把一整个场景都识别进去判断了,如果把墙体和平面去掉,只留下物体的点云会不会有效果呢