0xZohar commited on
Commit
e3e6ebe
·
verified ·
1 Parent(s): b196139

Add code/cube3d/training/group_objs_by_geometry.py

Browse files
code/cube3d/training/group_objs_by_geometry.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import open3d as o3d
4
+ import shutil
5
+ from collections import defaultdict
6
+ import pandas as pd
7
+
8
+ # --------------------------
9
+ # 配置参数(根据需求调整)
10
+ # --------------------------
11
+ # 包围盒筛选阈值(差异比例,0-1)
12
+ BBOX_DIMENSION_TOLERANCE = 0.2 # 长宽高单个维度差异不超过20%
13
+ BBOX_VOLUME_TOLERANCE = 0.3 # 总体积差异不超过30%
14
+
15
+ # ICP参数
16
+ ICP_DISTANCE_THRESHOLD = 0.05 # ICP匹配距离阈值
17
+ ICP_MAX_ITERATIONS = 100 # ICP最大迭代次数
18
+ SIMILARITY_THRESHOLD = 0.9 # 几何相似度阈值(0-1)
19
+
20
+ # 点云参数
21
+ SAMPLE_POINT_NUM = 1000 # 点云采样点数
22
+
23
+
24
+ def load_obj_and_calculate_bbox(obj_path):
25
+ """加载OBJ模型,计算包围盒(AABB)和点云(带归一化)"""
26
+ try:
27
+ # 加载网格模型
28
+ mesh = o3d.io.read_triangle_mesh(obj_path)
29
+ if not mesh.has_triangles():
30
+ print(f"警告:{os.path.basename(obj_path)} 无三角面,无法处理")
31
+ return None, None, None
32
+
33
+ # 1. 计算轴对齐包围盒(AABB)
34
+ bbox = mesh.get_axis_aligned_bounding_box()
35
+ bbox_min = bbox.min_bound # [min_x, min_y, min_z]
36
+ bbox_max = bbox.max_bound # [max_x, max_y, max_z]
37
+
38
+ # 计算包围盒尺寸(长宽高)和体积
39
+ bbox_dimensions = bbox_max - bbox_min # [dx, dy, dz]
40
+ bbox_volume = np.prod(bbox_dimensions) # 体积 = dx*dy*dz
41
+
42
+ # 2. 生成点云并预处理(归一化,为ICP做准备)
43
+ pcd = mesh.sample_points_uniformly(number_of_points=SAMPLE_POINT_NUM)
44
+ pcd, _ = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
45
+
46
+ # 归一化:平移到原点 + 缩放到单位球
47
+ pcd_center = pcd.get_center()
48
+ pcd.translate(-pcd_center)
49
+ pcd_scale = np.max(np.linalg.norm(np.asarray(pcd.points), axis=1))
50
+ if pcd_scale > 1e-6:
51
+ pcd.scale(1 / pcd_scale, center=np.zeros(3))
52
+
53
+ return bbox_dimensions, bbox_volume, pcd
54
+
55
+ except Exception as e:
56
+ print(f"错误:处理 {os.path.basename(obj_path)} 失败 - {str(e)}")
57
+ return None, None, None
58
+
59
+
60
+ def is_bbox_similar(bbox_dim1, vol1, bbox_dim2, vol2):
61
+ """判断两个包围盒是否相似(尺寸和体积差异在阈值内)"""
62
+ # 检查单个维度差异(dx, dy, dz)
63
+ dim_diff = np.abs(bbox_dim1 - bbox_dim2) / np.maximum(bbox_dim1, bbox_dim2)
64
+ if np.any(dim_diff > BBOX_DIMENSION_TOLERANCE):
65
+ return False # 任一维度差异过大
66
+
67
+ # 检查体积差异
68
+ vol_diff = abs(vol1 - vol2) / max(vol1, vol2)
69
+ if vol_diff > BBOX_VOLUME_TOLERANCE:
70
+ return False # 体积差异过大
71
+
72
+ return True # 包围盒相似
73
+
74
+
75
+ def calculate_icp_similarity(pcd1, pcd2):
76
+ """ICP计算点云相似度"""
77
+ icp_result = o3d.pipelines.registration.registration_icp(
78
+ source=pcd1,
79
+ target=pcd2,
80
+ max_correspondence_distance=ICP_DISTANCE_THRESHOLD,
81
+ init=np.eye(4),
82
+ estimation_method=o3d.pipelines.registration.TransformationEstimationPointToPoint(),
83
+ criteria=o3d.pipelines.registration.ICPConvergenceCriteria(
84
+ relative_fitness=1e-6,
85
+ relative_rmse=1e-6,
86
+ max_iteration=ICP_MAX_ITERATIONS
87
+ )
88
+ )
89
+
90
+ # 计算相似度(1-归一化距离)
91
+ avg_distance = icp_result.transformation_db
92
+ similarity = max(0.0, 1.0 - (avg_distance / ICP_DISTANCE_THRESHOLD))
93
+ return similarity
94
+
95
+
96
+ def group_objs_by_geometry(input_dir):
97
+ """先通过包围盒筛选,再用ICP分组"""
98
+ obj_info = []
99
+ print(f"开始加载 {input_dir} 下的OBJ文件并计算包围盒...")
100
+
101
+ # 1. 加载所有OBJ文件的信息(包围盒+点云)
102
+ for root, _, files in os.walk(input_dir):
103
+ for file in files:
104
+ if file.lower().endswith('.obj'):
105
+ obj_path = os.path.join(root, file)
106
+ bbox_dim, bbox_vol, pcd = load_obj_and_calculate_bbox(obj_path)
107
+
108
+ if bbox_dim is not None and pcd is not None and len(pcd.points) > 100:
109
+ obj_info.append({
110
+ "path": obj_path,
111
+ "name": file,
112
+ "bbox_dim": bbox_dim,
113
+ "bbox_vol": bbox_vol,
114
+ "pcd": pcd
115
+ })
116
+ print(f" 已加载:{file} → 包围盒尺寸 {bbox_dim.round(2)},体积 {bbox_vol:.2f}")
117
+
118
+ if len(obj_info) < 2:
119
+ print(f"提示:仅找到 {len(obj_info)} 个有效文件,无需分组")
120
+ return {0: [obj_info[0]["path"]]} if obj_info else {}
121
+
122
+ # 2. 按几何分组(先包围盒筛选,再ICP验证)
123
+ groups = defaultdict(list)
124
+ ungrouped = obj_info.copy()
125
+ group_id = 0
126
+
127
+ print(f"\n开始分组(共 {len(ungrouped)} 个文件)...")
128
+ while ungrouped:
129
+ base = ungrouped.pop(0)
130
+ groups[group_id].append(base["path"])
131
+ print(f"\n组 {group_id}:以 {base['name']} 为基准(尺寸 {base['bbox_dim'].round(2)})")
132
+
133
+ to_remove = []
134
+ for idx, candidate in enumerate(ungrouped):
135
+ # 第一步:包围盒筛选(直接比较尺寸和体积)
136
+ if not is_bbox_similar(
137
+ base["bbox_dim"], base["bbox_vol"],
138
+ candidate["bbox_dim"], candidate["bbox_vol"]
139
+ ):
140
+ # 包围盒差异过大,跳过ICP
141
+ print(f" 包围盒不匹配:{candidate['name']}(尺寸 {candidate['bbox_dim'].round(2)})→ 跳过")
142
+ continue
143
+
144
+ # 第二步:ICP验证几何形状
145
+ similarity = calculate_icp_similarity(base["pcd"], candidate["pcd"])
146
+ print(f" ICP匹配 {candidate['name']} → 相似度 {similarity:.3f}(阈值 {SIMILARITY_THRESHOLD})")
147
+
148
+ if similarity >= SIMILARITY_THRESHOLD:
149
+ groups[group_id].append(candidate["path"])
150
+ to_remove.append(idx)
151
+
152
+ # 移除已加入组的文件
153
+ for idx in sorted(to_remove, reverse=True):
154
+ removed = ungrouped.pop(idx)
155
+ print(f" 加入组 {group_id}:{removed['name']}")
156
+
157
+ group_id += 1
158
+
159
+ return groups
160
+
161
+
162
+ def merge_objs_by_geometry(input_dir, output_dir):
163
+ """合并几何相似的OBJ文件,每组保留第一个文件"""
164
+ groups = group_objs_by_geometry(input_dir)
165
+ if not groups:
166
+ print("未生成任何分组")
167
+ return
168
+
169
+ # 清空并创建输出目录
170
+ if os.path.exists(output_dir):
171
+ shutil.rmtree(output_dir)
172
+ os.makedirs(output_dir, exist_ok=True)
173
+
174
+ # 复制每组第一个文件
175
+ report = []
176
+ for group_id, paths in sorted(groups.items()):
177
+ rep_path = paths[0]
178
+ rep_name = os.path.basename(rep_path)
179
+ shutil.copy2(rep_path, os.path.join(output_dir, rep_name))
180
+ report.append({
181
+ "group_id": group_id,
182
+ "representative": rep_name,
183
+ "count": len(paths),
184
+ "files": [os.path.basename(p) for p in paths]
185
+ })
186
+ print(f"组 {group_id} 保留代表性文件:{rep_name}(共 {len(paths)} 个)")
187
+
188
+ # 生成报告
189
+ with open(os.path.join(output_dir, "merge_report.txt"), "w", encoding="utf-8") as f:
190
+ f.write("OBJ零件合并报告(包围盒+ICP)\n")
191
+ f.write(f"日期:{pd.Timestamp.now().strftime('%Y-%m-%d %H:%M')}\n")
192
+ f.write(f"包围盒阈值:尺寸差异≤{BBOX_DIMENSION_TOLERANCE*100}%,体积差异≤{BBOX_VOLUME_TOLERANCE*100}%\n")
193
+ f.write(f"ICP相似度阈值:≥{SIMILARITY_THRESHOLD}\n\n")
194
+ for item in report:
195
+ f.write(f"组 {item['group_id']}:\n")
196
+ f.write(f" 代表性文件:{item['representative']}\n")
197
+ f.write(f" 包含文件数:{item['count']}\n")
198
+ f.write(f" 文件列表:{', '.join(item['files'])}\n\n")
199
+
200
+ print(f"\n合并完成!输出目录:{output_dir},报告:merge_report.txt")
201
+
202
+
203
+ if __name__ == "__main__":
204
+ # 路径配置
205
+ INPUT_DIR = "/public/home/wangshuo/gap/assembly/data/obj_merged" # 输入OBJ目录
206
+ OUTPUT_DIR = "/public/home/wangshuo/gap/assembly/data/obj_geo_merged" # 输出目录
207
+
208
+ # 检查依赖
209
+ try:
210
+ import pandas as pd
211
+ o3d.__version__
212
+ except (ImportError, AttributeError):
213
+ print("请先安装依赖:pip install open3d pandas")
214
+ exit(1)
215
+
216
+ merge_objs_by_geometry(INPUT_DIR, OUTPUT_DIR)