denosie_downsampling_ply.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import open3d as o3d
  2. import numpy as np
  3. import os
  4. import shutil
  5. import zipfile
  6. import json
  7. from typing import Dict, Any
  8. import cv2
  9. import argparse
  10. # from pointclound_downsampling import run
  11. # 定义屋顶截断的 Z 轴最大阈值。
  12. # !!! 请根据您的点云数据特性调整这个值 !!!
  13. Z_MAX_THRESHOLD = 0.8 # 假设墙体最高点不超过 0.5 米
  14. Z_MIN_THRESHOLD = -1.7
  15. def generate_floor_json(width: int, height: int, floor_id: int = 0, name: str = "1楼") -> Dict[str, Any]:
  16. x_bound = width / 100.0
  17. y_bound = height / 100.0
  18. floor_data = {
  19. "id": floor_id,
  20. "subgroup": 0,
  21. "name": name,
  22. "resolution": {
  23. "width": width,
  24. "height": height
  25. },
  26. "bound": {
  27. # 边界范围从负值到正值对称
  28. "x_min": -x_bound,
  29. "x_max": x_bound,
  30. "y_min": -y_bound,
  31. "y_max": y_bound
  32. }
  33. }
  34. return {
  35. "floors": [floor_data]
  36. }
  37. def save_json_to_file(data: Dict[str, Any], filename: str = "output.json"):
  38. """
  39. 将 Python 字典写入格式化的 JSON 文件。
  40. """
  41. try:
  42. with open(filename, 'w', encoding='utf-8') as f:
  43. # indent=4 用于美化输出,ensure_ascii=False 确保中文正常显示
  44. json.dump(data, f, indent=4, ensure_ascii=False)
  45. print(f"✅ JSON 文件已成功保存为: {filename}")
  46. except IOError as e:
  47. print(f"❌ 写入文件时发生错误: {e}")
  48. def build_floor_transform_matrix(j_info: dict, floor_id: int):
  49. """
  50. 遍历 JSON 数据中的楼层信息,查找匹配的 floor_id,并构建 3x3 仿射变换矩阵。
  51. 此版本返回逆矩阵,用于从世界坐标到归一化坐标的转换。
  52. """
  53. tab = [[0.0] * 3 for _ in range(3)]
  54. res_width = None
  55. res_height = None
  56. for in_json in j_info.get("floors", []):
  57. floor_id_in_json = in_json.get("id")
  58. if floor_id_in_json != floor_id:
  59. continue
  60. res_width = in_json.get("resolution", {}).get("width")
  61. res_height = in_json.get("resolution", {}).get("height")
  62. x_min = in_json.get("bound", {}).get("x_min")
  63. x_max = in_json.get("bound", {}).get("x_max")
  64. y_min = in_json.get("bound", {}).get("y_min")
  65. y_max = in_json.get("bound", {}).get("y_max")
  66. # 用于从归一化 [0,1] 空间转换到世界坐标空间
  67. tab[0][0] = x_max - x_min
  68. tab[0][1] = 0.0
  69. tab[0][2] = x_min
  70. tab[1][0] = 0.0
  71. tab[1][1] = y_min - y_max
  72. tab[1][2] = y_max
  73. tab[2][0] = 0.0
  74. tab[2][1] = 0.0
  75. tab[2][2] = 1.0
  76. break
  77. if res_width is None:
  78. # 未找到楼层,返回单位矩阵和None
  79. return np.identity(3).tolist(), None, None
  80. # 逆矩阵
  81. tab_array = np.array(tab, dtype=np.float64)
  82. if np.linalg.det(tab_array) == 0:
  83. raise ValueError("矩阵是奇异的,无法求逆。")
  84. tab_inverse_array = np.linalg.inv(tab_array)
  85. tab_inverse = tab_inverse_array.tolist()
  86. return tab_inverse, res_width, res_height
  87. def zip_folder(folder_path, output_path):
  88. with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
  89. for root, dirs, files in os.walk(folder_path):
  90. for file in files:
  91. file_path = os.path.join(root, file)
  92. zipf.write(file_path, os.path.relpath(file_path, folder_path))
  93. def extract_ply_data(ply_file_path, output_dir, scene_info_json_path, floor_id=0, voxel_size=0.03):
  94. # 1. 读取 PLY 文件
  95. print(f"正在读取文件: {ply_file_path}...")
  96. pcd = o3d.io.read_point_cloud(ply_file_path)
  97. original_num_points = len(pcd.points)
  98. print(f" - 原始点数: {original_num_points}")
  99. max_z = pcd.get_max_bound()[2]
  100. min_z = pcd.get_min_bound()[2]
  101. print(f" - 原始 Z 轴范围: [{min_z:.4f}, {max_z:.4f}]")
  102. # 1.1. 获取变换矩阵
  103. if not os.path.exists(scene_info_json_path):
  104. raise FileNotFoundError(f"找不到场景信息JSON文件: {scene_info_json_path}")
  105. with open(scene_info_json_path, 'r', encoding='utf-8') as f:
  106. j_info = json.load(f)
  107. matrix, res_w, res_h = build_floor_transform_matrix(j_info, floor_id)
  108. if res_w is None:
  109. raise ValueError(f"在 {scene_info_json_path} 中找不到 floor_id {floor_id}")
  110. M = np.array(matrix, dtype=np.float64)
  111. # 1.2. 屋顶和地板截断:基于 Z 轴和XY投影进行裁剪
  112. print(f"正在进行截断 (Z 轴和XY平面)...")
  113. # 转换为 NumPy 数组以便创建布尔掩码
  114. points_np = np.asarray(pcd.points)
  115. # Z 轴筛选的布尔掩码
  116. z_mask = (points_np[:, 2] < Z_MAX_THRESHOLD) & (points_np[:, 2] > Z_MIN_THRESHOLD)
  117. # X/Y 轴筛选的布尔掩码
  118. homogeneous_points = np.hstack((points_np[:, :2], np.ones((points_np.shape[0], 1))))
  119. normalized_points = homogeneous_points @ M.T
  120. pixel_coords = normalized_points[:, :2] * np.array([res_w, res_h])
  121. xy_mask = (pixel_coords[:, 0] >= 0) & (pixel_coords[:, 0] < res_w) & \
  122. (pixel_coords[:, 1] >= 0) & (pixel_coords[:, 1] < res_h)
  123. # 合并掩码
  124. combined_mask = z_mask & xy_mask
  125. inlier_indices = np.where(combined_mask)[0]
  126. # 使用 select_by_index 方法同步裁剪所有属性
  127. pcd = pcd.select_by_index(inlier_indices)
  128. cut_num_points = len(pcd.points)
  129. print(f" - 截断后点数: {cut_num_points}")
  130. ######################################################
  131. # # # 1.2. 下采样到截断后点数的 1/10
  132. # sampling_ratio = 0.1
  133. # pcd = pcd.random_down_sample(sampling_ratio=sampling_ratio)
  134. ######################################################
  135. ######################################################
  136. # voxel_size = 0.03 # 0.02
  137. pcd = pcd.voxel_down_sample(voxel_size=voxel_size)
  138. print(f"已执行体素下采样,体素大小为: {voxel_size}")
  139. # MAX_POINTS = 550000
  140. MAX_POINTS = 500000
  141. num_points_after_voxel = len(pcd.points)
  142. print(f"体素下采样后点数: {num_points_after_voxel}")
  143. if num_points_after_voxel > MAX_POINTS:
  144. print(f" - 点数仍大于 {MAX_POINTS},正在进行第二阶段随机下采样...")
  145. indices_to_keep = np.random.choice(num_points_after_voxel, MAX_POINTS, replace=False)
  146. pcd = pcd.select_by_index(indices_to_keep)
  147. print(f" - 第二阶段下采样完成,最终点数: {len(pcd.points)}")
  148. ######################################################
  149. # o3d.io.write_point_cloud(output_dir+'/1.ply', pcd, write_ascii=True)
  150. ################************##########################
  151. # pcd = run(ply_file_path, "random")
  152. ################************###########################
  153. downsampled_num_points = len(pcd.points)
  154. print(f" - 下采样后点数: {downsampled_num_points}")
  155. print(f" - 实际保留比例 (相对于原始点数): {downsampled_num_points / original_num_points:.4f}")
  156. # 2. 提取数据并转换为 NumPy 数组
  157. # 提取坐标 (Points)
  158. points = np.asarray(pcd.points)
  159. points_npy_path = os.path.join(output_dir, 'coord.npy')
  160. np.save(points_npy_path, points)
  161. print(f" - 坐标信息 (形状: {points.shape}) 已保存到: {points_npy_path}")
  162. # 提取颜色 (Colors)
  163. if pcd.has_colors():
  164. colors = np.asarray(pcd.colors)
  165. colors_npy_path = os.path.join(output_dir, 'color.npy')
  166. np.save(colors_npy_path, colors)
  167. print(f" - 颜色信息 (形状: {colors.shape}, 范围: [0, 1]) 已保存到: {colors_npy_path}")
  168. else:
  169. # 如果 PLY 文件不包含颜色信息,则会打印此警告,这是正常现象。
  170. print(" - 警告: PLY 文件不包含颜色信息,跳过保存。")
  171. colors = None
  172. # 提取法线 (Normals)
  173. if pcd.has_normals():
  174. normals = np.asarray(pcd.normals)
  175. normals_npy_path = os.path.join(output_dir, 'normal.npy')
  176. np.save(normals_npy_path, normals)
  177. print(f" - 法线信息 (形状: {normals.shape}) 已保存到: {normals_npy_path}")
  178. else:
  179. # 如果 PLY 文件不包含法线信息,则会打印此警告,这是正常现象。
  180. print(" - 警告: PLY 文件不包含法线信息,跳过保存。")
  181. normals = None
  182. return points, colors, normals
  183. if __name__ == '__main__':
  184. parser = argparse.ArgumentParser(
  185. description="输入单个场景文件夹用于点云提取,分割和去噪",
  186. formatter_class=argparse.RawTextHelpFormatter
  187. )
  188. parser.add_argument(
  189. '-i',
  190. '--input_folder',
  191. type=str,
  192. required=True,
  193. help='指定输入场景的文件夹路径。'
  194. )
  195. args = parser.parse_args()
  196. scene_folder = args.input_folder
  197. scenece = os.path.basename(scene_folder)
  198. ply_file_path = os.path.join(scene_folder, "laser.ply")
  199. scene_info_json_path = os.path.join(scene_folder, f"{scenece}.json")
  200. floor_path = os.path.join(scene_folder, f"{scenece}.png")
  201. process_folder = os.path.join(scene_folder, 'scene/val/process_data')
  202. if not os.path.exists(process_folder):
  203. os.makedirs(process_folder)
  204. try:
  205. if not os.path.exists(ply_file_path):
  206. print(f"✅ 文件 '{ply_file_path}' 存在")
  207. except FileNotFoundError:
  208. raise FileNotFoundError(f"【文件缺失错误】无法找到点云文件:'{ply_file_path}'。请确保文件路径正确。")
  209. try:
  210. if not os.path.exists(floor_path):
  211. print(f"✅ 文件 '{floor_path}' 存在")
  212. except FileNotFoundError:
  213. raise FileNotFoundError(f"【文件缺失错误】无法找到平面图文件:'{floor_path}'。请确保文件路径正确。")
  214. if not os.path.exists(scene_info_json_path):
  215. img = cv2.imread(floor_path)
  216. height, width, _ = img.shape
  217. json_data = generate_floor_json(width, height)
  218. save_json_to_file(json_data, filename=scene_info_json_path)
  219. extract_ply_data(
  220. ply_file_path=ply_file_path,
  221. output_dir=process_folder,
  222. scene_info_json_path=scene_info_json_path,
  223. floor_id=0,
  224. voxel_size=0.03
  225. )
  226. data_file = os.path.join(scene_folder, "scene")
  227. zip_file = data_file + '.zip'
  228. zip_folder(data_file, zip_file)