FangSen9000 commited on
Commit
1173bb2
·
verified ·
1 Parent(s): d29801b

Update nano_WaveGen/utils/visualize_training.py

Browse files
nano_WaveGen/utils/visualize_training.py CHANGED
@@ -19,6 +19,7 @@ import cv2
19
  import time
20
  import webbrowser
21
  from scipy.spatial.transform import Rotation
 
22
 
23
  # 导入深度转点云模块
24
  try:
@@ -79,6 +80,7 @@ class TrainingVisualizer:
79
  self.camera_rgb_handle = None
80
  self.coordinate_frame_handle = None
81
  self.mesh_handles_pool = {}
 
82
 
83
  # 当前数据
84
  self.predictions_npz = None
@@ -95,6 +97,12 @@ class TrainingVisualizer:
95
  # 播放状态
96
  self.is_playing = False
97
 
 
 
 
 
 
 
98
  # 设置场景
99
  self.setup_scene()
100
 
@@ -112,15 +120,25 @@ class TrainingVisualizer:
112
 
113
  def setup_scene(self):
114
  """设置场景背景和坐标系"""
115
- # 设置深蓝色背景
116
- bg_color = [13, 13, 38]
117
- width, height = 1920, 1080
118
- solid_color_image = np.full((height, width, 3), bg_color, dtype=np.uint8)
119
- self.server.scene.set_background_image(solid_color_image, format="png")
120
 
121
  # 设置坐标系方向
122
  self.server.scene.set_up_direction("+y")
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def scan_training_outputs(self):
125
  """扫描core_space目录下的训练输出"""
126
  self.training_outputs = []
@@ -233,6 +251,11 @@ class TrainingVisualizer:
233
  )
234
  self.gui_controls['gt_color'].on_update(self._on_color_change)
235
 
 
 
 
 
 
236
  # 点云控制
237
  with self.server.gui.add_folder("点云显示"):
238
  self.gui_controls['show_pointcloud'] = self.server.gui.add_checkbox(
@@ -257,6 +280,11 @@ class TrainingVisualizer:
257
  )
258
  self.gui_controls['show_coordinate'].on_update(self._on_visibility_change)
259
 
 
 
 
 
 
260
  # 相机控制
261
  with self.server.gui.add_folder("相机控制"):
262
  self.gui_controls['reset_view'] = self.server.gui.add_button("重置视角")
@@ -278,6 +306,29 @@ class TrainingVisualizer:
278
  self.gui_controls['show_pred_frustum'].on_update(self._on_visibility_change)
279
  self.gui_controls['show_camera_rgb'].on_update(self._on_visibility_change)
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  print(f"✅ GUI 已设置 - 创建了 {len(self.gui_controls)} 个控件")
282
 
283
  def _on_output_change(self, event):
@@ -426,6 +477,21 @@ class TrainingVisualizer:
426
  self.mesh_handles_pool.clear()
427
  self.visualize_frame(self.current_frame)
428
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  def _on_reset_view(self, event):
430
  """重置视角"""
431
  # 设置默认相机位置
@@ -534,7 +600,11 @@ class TrainingVisualizer:
534
  except Exception:
535
  pass
536
 
537
- if show_pointcloud:
 
 
 
 
538
  # 点云用同一center/scale做归一化
539
  self._visualize_pointcloud(frame_idx, scene_center=self.scene_center, scene_scale=self.scene_scale)
540
 
@@ -558,6 +628,11 @@ class TrainingVisualizer:
558
  is_gt=True
559
  )
560
 
 
 
 
 
 
561
  # 显示坐标系
562
  if show_coordinate:
563
  self.coordinate_frame_handle = self.server.scene.add_frame(
@@ -568,8 +643,9 @@ class TrainingVisualizer:
568
  axes_radius=0.01
569
  )
570
 
571
- # 可视化相机椎体/RGB
572
- self._visualize_cameras(frame_idx)
 
573
 
574
  def _extract_predictions(self, frame_idx: int) -> Optional[np.ndarray]:
575
  """提取预测数据 (新格式)"""
@@ -674,6 +750,62 @@ class TrainingVisualizer:
674
  label = "GT" if is_gt else "生成"
675
  print(f" {label}对象数: {num_active}")
676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  def _visualize_pointcloud(self, frame_idx: int, scene_center: Optional[np.ndarray] = None, scene_scale: Optional[float] = None):
678
  """可视化点云"""
679
  if self.current_sample_path is None:
@@ -965,22 +1097,34 @@ class TrainingVisualizer:
965
 
966
  def get_or_create_mesh(self, key: str, vertices, faces, color, opacity):
967
  """获取或创建mesh(对象池)"""
 
 
 
 
 
 
 
 
 
 
 
 
968
  if key in self.mesh_handles_pool:
969
  mesh = self.mesh_handles_pool[key]
970
  mesh.vertices = vertices
971
  mesh.vertex_colors = None
972
- mesh.wireframe = False
973
- mesh.opacity = opacity
974
  mesh.visible = True
975
 
976
  # 更新颜色
977
- color_array = np.array(color, dtype=np.uint8)
978
  if color_array.max() <= 1.0:
979
  color_array = (color_array * 255).astype(np.uint8)
980
  mesh.color = tuple(color_array)
981
  else:
982
  # 创建新mesh
983
- color_array = np.array(color, dtype=np.uint8)
984
  if color_array.max() <= 1.0:
985
  color_array = (color_array * 255).astype(np.uint8)
986
 
@@ -989,8 +1133,8 @@ class TrainingVisualizer:
989
  vertices=vertices,
990
  faces=faces,
991
  color=tuple(color_array),
992
- opacity=opacity,
993
- wireframe=False,
994
  flat_shading=False
995
  )
996
  self.mesh_handles_pool[key] = mesh
@@ -1025,6 +1169,692 @@ class TrainingVisualizer:
1025
  self.coordinate_frame_handle.remove()
1026
  self.coordinate_frame_handle = None
1027
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1028
  def run(self, auto_open_browser: bool = True):
1029
  """运行可视化器"""
1030
  print("\n" + "="*60)
 
19
  import time
20
  import webbrowser
21
  from scipy.spatial.transform import Rotation
22
+ import threading
23
 
24
  # 导入深度转点云模块
25
  try:
 
80
  self.camera_rgb_handle = None
81
  self.coordinate_frame_handle = None
82
  self.mesh_handles_pool = {}
83
+ self.object_label_handles = [] # 物体信息标签
84
 
85
  # 当前数据
86
  self.predictions_npz = None
 
97
  # 播放状态
98
  self.is_playing = False
99
 
100
+ # 视频导出状态
101
+ self.is_exporting = False
102
+ self.export_progress = 0
103
+ self.export_camera_pos = None
104
+ self.export_camera_wxyz = None
105
+
106
  # 设置场景
107
  self.setup_scene()
108
 
 
120
 
121
  def setup_scene(self):
122
  """设置场景背景和坐标系"""
123
+ # 设置深蓝色背景(默认)
124
+ self.update_background(wireframe_mode=False)
 
 
 
125
 
126
  # 设置坐标系方向
127
  self.server.scene.set_up_direction("+y")
128
 
129
+ def update_background(self, wireframe_mode: bool):
130
+ """更新场景背景颜色"""
131
+ if wireframe_mode:
132
+ # 线框模式:全黑背景
133
+ bg_color = [0, 0, 0]
134
+ else:
135
+ # 正常模式:深蓝色背景
136
+ bg_color = [13, 13, 38]
137
+
138
+ width, height = 1920, 1080
139
+ solid_color_image = np.full((height, width, 3), bg_color, dtype=np.uint8)
140
+ self.server.scene.set_background_image(solid_color_image, format="png")
141
+
142
  def scan_training_outputs(self):
143
  """扫描core_space目录下的训练输出"""
144
  self.training_outputs = []
 
251
  )
252
  self.gui_controls['gt_color'].on_update(self._on_color_change)
253
 
254
+ self.gui_controls['show_object_info'] = self.server.gui.add_checkbox(
255
+ "显示物体信息", initial_value=False
256
+ )
257
+ self.gui_controls['show_object_info'].on_update(self._on_visibility_change)
258
+
259
  # 点云控制
260
  with self.server.gui.add_folder("点云显示"):
261
  self.gui_controls['show_pointcloud'] = self.server.gui.add_checkbox(
 
280
  )
281
  self.gui_controls['show_coordinate'].on_update(self._on_visibility_change)
282
 
283
+ self.gui_controls['wireframe_mode'] = self.server.gui.add_checkbox(
284
+ "线框模式 (黑白边缘)", initial_value=False
285
+ )
286
+ self.gui_controls['wireframe_mode'].on_update(self._on_wireframe_mode_change)
287
+
288
  # 相机控制
289
  with self.server.gui.add_folder("相机控制"):
290
  self.gui_controls['reset_view'] = self.server.gui.add_button("重置视角")
 
306
  self.gui_controls['show_pred_frustum'].on_update(self._on_visibility_change)
307
  self.gui_controls['show_camera_rgb'].on_update(self._on_visibility_change)
308
 
309
+ # 视频导出
310
+ with self.server.gui.add_folder("视频导出"):
311
+ self.gui_controls['export_status'] = self.server.gui.add_text(
312
+ "状态", initial_value="就绪"
313
+ )
314
+
315
+ self.gui_controls['export_resolution'] = self.server.gui.add_slider(
316
+ "导出分辨率", min=480, max=1080, step=120, initial_value=720
317
+ )
318
+
319
+ self.gui_controls['capture_camera_button'] = self.server.gui.add_button(
320
+ "📸 捕获当前视角"
321
+ )
322
+ self.gui_controls['capture_camera_button'].on_click(self._on_capture_camera)
323
+
324
+ self.gui_controls['export_viser_button'] = self.server.gui.add_button(
325
+ "💾 导出场景(.viser)"
326
+ )
327
+ self.gui_controls['export_viser_button'].on_click(self._on_export_viser)
328
+
329
+ self.gui_controls['export_button'] = self.server.gui.add_button("🎬 导出视频(MP4)")
330
+ self.gui_controls['export_button'].on_click(self._on_export_video)
331
+
332
  print(f"✅ GUI 已设置 - 创建了 {len(self.gui_controls)} 个控件")
333
 
334
  def _on_output_change(self, event):
 
477
  self.mesh_handles_pool.clear()
478
  self.visualize_frame(self.current_frame)
479
 
480
+ def _on_wireframe_mode_change(self, event):
481
+ """线框模式改变"""
482
+ wireframe_mode = event.target.value
483
+
484
+ # 更新背景颜色
485
+ self.update_background(wireframe_mode)
486
+
487
+ # 清空对象池,强制重新生成mesh(应用线框模式)
488
+ for mesh in self.mesh_handles_pool.values():
489
+ mesh.remove()
490
+ self.mesh_handles_pool.clear()
491
+
492
+ # 重新可视���当前帧
493
+ self.visualize_frame(self.current_frame)
494
+
495
  def _on_reset_view(self, event):
496
  """重置视角"""
497
  # 设置默认相机位置
 
600
  except Exception:
601
  pass
602
 
603
+ # 线框模式下不显示点云(黑背景下点云不清晰)
604
+ wireframe_mode = self.gui_controls.get('wireframe_mode', None)
605
+ is_wireframe = wireframe_mode.value if wireframe_mode else False
606
+
607
+ if show_pointcloud and not is_wireframe:
608
  # 点云用同一center/scale做归一化
609
  self._visualize_pointcloud(frame_idx, scene_center=self.scene_center, scene_scale=self.scene_scale)
610
 
 
628
  is_gt=True
629
  )
630
 
631
+ # 显示物体信息(如果启用且不在线框模式)
632
+ show_info = self.gui_controls['show_object_info'].value
633
+ if show_info and not is_wireframe:
634
+ self._visualize_object_labels(frame_idx, targets, is_gt=True)
635
+
636
  # 显示坐标系
637
  if show_coordinate:
638
  self.coordinate_frame_handle = self.server.scene.add_frame(
 
643
  axes_radius=0.01
644
  )
645
 
646
+ # 可视化相机椎体/RGB(线框模式下不显示)
647
+ if not is_wireframe:
648
+ self._visualize_cameras(frame_idx)
649
 
650
  def _extract_predictions(self, frame_idx: int) -> Optional[np.ndarray]:
651
  """提取预测数据 (新格式)"""
 
750
  label = "GT" if is_gt else "生成"
751
  print(f" {label}对象数: {num_active}")
752
 
753
+ def _visualize_object_labels(self, frame_idx: int, objects: np.ndarray, is_gt: bool):
754
+ """在物体上显示信息标签"""
755
+ # 获取原始字典数据以访问inlier_ratio等
756
+ if is_gt and self.targets_npz is not None and 'frames' in self.targets_npz:
757
+ frames = self.targets_npz['frames']
758
+ if frame_idx >= len(frames):
759
+ return
760
+
761
+ frame_data = frames[frame_idx]
762
+ if isinstance(frame_data, np.ndarray):
763
+ frame_data = frame_data.item()
764
+
765
+ if 'superquadrics' not in frame_data:
766
+ return
767
+
768
+ superquadrics = frame_data['superquadrics']
769
+
770
+ for obj_idx, sq in enumerate(superquadrics):
771
+ if not sq['exists']:
772
+ continue
773
+
774
+ # 获取物体位置(用于放置标签)
775
+ translation = sq['translation']
776
+ scale = sq['scale']
777
+
778
+ # 标签位置:物体中心上方
779
+ label_position = (
780
+ float(translation[0]),
781
+ float(translation[1]) + float(scale[1]) * 1.5, # 在物体上方
782
+ float(translation[2])
783
+ )
784
+
785
+ # 构建信息文本
786
+ inlier_ratio = sq.get('inlier_ratio', 0.0)
787
+ shape = sq.get('shape', [0, 0])
788
+
789
+ info_text = (
790
+ f"ID: {obj_idx}\n"
791
+ f"Density: {inlier_ratio:.3f}\n"
792
+ f"Shape: ε1={shape[0]:.2f}, ε2={shape[1]:.2f}\n"
793
+ f"Size: {scale[0]:.2f}×{scale[1]:.2f}×{scale[2]:.2f}"
794
+ )
795
+
796
+ # 添加文本标签
797
+ # 使用时间戳确保名称唯一,避免冲突
798
+ label_name = f"/object_label_f{frame_idx}_o{obj_idx}"
799
+ try:
800
+ label_handle = self.server.scene.add_label(
801
+ label_name,
802
+ text=info_text,
803
+ position=label_position
804
+ )
805
+ self.object_label_handles.append(label_handle)
806
+ except Exception as e:
807
+ print(f"⚠️ 创建标签失败: {e}")
808
+
809
  def _visualize_pointcloud(self, frame_idx: int, scene_center: Optional[np.ndarray] = None, scene_scale: Optional[float] = None):
810
  """可视化点云"""
811
  if self.current_sample_path is None:
 
1097
 
1098
  def get_or_create_mesh(self, key: str, vertices, faces, color, opacity):
1099
  """获取或创建mesh(对象池)"""
1100
+ # 检查是否启用线框模式
1101
+ wireframe_mode = self.gui_controls.get('wireframe_mode', None)
1102
+ is_wireframe = wireframe_mode.value if wireframe_mode else False
1103
+
1104
+ # 线框模式:强制白色,完全不透明
1105
+ if is_wireframe:
1106
+ display_color = (255, 255, 255)
1107
+ display_opacity = 1.0
1108
+ else:
1109
+ display_color = color
1110
+ display_opacity = opacity
1111
+
1112
  if key in self.mesh_handles_pool:
1113
  mesh = self.mesh_handles_pool[key]
1114
  mesh.vertices = vertices
1115
  mesh.vertex_colors = None
1116
+ mesh.wireframe = is_wireframe
1117
+ mesh.opacity = display_opacity
1118
  mesh.visible = True
1119
 
1120
  # 更新颜色
1121
+ color_array = np.array(display_color, dtype=np.uint8)
1122
  if color_array.max() <= 1.0:
1123
  color_array = (color_array * 255).astype(np.uint8)
1124
  mesh.color = tuple(color_array)
1125
  else:
1126
  # 创建新mesh
1127
+ color_array = np.array(display_color, dtype=np.uint8)
1128
  if color_array.max() <= 1.0:
1129
  color_array = (color_array * 255).astype(np.uint8)
1130
 
 
1133
  vertices=vertices,
1134
  faces=faces,
1135
  color=tuple(color_array),
1136
+ opacity=display_opacity,
1137
+ wireframe=is_wireframe,
1138
  flat_shading=False
1139
  )
1140
  self.mesh_handles_pool[key] = mesh
 
1169
  self.coordinate_frame_handle.remove()
1170
  self.coordinate_frame_handle = None
1171
 
1172
+ # 删除物体信息标签
1173
+ for handle in self.object_label_handles:
1174
+ try:
1175
+ handle.remove()
1176
+ except (KeyError, AttributeError):
1177
+ # 标签可能已经被删除,忽略错误
1178
+ pass
1179
+ self.object_label_handles = []
1180
+
1181
+ def _on_capture_camera(self, event):
1182
+ """捕获当前相机视角"""
1183
+ clients = list(self.server.get_clients().values())
1184
+ if not clients:
1185
+ print("⚠️ 没有连接的客户端")
1186
+ self.gui_controls['export_status'].value = "错误: 没有连接的客户端"
1187
+ return
1188
+
1189
+ # 获取第一个客户端的相机参数
1190
+ client = clients[0]
1191
+ self.export_camera_pos = np.array(client.camera.position)
1192
+ self.export_camera_wxyz = np.array(client.camera.wxyz)
1193
+
1194
+ print(f"📸 已捕获相机视角: pos={self.export_camera_pos}, wxyz={self.export_camera_wxyz}")
1195
+ self.gui_controls['export_status'].value = f"已捕获视角: {self.export_camera_pos}"
1196
+
1197
+ def _on_export_viser(self, event):
1198
+ """导出为viser场景文件(可交互)"""
1199
+ if self.current_sample_path is None:
1200
+ print("⚠️ 请先加载样本")
1201
+ self.gui_controls['export_status'].value = "错误: 请先加载样本"
1202
+ return
1203
+
1204
+ if self.original_frame_count <= 0:
1205
+ print("⚠️ 没有帧可以导出")
1206
+ self.gui_controls['export_status'].value = "错误: 没有帧可以导出"
1207
+ return
1208
+
1209
+ # 在后台线程导出
1210
+ threading.Thread(target=self._export_viser_thread, daemon=True).start()
1211
+
1212
+ def _export_viser_thread(self):
1213
+ """导出viser场景文件(带动画)"""
1214
+ try:
1215
+ print(f"\n{'='*60}")
1216
+ print(f"💾 开始导出Viser场景")
1217
+ print(f"{'='*60}")
1218
+
1219
+ # 获取当前客户端的相机参数
1220
+ clients = list(self.server.get_clients().values())
1221
+ camera_params = None
1222
+ if clients:
1223
+ client = clients[0]
1224
+ cam_pos = client.camera.position
1225
+ cam_lookat = client.camera.look_at
1226
+ cam_up = client.camera.up_direction
1227
+
1228
+ # 生成viser URL参数格式
1229
+ camera_params = (
1230
+ f"&initialCameraPosition={cam_pos[0]:.3f},{cam_pos[1]:.3f},{cam_pos[2]:.3f}"
1231
+ f"&initialCameraLookAt={cam_lookat[0]:.3f},{cam_lookat[1]:.3f},{cam_lookat[2]:.3f}"
1232
+ f"&initialCameraUp={cam_up[0]:.3f},{cam_up[1]:.3f},{cam_up[2]:.3f}"
1233
+ )
1234
+ print(f" 📸 记录相机视角:")
1235
+ print(f" 位置: {cam_pos}")
1236
+ print(f" 朝向: {cam_lookat}")
1237
+ print(f" 向上: {cam_up}")
1238
+
1239
+ # 获取FPS
1240
+ fps = int(self.gui_controls['fps_slider'].value)
1241
+
1242
+ # 创建输出目录
1243
+ output_dir = self.core_space_dir / "exports"
1244
+ output_dir.mkdir(exist_ok=True)
1245
+
1246
+ # 生成文件名
1247
+ selected_output = self.gui_controls['output_selector'].value
1248
+ sample_idx = int(self.gui_controls['sample_slider'].value)
1249
+ step_info = "unknown"
1250
+ if "step" in selected_output:
1251
+ try:
1252
+ step_part = selected_output.split("_step")[1].split("_")[0]
1253
+ step_info = f"step{step_part}"
1254
+ except:
1255
+ pass
1256
+
1257
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
1258
+ experiment_name = selected_output.split("_")[0]
1259
+ output_file = output_dir / f"{experiment_name}_{step_info}_sample{sample_idx}_{timestamp}.viser"
1260
+
1261
+ print(f" 输出文件: {output_file}")
1262
+ print(f" 帧数: {self.original_frame_count}")
1263
+ print(f" FPS: {fps}")
1264
+
1265
+ # 获取场景序列化器
1266
+ serializer = self.server.get_scene_serializer()
1267
+
1268
+ # 记录初始状态(第一帧)
1269
+ self.visualize_frame(0)
1270
+ serializer.insert_sleep(1.0 / fps)
1271
+
1272
+ # 逐帧更新并记录
1273
+ for frame_idx in range(1, self.original_frame_count):
1274
+ self.export_progress = int((frame_idx + 1) / self.original_frame_count * 100)
1275
+ self.gui_controls['export_status'].value = f"导出中... {self.export_progress}%"
1276
+
1277
+ # 更新场景(会自动更新viser场景)
1278
+ self.visualize_frame(frame_idx)
1279
+
1280
+ # 添加帧延迟
1281
+ serializer.insert_sleep(1.0 / fps)
1282
+
1283
+ print(f" 记录帧 {frame_idx+1}/{self.original_frame_count}")
1284
+
1285
+ # 序列化并保存
1286
+ data = serializer.serialize()
1287
+ output_file.write_bytes(data)
1288
+
1289
+ print(f"✅ 场景导出完成: {output_file}")
1290
+ print(f" 文件大小: {len(data) / 1024 / 1024:.2f} MB")
1291
+ print(f"\n📖 查看方式:")
1292
+ print(f" 1. 安装viser客户端: viser-build-client --output-dir viser-client/")
1293
+ print(f" 2. 启动HTTP服务器: python -m http.server 8000")
1294
+
1295
+ # 生成完整URL(带相机参数)
1296
+ base_url = f"http://localhost:8000/viser-client/?playbackPath=http://localhost:8000/exports/{output_file.name}"
1297
+ if camera_params:
1298
+ full_url = base_url + camera_params
1299
+ print(f" 3. 打开浏览器(带相机视角):")
1300
+ print(f" {full_url}")
1301
+ else:
1302
+ print(f" 3. 打开浏览器:")
1303
+ print(f" {base_url}")
1304
+
1305
+ relative_path = output_file.relative_to(self.core_space_dir)
1306
+ self.gui_controls['export_status'].value = f"完成! {relative_path}"
1307
+
1308
+ # 提供下载
1309
+ clients = list(self.server.get_clients().values())
1310
+ if clients:
1311
+ clients[0].send_file_download(output_file.name, data)
1312
+ print(f" 💾 已发送下载到浏览器")
1313
+
1314
+ except Exception as e:
1315
+ print(f"❌ 导出失败: {e}")
1316
+ import traceback
1317
+ traceback.print_exc()
1318
+ self.gui_controls['export_status'].value = f"错误: {str(e)}"
1319
+
1320
+ def _on_export_video(self, event):
1321
+ """导出视频"""
1322
+ if self.is_exporting:
1323
+ print("⚠️ 正在导出中,请等待...")
1324
+ return
1325
+
1326
+ if self.current_sample_path is None:
1327
+ print("⚠️ 请先加载样本")
1328
+ self.gui_controls['export_status'].value = "错误: 请先加载样本"
1329
+ return
1330
+
1331
+ if self.original_frame_count <= 0:
1332
+ print("⚠️ 没有帧可以导出")
1333
+ self.gui_controls['export_status'].value = "错误: 没有帧可以导出"
1334
+ return
1335
+
1336
+ # 检查是否有连接的客户端
1337
+ clients = list(self.server.get_clients().values())
1338
+ if not clients:
1339
+ print("⚠️ 没有连接的客户端")
1340
+ self.gui_controls['export_status'].value = "错误: 请先在浏览器中打开viser界面"
1341
+ return
1342
+
1343
+ # 每次导出都获取最新的相机视角(重要!)
1344
+ # 无论之前是否捕获过,都使用当前最新的视角
1345
+ client = clients[0]
1346
+ self.export_camera_pos = np.array(client.camera.position)
1347
+ self.export_camera_wxyz = np.array(client.camera.wxyz)
1348
+ print(f"📸 使用当前视角: pos={self.export_camera_pos}, wxyz={self.export_camera_wxyz}")
1349
+
1350
+ # 在后台线程导出视频
1351
+ threading.Thread(target=self._export_video_thread_screenshot, daemon=True).start()
1352
+
1353
+ def _export_video_thread(self):
1354
+ """视频导出线程"""
1355
+ try:
1356
+ self.is_exporting = True
1357
+ self.gui_controls['export_status'].value = "正在导出..."
1358
+
1359
+ # 确保场景归一化参数已设置(通过可视化当前帧来初始化)
1360
+ if not hasattr(self, 'scene_center') or self.scene_center is None:
1361
+ print(" 初始化场景参数...")
1362
+ self.visualize_frame(self.current_frame)
1363
+
1364
+ # 获取参数
1365
+ fps = int(self.gui_controls['fps_slider'].value)
1366
+ resolution = int(self.gui_controls['export_resolution'].value)
1367
+
1368
+ # 创建输出目录 - 放在core_space根目录下
1369
+ output_dir = self.core_space_dir / "exports"
1370
+ output_dir.mkdir(exist_ok=True)
1371
+
1372
+ # 提取实验信息
1373
+ selected_output = self.gui_controls['output_selector'].value
1374
+ sample_idx = int(self.gui_controls['sample_slider'].value)
1375
+
1376
+ # 从输出名称提取步数 (例如: 20251205_184253_step5_text2wave -> step5)
1377
+ step_info = "unknown"
1378
+ if "step" in selected_output:
1379
+ try:
1380
+ step_part = selected_output.split("_step")[1].split("_")[0]
1381
+ step_info = f"step{step_part}"
1382
+ except:
1383
+ pass
1384
+
1385
+ # 生成输出文件名: {实验名}_{step}_sample{idx}_{timestamp}.mp4
1386
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
1387
+ experiment_name = selected_output.split("_")[0] # 取日期部分作为实验名
1388
+ output_file = output_dir / f"{experiment_name}_{step_info}_sample{sample_idx}_{timestamp}.mp4"
1389
+
1390
+ print(f"\n{'='*60}")
1391
+ print(f"🎬 开始导出视频")
1392
+ print(f"{'='*60}")
1393
+ print(f" 实验: {selected_output}")
1394
+ print(f" 样本: {sample_idx}")
1395
+ print(f" 输出文件: {output_file}")
1396
+ print(f" 帧数: {self.original_frame_count}")
1397
+ print(f" FPS: {fps}")
1398
+ print(f" 分辨率: {resolution}x{resolution}")
1399
+ print(f" 相机位置: {self.export_camera_pos}")
1400
+ print(f" 相机旋转: {self.export_camera_wxyz}")
1401
+
1402
+ # 尝试使用imageio(更好的兼容性),如果不可用则使用OpenCV
1403
+ try:
1404
+ import imageio
1405
+ use_imageio = True
1406
+ print(" 使用 imageio 进行视频编码(H.264)")
1407
+ except ImportError:
1408
+ use_imageio = False
1409
+ print(" 使用 OpenCV 进行视频编码")
1410
+
1411
+ if use_imageio:
1412
+ # 使用imageio-ffmpeg,生成高兼容性的H.264视频
1413
+ # 注意:必须指定format='FFMPEG'来确保使用FFmpeg插件
1414
+ writer = imageio.get_writer(
1415
+ str(output_file),
1416
+ format='FFMPEG',
1417
+ mode='I',
1418
+ fps=fps,
1419
+ codec='libx264',
1420
+ pixelformat='yuv420p', # 确保兼容性
1421
+ output_params=['-crf', '18'] # H.264质量参数,18是高质量
1422
+ )
1423
+
1424
+ # 渲染每一帧
1425
+ for frame_idx in range(self.original_frame_count):
1426
+ self.export_progress = int((frame_idx + 1) / self.original_frame_count * 100)
1427
+ self.gui_controls['export_status'].value = f"导出中... {self.export_progress}%"
1428
+
1429
+ # 渲染帧
1430
+ frame_image = self._render_frame_offline(
1431
+ frame_idx,
1432
+ resolution=resolution,
1433
+ camera_pos=self.export_camera_pos,
1434
+ camera_wxyz=self.export_camera_wxyz
1435
+ )
1436
+
1437
+ # 写入视频(imageio需要RGB格式)
1438
+ if frame_image is not None:
1439
+ writer.append_data(frame_image)
1440
+
1441
+ print(f" 渲染帧 {frame_idx+1}/{self.original_frame_count}")
1442
+
1443
+ writer.close()
1444
+
1445
+ else:
1446
+ # 使用OpenCV,尝试更兼容的编码器
1447
+ # 尝试顺序: H264 -> avc1 -> X264 -> mp4v
1448
+ codecs_to_try = [
1449
+ ('H264', 'H.264'),
1450
+ ('avc1', 'H.264 (AVC1)'),
1451
+ ('X264', 'X264'),
1452
+ ('mp4v', 'MPEG-4')
1453
+ ]
1454
+
1455
+ writer = None
1456
+ used_codec = None
1457
+
1458
+ for codec_fourcc, codec_name in codecs_to_try:
1459
+ try:
1460
+ fourcc = cv2.VideoWriter_fourcc(*codec_fourcc)
1461
+ test_writer = cv2.VideoWriter(
1462
+ str(output_file),
1463
+ fourcc,
1464
+ fps,
1465
+ (resolution, resolution)
1466
+ )
1467
+ if test_writer.isOpened():
1468
+ writer = test_writer
1469
+ used_codec = codec_name
1470
+ print(f" 使用编码器: {codec_name}")
1471
+ break
1472
+ else:
1473
+ test_writer.release()
1474
+ except:
1475
+ continue
1476
+
1477
+ if writer is None:
1478
+ raise RuntimeError("无法初始化视频编码器")
1479
+
1480
+ # 渲染每一帧
1481
+ for frame_idx in range(self.original_frame_count):
1482
+ self.export_progress = int((frame_idx + 1) / self.original_frame_count * 100)
1483
+ self.gui_controls['export_status'].value = f"导出中... {self.export_progress}%"
1484
+
1485
+ # 渲染帧
1486
+ frame_image = self._render_frame_offline(
1487
+ frame_idx,
1488
+ resolution=resolution,
1489
+ camera_pos=self.export_camera_pos,
1490
+ camera_wxyz=self.export_camera_wxyz
1491
+ )
1492
+
1493
+ # 写入视频(OpenCV需要BGR格式)
1494
+ if frame_image is not None:
1495
+ writer.write(cv2.cvtColor(frame_image, cv2.COLOR_RGB2BGR))
1496
+
1497
+ print(f" 渲染帧 {frame_idx+1}/{self.original_frame_count}")
1498
+
1499
+ writer.release()
1500
+
1501
+ print(f"✅ 视频导出完成: {output_file}")
1502
+ relative_path = output_file.relative_to(self.core_space_dir)
1503
+ self.gui_controls['export_status'].value = f"完成! {relative_path}"
1504
+
1505
+ except Exception as e:
1506
+ print(f"❌ 导出视频失败: {e}")
1507
+ import traceback
1508
+ traceback.print_exc()
1509
+ self.gui_controls['export_status'].value = f"错误: {str(e)}"
1510
+
1511
+ finally:
1512
+ self.is_exporting = False
1513
+
1514
+ def _export_video_thread_screenshot(self):
1515
+ """视频导出线程(基于截图viser界面)"""
1516
+ try:
1517
+ self.is_exporting = True
1518
+ self.gui_controls['export_status'].value = "正在导出..."
1519
+
1520
+ # 获取参数
1521
+ fps = int(self.gui_controls['fps_slider'].value)
1522
+
1523
+ # 创建输出目录
1524
+ output_dir = self.core_space_dir / "exports"
1525
+ output_dir.mkdir(exist_ok=True)
1526
+
1527
+ # 提取实验信息并生成文件名
1528
+ selected_output = self.gui_controls['output_selector'].value
1529
+ sample_idx = int(self.gui_controls['sample_slider'].value)
1530
+ step_info = "unknown"
1531
+ if "step" in selected_output:
1532
+ try:
1533
+ step_part = selected_output.split("_step")[1].split("_")[0]
1534
+ step_info = f"step{step_part}"
1535
+ except:
1536
+ pass
1537
+
1538
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
1539
+ experiment_name = selected_output.split("_")[0]
1540
+ output_file = output_dir / f"{experiment_name}_{step_info}_sample{sample_idx}_{timestamp}.mp4"
1541
+
1542
+ print(f"\n{'='*60}")
1543
+ print(f"🎬 开始导出视频(截图模式)")
1544
+ print(f"{'='*60}")
1545
+ print(f" 实验: {selected_output}")
1546
+ print(f" 样本: {sample_idx}")
1547
+ print(f" 输出文件: {output_file}")
1548
+ print(f" 帧数: {self.original_frame_count}")
1549
+ print(f" FPS: {fps}")
1550
+ print(f" 方法: 直接截取Viser显示画面")
1551
+
1552
+ # 检查selenium
1553
+ try:
1554
+ from selenium import webdriver
1555
+ from selenium.webdriver.chrome.options import Options
1556
+ from selenium.webdriver.common.by import By
1557
+ import time as time_module
1558
+ use_selenium = True
1559
+ print(" ✅ 使用 Selenium 截图")
1560
+ except ImportError:
1561
+ print(" ⚠️ Selenium未安装,使用逐帧渲染方法")
1562
+ print(" 提示: pip install selenium")
1563
+ use_selenium = False
1564
+
1565
+ if use_selenium:
1566
+ # 使用Selenium截图方法
1567
+ frames = []
1568
+
1569
+ # 配置Chrome
1570
+ chrome_options = Options()
1571
+ chrome_options.add_argument('--headless') # 无头模式
1572
+ chrome_options.add_argument('--no-sandbox')
1573
+ chrome_options.add_argument('--disable-dev-shm-usage')
1574
+ chrome_options.add_argument('--window-size=1920,1080')
1575
+
1576
+ try:
1577
+ driver = webdriver.Chrome(options=chrome_options)
1578
+ url = f"http://localhost:{self.port}"
1579
+ driver.get(url)
1580
+ print(f" 📱 打开浏览器: {url}")
1581
+
1582
+ # 等待页面加载
1583
+ time_module.sleep(3)
1584
+
1585
+ # 逐帧截图
1586
+ for frame_idx in range(self.original_frame_count):
1587
+ self.export_progress = int((frame_idx + 1) / self.original_frame_count * 100)
1588
+ self.gui_controls['export_status'].value = f"截图中... {self.export_progress}%"
1589
+
1590
+ # 通过GUI更新帧
1591
+ self.gui_controls['frame_slider'].value = frame_idx
1592
+ time_module.sleep(0.3) # 等待渲染
1593
+
1594
+ # 截图
1595
+ screenshot = driver.get_screenshot_as_png()
1596
+ img = cv2.imdecode(np.frombuffer(screenshot, np.uint8), cv2.IMREAD_COLOR)
1597
+ frames.append(img)
1598
+
1599
+ print(f" 截图帧 {frame_idx+1}/{self.original_frame_count}")
1600
+
1601
+ driver.quit()
1602
+
1603
+ # 使用imageio写入视频
1604
+ try:
1605
+ import imageio
1606
+ writer = imageio.get_writer(
1607
+ str(output_file),
1608
+ format='FFMPEG',
1609
+ mode='I',
1610
+ fps=fps,
1611
+ codec='libx264',
1612
+ pixelformat='yuv420p',
1613
+ output_params=['-crf', '18']
1614
+ )
1615
+
1616
+ for frame in frames:
1617
+ # 转换BGR到RGB
1618
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
1619
+ writer.append_data(frame_rgb)
1620
+
1621
+ writer.close()
1622
+ print(f"✅ 视频导出完成: {output_file}")
1623
+ relative_path = output_file.relative_to(self.core_space_dir)
1624
+ self.gui_controls['export_status'].value = f"完成! {relative_path}"
1625
+
1626
+ except ImportError:
1627
+ # 使用OpenCV写入
1628
+ height, width = frames[0].shape[:2]
1629
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
1630
+ writer = cv2.VideoWriter(str(output_file), fourcc, fps, (width, height))
1631
+ for frame in frames:
1632
+ writer.write(frame)
1633
+ writer.release()
1634
+ print(f"✅ 视频导出完成: {output_file}")
1635
+ relative_path = output_file.relative_to(self.core_space_dir)
1636
+ self.gui_controls['export_status'].value = f"完成! {relative_path}"
1637
+
1638
+ except Exception as e:
1639
+ print(f"❌ Selenium截图失败: {e}")
1640
+ import traceback
1641
+ traceback.print_exc()
1642
+ # 回退到渲染方法
1643
+ use_selenium = False
1644
+
1645
+ if not use_selenium:
1646
+ # 回退到原来的渲染方法
1647
+ print(" 使用PyRender离线渲染...")
1648
+ self._export_video_thread()
1649
+ return
1650
+
1651
+ except Exception as e:
1652
+ print(f"❌ 导出视频失败: {e}")
1653
+ import traceback
1654
+ traceback.print_exc()
1655
+ self.gui_controls['export_status'].value = f"错误: {str(e)}"
1656
+
1657
+ finally:
1658
+ self.is_exporting = False
1659
+
1660
+ def _render_frame_offline(self, frame_idx: int, resolution: int,
1661
+ camera_pos: np.ndarray, camera_wxyz: np.ndarray) -> Optional[np.ndarray]:
1662
+ """离线渲染一帧"""
1663
+ # 尝试导入pyrender
1664
+ try:
1665
+ import pyrender
1666
+ import trimesh
1667
+ except ImportError:
1668
+ if frame_idx == 0:
1669
+ print("⚠️ pyrender未安装,使用简化渲染...")
1670
+ print(" 提示: 安装 pyrender 以获得完整3D渲染")
1671
+ print(" pip install pyrender trimesh")
1672
+ return self._render_frame_simple(frame_idx, resolution)
1673
+
1674
+ # 设置PyRender使用离屏渲染(EGL或OSMesa)
1675
+ # 优先尝试EGL,如果失败则尝试OSMesa
1676
+ for platform in ['egl', 'osmesa']:
1677
+ try:
1678
+ os.environ['PYOPENGL_PLATFORM'] = platform
1679
+
1680
+ # 创建场景 - 设置深蓝色背景(与viser一致)
1681
+ scene = pyrender.Scene(
1682
+ ambient_light=[0.3, 0.3, 0.3],
1683
+ bg_color=[13/255, 13/255, 38/255, 1.0] # 深蓝色背景
1684
+ )
1685
+
1686
+ # 获取GUI参数
1687
+ show_generated = self.gui_controls['show_generated'].value
1688
+ show_gt = self.gui_controls['show_gt'].value
1689
+ generated_color = np.array(self.gui_controls['generated_color'].value) / 255.0
1690
+ gt_color = np.array(self.gui_controls['gt_color'].value) / 255.0
1691
+ mesh_resolution = int(self.gui_controls['mesh_resolution'].value)
1692
+
1693
+ mesh_count = 0
1694
+
1695
+ # 添加生成的超二次曲面
1696
+ # 重要:需要应用场景归一化,使物体坐标与相机坐标在同一空间
1697
+ if show_generated:
1698
+ predictions = self._extract_predictions(frame_idx)
1699
+ if predictions is not None:
1700
+ for obj_idx, obj_params in enumerate(predictions):
1701
+ if obj_params[0] > 0.5:
1702
+ # 复制参数并应用场景归一化到平移部分
1703
+ obj_params_normalized = obj_params.copy()
1704
+ # 归一化平移: (translation - scene_center) * scene_scale
1705
+ translation = obj_params[6:9]
1706
+ translation_normalized = (translation - self.scene_center) * self.scene_scale
1707
+ obj_params_normalized[6:9] = translation_normalized
1708
+ # 归一化缩放: scale * scene_scale
1709
+ obj_params_normalized[3:6] = obj_params[3:6] * self.scene_scale
1710
+
1711
+ vertices, faces = self.generate_superquadric_mesh(
1712
+ obj_params_normalized, num_samples=mesh_resolution
1713
+ )
1714
+
1715
+ if frame_idx == 0 and obj_idx == 0:
1716
+ print(f" 物体原始位置: {translation}")
1717
+ print(f" 物体归一化位置: {translation_normalized}")
1718
+ print(f" 场景中心: {self.scene_center}, 缩放: {self.scene_scale}")
1719
+
1720
+ mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
1721
+ # 为每个顶点设置颜色 (N, 4) - RGBA
1722
+ num_verts = len(vertices)
1723
+ vertex_colors = np.zeros((num_verts, 4), dtype=np.uint8)
1724
+ vertex_colors[:, :3] = (generated_color * 255).astype(np.uint8) # RGB
1725
+ vertex_colors[:, 3] = 255 # 完全不透明
1726
+ mesh.visual.vertex_colors = vertex_colors
1727
+
1728
+ # 创建PyRender材质
1729
+ material = pyrender.MetallicRoughnessMaterial(
1730
+ baseColorFactor=list(generated_color) + [1.0],
1731
+ metallicFactor=0.3,
1732
+ roughnessFactor=0.7
1733
+ )
1734
+ mesh_obj = pyrender.Mesh.from_trimesh(mesh, material=material)
1735
+ scene.add(mesh_obj)
1736
+ mesh_count += 1
1737
+
1738
+ # 添加GT超二次曲面
1739
+ if show_gt:
1740
+ targets = self._extract_targets(frame_idx)
1741
+ if targets is not None:
1742
+ for obj_idx, obj_params in enumerate(targets):
1743
+ if obj_params[0] > 0.5:
1744
+ # 复制参数并应用场景归一化
1745
+ obj_params_normalized = obj_params.copy()
1746
+ translation = obj_params[6:9]
1747
+ translation_normalized = (translation - self.scene_center) * self.scene_scale
1748
+ obj_params_normalized[6:9] = translation_normalized
1749
+ obj_params_normalized[3:6] = obj_params[3:6] * self.scene_scale
1750
+
1751
+ vertices, faces = self.generate_superquadric_mesh(
1752
+ obj_params_normalized, num_samples=mesh_resolution
1753
+ )
1754
+ mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
1755
+ # 为每个顶点设置颜色 (N, 4) - RGBA
1756
+ num_verts = len(vertices)
1757
+ vertex_colors = np.zeros((num_verts, 4), dtype=np.uint8)
1758
+ vertex_colors[:, :3] = (gt_color * 255).astype(np.uint8) # RGB
1759
+ vertex_colors[:, 3] = 255 # 完全不透明
1760
+ mesh.visual.vertex_colors = vertex_colors
1761
+
1762
+ # 创建PyRender材质
1763
+ material = pyrender.MetallicRoughnessMaterial(
1764
+ baseColorFactor=list(gt_color) + [0.5],
1765
+ metallicFactor=0.3,
1766
+ roughnessFactor=0.7
1767
+ )
1768
+ mesh_obj = pyrender.Mesh.from_trimesh(mesh, material=material)
1769
+ scene.add(mesh_obj)
1770
+ mesh_count += 1
1771
+
1772
+ if frame_idx == 0:
1773
+ print(f" 场景中添加了 {mesh_count} 个mesh")
1774
+
1775
+ # 设置相机
1776
+ # Viser使用的是wxyz四元数,需要转换为PyRender的变换矩阵
1777
+ from scipy.spatial.transform import Rotation as R
1778
+
1779
+ # wxyz -> xyzw for scipy
1780
+ rot = R.from_quat([camera_wxyz[1], camera_wxyz[2], camera_wxyz[3], camera_wxyz[0]])
1781
+ rot_matrix = rot.as_matrix()
1782
+
1783
+ # PyRender使用OpenGL坐标系
1784
+ # 构建相机变换矩阵
1785
+ camera_pose = np.eye(4)
1786
+ camera_pose[:3, :3] = rot_matrix
1787
+ camera_pose[:3, 3] = camera_pos
1788
+
1789
+ if frame_idx == 0:
1790
+ print(f" 相机位置: {camera_pos}")
1791
+ print(f" 相机旋转矩阵:\n{rot_matrix}")
1792
+
1793
+ # 创建透视相机
1794
+ camera = pyrender.PerspectiveCamera(yfov=np.pi / 3.0, aspectRatio=1.0)
1795
+ scene.add(camera, pose=camera_pose)
1796
+
1797
+ # 添加多个光源以确保场景被充分照亮
1798
+ # 主光源跟随相机
1799
+ light1 = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=2.0)
1800
+ scene.add(light1, pose=camera_pose)
1801
+
1802
+ # 额外的环境光源
1803
+ light2 = pyrender.DirectionalLight(color=[1.0, 1.0, 1.0], intensity=1.0)
1804
+ light_pose = np.eye(4)
1805
+ light_pose[:3, 3] = [10, 10, 10]
1806
+ scene.add(light2, pose=light_pose)
1807
+
1808
+ # 渲染
1809
+ renderer = pyrender.OffscreenRenderer(resolution, resolution)
1810
+ color, depth = renderer.render(scene)
1811
+ renderer.delete()
1812
+
1813
+ # 首次成功时打印使用的平台和渲染统计
1814
+ if frame_idx == 0:
1815
+ print(f" ✅ 使用 {platform.upper()} 进行离线渲染")
1816
+ print(f" 渲染输出范围: [{color.min()}, {color.max()}]")
1817
+ print(f" 深度范围: [{depth.min()}, {depth.max()}]")
1818
+
1819
+ return color
1820
+
1821
+ except Exception as e:
1822
+ if platform == 'osmesa':
1823
+ # 两种方式都失败了
1824
+ if frame_idx == 0:
1825
+ print(f"❌ PyRender渲染失败 (EGL和OSMesa都不可用): {e}")
1826
+ print(" 使用简化渲染模式...")
1827
+ return self._render_frame_simple(frame_idx, resolution)
1828
+ # EGL失败,继续尝试OSMesa
1829
+ continue
1830
+
1831
+ # 不应该到达这里,但以防万一
1832
+ return self._render_frame_simple(frame_idx, resolution)
1833
+
1834
+ def _render_frame_simple(self, frame_idx: int, resolution: int) -> np.ndarray:
1835
+ """简化渲染(纯色背景 + 文字提示)"""
1836
+ # 创建空白图像
1837
+ image = np.full((resolution, resolution, 3), [13, 13, 38], dtype=np.uint8)
1838
+
1839
+ # 添加文字
1840
+ text = f"Frame {frame_idx + 1}/{self.original_frame_count}"
1841
+ font = cv2.FONT_HERSHEY_SIMPLEX
1842
+ text_size = cv2.getTextSize(text, font, 1, 2)[0]
1843
+ text_x = (resolution - text_size[0]) // 2
1844
+ text_y = (resolution + text_size[1]) // 2
1845
+
1846
+ cv2.putText(image, text, (text_x, text_y), font, 1, (255, 255, 255), 2)
1847
+
1848
+ # 添加提示信息
1849
+ hint = "Install pyrender for full rendering"
1850
+ hint_size = cv2.getTextSize(hint, font, 0.5, 1)[0]
1851
+ hint_x = (resolution - hint_size[0]) // 2
1852
+ hint_y = text_y + 40
1853
+
1854
+ cv2.putText(image, hint, (hint_x, hint_y), font, 0.5, (150, 150, 150), 1)
1855
+
1856
+ return image
1857
+
1858
  def run(self, auto_open_browser: bool = True):
1859
  """运行可视化器"""
1860
  print("\n" + "="*60)