analyze_poses.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 分析全景图点位,找出最靠近入户门的点位
  5. - 基于拍摄顺序(id 递增)和可见性关系
  6. - 将 3D 世界坐标映射到 2D 平面可视化
  7. - 用红色标记最靠近入户门的点,绿色标记其他点
  8. """
  9. import json
  10. import os
  11. import numpy as np
  12. import matplotlib.pyplot as plt
  13. from matplotlib.patches import Circle
  14. def load_vision_data(filepath):
  15. """加载 vision.txt 数据"""
  16. with open(filepath, 'r', encoding='utf-8') as f:
  17. content = f.read()
  18. # 找到 JSON 开始的位置
  19. start_idx = content.find('{')
  20. data = json.loads(content[start_idx:])
  21. return data
  22. def extract_poses(data):
  23. """提取所有点位的 pose 信息"""
  24. poses = []
  25. for sweep in data['sweepLocations']:
  26. pose_info = {
  27. 'id': sweep['id'],
  28. 'uuid': sweep['uuid'],
  29. 'x': sweep['pose']['translation']['x'],
  30. 'y': sweep['pose']['translation']['y'],
  31. 'z': sweep['pose']['translation']['z'],
  32. 'visibles': sweep.get('visibles', []),
  33. 'visibles2': sweep.get('visibles2', []),
  34. 'visibles3': sweep.get('visibles3', [])
  35. }
  36. poses.append(pose_info)
  37. return poses
  38. def find_entrance_point(poses):
  39. """
  40. 找出最靠近入户门的点位
  41. 逻辑:
  42. 1. 入户门位于边缘(几何中心最远的点)
  43. 2. 入户门在走廊上(可见性符合距离规律:近的能看见,远的看不见)
  44. 3. 房间点会被排除(近的点可能看不见,因为有墙遮挡)
  45. 关键洞察:
  46. - 走廊是开阔的,可见性主要由距离决定
  47. - 房间内有墙体,可见性会被墙阻挡
  48. - 如果"近的点看不见"但"远的点反而能看见",说明在房间内
  49. """
  50. # 计算几何中心
  51. center_x = sum(p['x'] for p in poses) / len(poses)
  52. center_y = sum(p['y'] for p in poses) / len(poses)
  53. # 计算所有点对之间的距离
  54. n = len(poses)
  55. distances = {} # distances[(id1, id2)] = 距离
  56. for i in range(n):
  57. for j in range(i + 1, n):
  58. p1, p2 = poses[i], poses[j]
  59. dx = p1['x'] - p2['x']
  60. dy = p1['y'] - p2['y']
  61. dist = np.sqrt(dx ** 2 + dy ** 2)
  62. distances[(p1['id'], p2['id'])] = dist
  63. distances[(p2['id'], p1['id'])] = dist
  64. # 对每个点,分析其可见性与距离的关系
  65. corridor_scores = {} # 走廊分数(越高越可能在走廊)
  66. for p in poses:
  67. pid = p['id']
  68. visibles = set(p['visibles'])
  69. # 收集该点到其他所有点的距离和可见性
  70. visible_distances = [] # 可见的点的距离
  71. invisible_distances = [] # 不可见的点的距离
  72. for other in poses:
  73. if other['id'] == pid:
  74. continue
  75. dist = distances[(pid, other['id'])]
  76. if other['id'] in visibles:
  77. visible_distances.append(dist)
  78. else:
  79. invisible_distances.append(dist)
  80. # 关键分析:
  81. # 1. 走廊点:可见的最大距离 > 不可见的最小距离(远的能看见,近的肯定能看见)
  82. # 2. 房间点:存在不可见的近点,但存在可见的远点
  83. if len(invisible_distances) == 0:
  84. # 所有点都可见,肯定是走廊中心点
  85. corridor_scores[pid] = 1.0
  86. continue
  87. min_invisible_dist = min(invisible_distances) # 最近的不可见点
  88. max_visible_dist = max(visible_distances) if visible_distances else 0 # 最远的可见点
  89. # 计算"走廊分数"
  90. # 如果 min_invisible_dist > max_visible_dist,说明可见性符合距离规律(走廊)
  91. # 如果 min_invisible_dist < max_visible_dist,说明有异常(房间)
  92. if max_visible_dist == 0:
  93. corridor_scores[pid] = 0.0 # 什么都看不见,可能在死角
  94. else:
  95. ratio = min_invisible_dist / max_visible_dist
  96. # ratio > 1: 走廊(所有不可见点都比可见点远)
  97. # ratio < 1: 房间(存在不可见的近点)
  98. corridor_scores[pid] = ratio / (1 + ratio) # 映射到 0-1
  99. # 计算边缘度(距离中心的距离)
  100. center_distances = {}
  101. for p in poses:
  102. dx = p['x'] - center_x
  103. dy = p['y'] - center_y
  104. center_distances[p['id']] = np.sqrt(dx ** 2 + dy ** 2)
  105. max_center_dist = max(center_distances.values())
  106. # 综合分数:走廊分数 * 边缘度
  107. entrance_scores = {}
  108. for p in poses:
  109. pid = p['id']
  110. edge_score = center_distances[pid] / max_center_dist
  111. corridor_score = corridor_scores[pid]
  112. # 入户门 = 边缘 + 走廊
  113. entrance_scores[pid] = edge_score * 0.6 + corridor_score * 0.4
  114. # 输出分析结果
  115. print("=== 几何中心 ===")
  116. print(f"中心坐标:({center_x:.3f}, {center_y:.3f})")
  117. print("\n=== 点位分析(可见性与距离关系)===")
  118. for pid in sorted(corridor_scores.keys()):
  119. p = next(po for po in poses if po['id'] == pid)
  120. visibles = p['visibles']
  121. visible_dists = [distances[(pid, vid)] for vid in visibles if (pid, vid) in distances]
  122. invisible_dists = [distances[(pid, oid)] for oid in
  123. [po['id'] for po in poses if po['id'] != pid and po['id'] not in visibles]]
  124. min_inv = min(invisible_dists) if invisible_dists else float('inf')
  125. max_vis = max(visible_dists) if visible_dists else 0
  126. print(f"点 {pid}: 可见{len(visibles)}个点," +
  127. f"最近不可见={min_inv:.2f}m, 最远可见={max_vis:.2f}m, " +
  128. f"走廊分数={corridor_scores[pid]:.3f}")
  129. print("\n=== 入口分数(边缘度 60% + 走廊分数 40%)===")
  130. for pid in sorted(entrance_scores.keys(), key=lambda x: entrance_scores[x], reverse=True):
  131. print(f"点 {pid}: 分数={entrance_scores[pid]:.3f}")
  132. entrance_id = max(entrance_scores, key=entrance_scores.get)
  133. print(f"\n>>> 最靠近入户门的点位:ID = {entrance_id}")
  134. return entrance_id
  135. def transform_to_world_coords(poses):
  136. """
  137. 将所有拍摄点位通过位姿转换到世界坐标系
  138. 对于每个 sweep,其 pose.translation 已经是世界坐标
  139. 这里统一以第一个点 (id=0) 作为参考原点,将所有点转换到局部坐标系
  140. Args:
  141. poses: 包含位姿信息的列表
  142. Returns:
  143. 转换后的点位列表
  144. """
  145. # 以第一个点作为坐标原点
  146. origin_pose = poses[0]
  147. origin_x = origin_pose['x']
  148. origin_y = origin_pose['y']
  149. origin_z = origin_pose['z']
  150. # 获取第一个点的旋转(用于方向对齐)
  151. origin_rot = origin_pose.get_rotation() if hasattr(origin_pose, 'get_rotation') else None
  152. transformed_poses = []
  153. for p in poses:
  154. # 平移变换:将原点移到第一个点
  155. world_x = p['x'] - origin_x
  156. world_y = p['y'] - origin_y
  157. world_z = p['z'] - origin_z # 保持高度信息
  158. transformed_poses.append({
  159. 'id': p['id'],
  160. 'x': world_x,
  161. 'y': world_y,
  162. 'z': world_z,
  163. 'visibles': p['visibles'],
  164. 'original_x': p['x'],
  165. 'original_y': p['y'],
  166. 'original_z': p['z']
  167. })
  168. print(f"\n=== 坐标变换 ===")
  169. print(f"参考原点 (ID=0): ({origin_x:.3f}, {origin_y:.3f}, {origin_z:.3f})")
  170. print(f"变换后所有点位偏移了:(-{origin_x:.3f}, -{origin_y:.3f}, -{origin_z:.3f})")
  171. return transformed_poses
  172. def project_to_2d(transformed_poses):
  173. """
  174. 将世界坐标系下的 3D 点映射到 XY 平面(俯视图)
  175. 忽略 Z 坐标,只保留 X, Y 用于平面绘制
  176. 根据 demo.png,直接使用原始坐标,不交换轴
  177. """
  178. points_2d = []
  179. for p in transformed_poses:
  180. points_2d.append({
  181. 'id': p['id'],
  182. 'x': p['x'], # 世界坐标系 X(横向)
  183. 'y': p['y'], # 世界坐标系 Y(纵向)
  184. 'z': p['z'], # 高度(用于参考)
  185. 'visibles': p['visibles']
  186. })
  187. return points_2d
  188. def visualize_poses(poses_2d, entrance_id, output_path):
  189. """可视化点位分布"""
  190. fig, ax = plt.subplots(figsize=(12, 10))
  191. # 提取所有坐标
  192. xs = [p['x'] for p in poses_2d]
  193. ys = [p['y'] for p in poses_2d]
  194. # 计算坐标范围
  195. x_min, x_max = min(xs), max(xs)
  196. y_min, y_max = min(ys), max(ys)
  197. # 设置坐标范围(带边距)
  198. x_margin = (x_max - x_min) * 0.1
  199. y_margin = (y_max - y_min) * 0.1
  200. ax.set_xlim(x_min - x_margin, x_max + x_margin)
  201. ax.set_ylim(y_min - y_margin, y_max + y_margin)
  202. # 保持 Y 轴方向与世界坐标系一致(上北下南)
  203. # ax.invert_yaxis() # 如果需要俯视效果可取消注释
  204. # 绘制连线(可见性关系)
  205. for p in poses_2d:
  206. for vid in p['visibles']:
  207. # 找到对应的点
  208. target = next((pt for pt in poses_2d if pt['id'] == vid), None)
  209. if target:
  210. ax.plot([p['x'], target['x']], [p['y'], target['y']],
  211. 'gray', linewidth=0.5, alpha=0.3)
  212. # 绘制点位
  213. for p in poses_2d:
  214. if p['id'] == entrance_id:
  215. # 入口点 - 红色实心圆
  216. circle = Circle((p['x'], p['y']), radius=0.3,
  217. color='red', fill=True,
  218. label='入口点' if p['id'] == entrance_id else '')
  219. ax.add_patch(circle)
  220. # 添加 ID 标签
  221. ax.annotate(f'ID:{p["id"]}', (p['x'], p['y']),
  222. textcoords="offset points", xytext=(0, 15),
  223. ha='center', color='red', fontsize=12, fontweight='bold')
  224. else:
  225. # 其他点 - 绿色实心圆
  226. circle = Circle((p['x'], p['y']), radius=0.2,
  227. color='green', fill=True)
  228. ax.add_patch(circle)
  229. # 添加 ID 标签
  230. ax.annotate(f'ID:{p["id"]}', (p['x'], p['y']),
  231. textcoords="offset points", xytext=(0, -12),
  232. ha='center', color='green', fontsize=9)
  233. # 添加图例
  234. red_circle = plt.Line2D([], [], marker='o', markersize=15,
  235. markerfacecolor='red', markeredgewidth=0,
  236. label='Entrance Point')
  237. green_circle = plt.Line2D([], [], marker='o', markersize=12,
  238. markerfacecolor='green', markeredgewidth=0,
  239. label='Other Points')
  240. ax.legend(handles=[red_circle, green_circle], loc='upper right')
  241. # 设置标签和标题
  242. ax.set_xlabel('X (meter)', fontsize=11)
  243. ax.set_ylabel('Y (meter)', fontsize=11)
  244. ax.set_title('Panoramic Point Distribution\n(Red=Near Entrance, Green=Other Points)', fontsize=13)
  245. ax.set_aspect('equal')
  246. ax.grid(True, alpha=0.3)
  247. plt.tight_layout()
  248. plt.savefig(output_path, dpi=150, bbox_inches='tight')
  249. print(f"\n可视化结果已保存至:{output_path}")
  250. return fig, ax
  251. def main(vision_file, output_image):
  252. print("加载数据...")
  253. data = load_vision_data(vision_file)
  254. print("提取点位信息...")
  255. poses = extract_poses(data)
  256. print(f"共 {len(poses)} 个点位")
  257. # 打印每个点位的详细信息
  258. print("\n=== 点位详情 ===")
  259. for p in poses:
  260. print(f"ID {p['id']:2d}: pos=({p['x']:8.3f}, {p['y']:8.3f}, {p['z']:8.3f}), visibles={p['visibles']}")
  261. # 找出入口点
  262. print("\n=== 分析入户门位置 ===")
  263. entrance_id = find_entrance_point(poses)
  264. # 投影到 2D
  265. poses_2d = project_to_2d(poses)
  266. # 可视化
  267. visualize_poses(poses_2d, entrance_id, output_image)
  268. # 打印结论
  269. print("\n" + "=" * 50)
  270. print("结 论:")
  271. print("=" * 50)
  272. entrance_point = next(p for p in poses if p['id'] == entrance_id)
  273. print(f"最靠近入户门的点位:ID = {entrance_id}")
  274. print(f" 世界坐标:({entrance_point['x']:.3f}, {entrance_point['y']:.3f}, {entrance_point['z']:.3f})")
  275. print(f" 可见点位:{entrance_point['visibles']}")
  276. print(f"\n其余点位 (绿色):{[p['id'] for p in poses if p['id'] != entrance_id]}")
  277. if __name__ == '__main__':
  278. vision_folder = "/home/gu/视频/door_detect/high_temp/ten_pano_demo/depth"
  279. out_folder = "/home/gu/视频/door_detect/high_temp/ten_pano_demo/2d_image"
  280. vision_scenes = os.listdir(vision_folder)
  281. for scene in vision_scenes:
  282. scene_name = os.path.join(vision_folder, scene, 'vision.txt')
  283. out_name = os.path.join(out_folder, scene + ".png")
  284. main(scene_name, out_name)