import open3d as o3d import numpy as np import json import os from PIL import Image import argparse def custom_mesh_transform(vertices, transform_matrix): """ 手动实现网格变换:对每个顶点应用齐次变换矩阵 参数: vertices: 网格顶点数组 (N, 3) transform_matrix: 4x4 齐次变换矩阵 返回: 变换后的顶点数组 (N, 3) """ # 1. 顶点转齐次坐标 (N, 3) → (N, 4) homogeneous_vertices = np.hstack((vertices, np.ones((vertices.shape[0], 1)))) # 2. 应用变换矩阵:矩阵乘法 (4x4) * (4xN) → (4xN) transformed_homogeneous = transform_matrix @ homogeneous_vertices.T # 3. 转回非齐次坐标 (3xN) → (N, 3) transformed_vertices = transformed_homogeneous[:3, :].T return transformed_vertices def load_and_transform_models(base_path, dict_origin, blank_path, json_name): meshes = [] # 存储所有变换后的网格 json_path = os.path.join(base_path, json_name) """ 加载JSON文件,读取所有模型信息,应用变换后返回模型列表 """ # 检查JSON文件是否存在 if not os.path.exists(json_path): print(f"错误: JSON文件不存在 - {json_path}") return [] # 读取JSON文件 try: with open(json_path, 'r') as f: data = json.load(f) except Exception as e: print(f"读取JSON文件失败: {e}") return [] # 处理每个模型 for model in data.get('models', []): obj_name = model.get('file_name', '') obj_path = os.path.join(base_path, obj_name) # 检查文件路径是否存在 if not obj_path: print("警告: 跳过缺少文件路径的模型") continue # print("load path", obj_path) # 检查文件是否存在 if not os.path.exists(obj_path): print(f"警告: 模型文件不存在 - {obj_path}") continue # 加载网格 try: # print("dict_origin obj_path=", obj_path) key = obj_path if key in dict_origin: # print("key in dict_origin") mesh = dict_origin[key] else : # print("key not in dict_origin") mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) if not mesh.has_vertices(): print(f"警告: 网格无有效顶点 - {obj_path}") continue except Exception as e: print(f"加载模型失败: {obj_path} - {e}") continue transform = model.get('transform', {}) homo_matrix = transform["homo_matrix"] # 获取存储的列表 reconstructed_matrix = np.array(homo_matrix, dtype=np.float64) # 手动变换顶点 original_vertices = np.asarray(mesh.vertices) transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix) mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) # 添加到列表 meshes.append(mesh) print(f"已加载并变换: {os.path.basename(obj_path)}") add_plank = True if add_plank: # obj_path = os.path.join(blank_dir, "blank2.obj") obj_path = blank_path print("add_plank",obj_path) try: mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) if not mesh.has_vertices(): print(f"警告: 网格无有效顶点 - {obj_path}") except Exception as e: print(f"加载模型失败: {obj_path} - {e}") rotation = [0.0, 0.0, 0.0] mesh_center = compute_mesh_center(mesh.vertices) R_x = mesh.get_rotation_matrix_from_axis_angle(np.array([1, 0, 0]) * np.radians(rotation[0])) mesh.rotate(R_x,center=mesh_center) R_y = mesh.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * np.radians(rotation[1])) mesh.rotate(R_y,center=mesh_center) R_z = mesh.get_rotation_matrix_from_axis_angle(np.array([0, 0, 1]) * np.radians(rotation[2])) mesh.rotate(R_z,center=mesh_center) displacement = [0.0, 0.0, 0.0] displacement = np.asarray(displacement) mesh.translate(displacement) meshes.append(mesh) # print(f"已加载并变换: {obj_path}") return meshes def compute_mesh_center(vertices): if len(vertices) == 0: raise ValueError("顶点数组不能为空") n = len(vertices) # 顶点数量 # 初始化坐标累加器 sum_x, sum_y, sum_z = 0.0, 0.0, 0.0 # 遍历所有顶点累加坐标值 for vertex in vertices: sum_x += vertex[0] sum_y += vertex[1] sum_z += vertex[2] # 计算各坐标轴的平均值 centroid = np.array([sum_x / n, sum_y / n, sum_z / n]) return centroid def load_and_show(base_path, blank_dir, json_name="3DPrintLayout.json"): # 加载并变换所有模型 transformed_meshes = load_and_transform_models(base_path, {}, blank_dir, json_name) if not transformed_meshes: print("没有加载到任何模型,请检查错误信息") else: print(f"成功加载并变换了 {len(transformed_meshes)} 个模型") # 可视化所有模型 print("显示所有模型... (按'Q'退出)") try: from packaging import version o3d_version = version.parse(o3d.__version__) # 新版本 draw_geometries 参数 if o3d_version >= version.parse("0.13.0"): o3d.visualization.draw_geometries( transformed_meshes, window_name="模型展示", mesh_show_back_face=True, mesh_show_wireframe=False ) # 旧版本 draw_geometries 参数 else: o3d.visualization.draw_geometries( transformed_meshes, window_name="模型展示", point_show_normal=False, mesh_show_back_face=True ) except Exception as e: print(f"使用 draw_geometries 可视化失败: {e}") def setup_orthographic_camera(vis, meshes, ortho_width=15.0, camera_height=20.0): """ 设置精确的正交投影相机 """ view_control = vis.get_view_control() # 计算场景边界框以确定合适的正交参数 all_points = [] for mesh in meshes: if hasattr(mesh, 'vertices'): points = np.asarray(mesh.vertices) else: points = np.asarray(mesh.points) all_points.append(points) if all_points: all_points = np.vstack(all_points) bbox_min = np.min(all_points, axis=0) bbox_max = np.max(all_points, axis=0) scene_center = (bbox_min + bbox_max) / 2 scene_size = np.max(bbox_max - bbox_min) # 设置观察点为场景中心 view_control.set_lookat(scene_center) # 设置相机在场景上方,俯视场景 view_control.set_front([0, 0, -1]) # 看向负Z轴(从上向下) view_control.set_up([0, 1, 0]) # Y轴向上 try: # 启用正交投影 view_control.set_orthogonal(True) # 根据场景大小设置正交投影宽度 view_control.set_orthogonal_width(max(scene_size * 1.2, ortho_width)) print(f"正交投影已设置: 宽度={max(scene_size * 1.2, ortho_width):.2f}") except AttributeError: # 回退到透视投影模拟 view_control.set_zoom(0.1) print("使用透视投影模拟正交效果") return True def auto_fit_to_view(vis, meshes): """ 自动调整视图以显示所有模型 """ view_control = vis.get_view_control() # 方法1: 使用 Open3D 的自动适配功能 try: view_control.fit_to_geometry(meshes) print("已自动适配视图以显示所有模型") return True except: pass # 方法2: 手动计算并设置 all_points = [] for mesh in meshes: if hasattr(mesh, 'vertices'): points = np.asarray(mesh.vertices) elif hasattr(mesh, 'points'): points = np.asarray(mesh.points) else: continue all_points.append(points) if all_points: all_points = np.vstack(all_points) bbox_min = np.min(all_points, axis=0) bbox_max = np.max(all_points, axis=0) scene_center = (bbox_min + bbox_max) / 2 scene_size = np.max(bbox_max - bbox_min) # 设置合适的视角和缩放 view_control.set_lookat(scene_center) # 根据场景大小设置 zoom zoom_level = max(0.05, min(1.0, 10.0 / scene_size)) zoom_level = 0.5 view_control.set_zoom(zoom_level) print(f"手动适配视图: 场景大小 {scene_size:.2f}, zoom {zoom_level:.3f}") return True return False def set_orthographic_camera(view_control, desired_width=1920, desired_height=1080): """ 通过相机参数设置正交投影,并确保尺寸匹配 """ # 更可靠的方式:使用你创建窗口时已知的尺寸 # 假设你创建窗口时使用的是 desired_width 和 desired_height param = view_control.convert_to_pinhole_camera_parameters() # 修改内参,确保使用与创建窗口时一致的尺寸 param.intrinsic.set_intrinsics( width=desired_width, # 必须与create_window的width一致 height=desired_height, # 必须与create_window的height一致 fx=1000.0, # 对于正交投影,此值意义不同,但仍需合理设置 fy=1000.0, # 避免使用1.0这样的极端值 cx=desired_width / 2, cy=desired_height / 2 ) # 同时,仍需通过ViewControl启用正交投影 view_control.set_orthogonal(True) view_control.convert_from_pinhole_camera_parameters(param) def set_orthographic_projection(view_control, ortho_scale=10.0): """ 尝试使用 ViewControl 接口配置正交投影效果。 参数: vis: Open3D 可视化器实例 ortho_scale: 控制正交投影的“视野”大小,值越大,场景中的物体看起来越小。 """ try: # 1. 尝试设置一个非常小的视野(Field of View)来减少透视感 # 注意:此方法可能在某些版本中效果不明显,但它是可用的最接近正交投影的直接控制 view_control.change_field_of_view(step=-5) # 尝试将FOV调到极小[1](@ref) # 2. 设置固定的近、远裁剪平面,避免因自动计算带来的透视变形 # 这对于保持缩放时物体大小一致很重要 view_control.set_constant_z_near(0.1) # 设置一个固定的近平面 view_control.set_constant_z_far(1000.0) # 设置一个固定的远平面[1](@ref) # 3. 使用一个较小的缩放值(Zoom),这有助于让视角更“平行” # 在正交投影的模拟中,通常使用较大的zoom值(>1)来拉远相机,减少透视效果。 # 但根据实际测试,您可能需要尝试一个具体的值,例如 0.5 或 0.1 view_control.set_zoom(0.46) # 这个值需要根据你的场景进行调整[6,7](@ref) # print("已尝试通过 ViewControl 配置正交投影参数。") return True except Exception as e: print(f"在配置 ViewControl 参数时出错: {e}") return False def set_orthographic(meshes, output_path, width=1920, height=1080, background_color=[1, 1, 1], camera_position=None, ortho_width=None, zoom=1.0): vis = o3d.visualization.Visualizer() vis.create_window(width=width, height=height, visible=False) # 添加几何体 for mesh in meshes: vis.add_geometry(mesh) # 设置渲染选项 render_opt = vis.get_render_option() render_opt.background_color = np.asarray(background_color) render_opt.mesh_show_back_face = True render_opt.mesh_show_wireframe = False render_opt.point_size = 3.0 # 视角控制 view_control = vis.get_view_control() # 计算所有网格的合并边界框,用于自适应设置投影参数 all_points = [] for mesh in meshes: if hasattr(mesh, 'vertices'): points = np.asarray(mesh.vertices) elif hasattr(mesh, 'points'): points = np.asarray(mesh.points) else: continue if len(points) > 0: all_points.append(points) if len(all_points) > 0: all_points = np.vstack(all_points) bbox_min = np.min(all_points, axis=0) bbox_max = np.max(all_points, axis=0) bbox_center = (bbox_min + bbox_max) / 2.0 bbox_size = np.max(bbox_max - bbox_min) # 设置相机视角,看向场景中心 if not camera_position: # 默认顶视图 view_control.set_front([0, 0, 1]) view_control.set_lookat(bbox_center) view_control.set_up([0, 1, 0]) else: view_control.set_front(camera_position['front']) view_control.set_lookat(camera_position['lookat']) view_control.set_up(camera_position['up']) # 自适应设置正交投影宽度,考虑屏幕宽高比 if ortho_width is None: aspect_ratio = width / height ortho_width = bbox_size * 1.5 # 根据宽高比调整,确保场景适合窗口 if aspect_ratio > 1: ortho_width *= aspect_ratio if ortho_width <= 0: ortho_width = 10.0 else: # 如果没有顶点,使用默认值 if not camera_position: view_control.set_front([0, 0, 1]) view_control.set_lookat([0, 0, 0]) view_control.set_up([0, 1, 0]) ortho_width = ortho_width or 10.0 for _ in range(2): vis.poll_events() vis.update_renderer() # 设置正交投影 try: set_orthographic_camera(view_control) except AttributeError: set_orthographic_projection(view_control, ortho_scale=15.0) for _ in range(5): vis.poll_events() vis.update_renderer() return vis def render_to_texture(meshes, output_path, width=1920, height=1080, background_color=[1, 1, 1], camera_position=None, ortho_width=None, zoom=1.0): vis = set_orthographic(meshes, output_path) # 渲染并保存 vis.capture_screen_image(output_path, do_render=True) print(f"高级渲染图片已保存到: {output_path}") return vis def load_show_save(base_path, dict_origin, blank_path, batch_id, is_show=False): folder_name = batch_id # base_path = f"{print_factory_type_dir}/{folder_name}/" # 替换为实际路径 #json_name = "3DPrintLayout.json" json_name = f"{batch_id}.json" output_image_path = os.path.join(base_path, f"{folder_name}.jpg") # 加载并变换所有模型 transformed_meshes = load_and_transform_models(base_path, dict_origin, blank_path, json_name) if not transformed_meshes: print("没有加载到任何模型,请检查错误信息") else: print(f"成功加载并变换了 {len(transformed_meshes)} 个模型") render_to_texture(transformed_meshes, output_image_path, background_color=[0.9, 0.9, 0.9]) if is_show: # 可视化所有模型 print("显示所有模型... (按'Q'退出)") vis = o3d.visualization.Visualizer() vis.create_window(window_name="正交投影模型展示") # 添加所有网格到可视化器 for mesh in transformed_meshes: vis.add_geometry(mesh) vis.poll_events() vis.update_renderer() # 获取渲染选项并设置 render_option = vis.get_render_option() render_option.background_color = np.array([0.9, 0.9, 0.9]) # 浅灰色背景 render_option.mesh_show_back_face = True render_option.mesh_show_wireframe = False # 更新渲染器 vis.update_renderer() # 运行可视化 vis.run() vis.destroy_window() # 主程序 if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--batch_id", type=str, required=True, help="batch_id") args = parser.parse_args() # batch_id = args.batch_id batch_id = "9" # 1113-MY-4 print_factory_type_dir="/root/print_factory_type" base_path = f"{print_factory_type_dir}/{batch_id}/" # blank_path = "{print_factory_type_dir}/blank/blank_bias/blank2.obj" blank_path = f"{print_factory_type_dir}/blank/blank_bias/blank_small.obj" load_show_save(base_path, {}, blank_path, batch_id, True)