发布时间:2026/8/11 18:37:07
Blender模拟结构光3D Scanner(三)获取相机观测点云的真值 模拟结构光3D Scanner时常见的一个问题是如何获取重建点云的真值在Blender中可以使用光线求交的方法从相机光心沿各像素发出射线与场景物体求交并将交点导出为点云文件。本文介绍了一个Blender插件用于通过光线追踪技术将相机视角下的3D场景转换为点云数据。该插件从相机光心向每个像素发射射线与场景物体求交后将交点坐标导出为ASC或NPZ格式的点云文件。核心功能包括1支持设置分辨率、采样步长等参数2可选择场景中的任意相机3提供点云数据可视化和导出功能。代码实现了射线投射计算、数据存储和用户界面交互适用于3D扫描、计算机视觉等需要将3D场景转换为点云数据的应用场景。插件安装后可在Blender的3D视图侧边栏中调用。以下是Blender插件代码在Preference-Add-ons中从文件导入导入方法参见Blender模拟结构光3D Scanner二投影仪内参数匹配-CSDN博客word_coordinate_mapper.pybl_info { name: 世界坐标映射工具, author: Your Name, version: (1, 1), blender: (2, 80, 0), location: View3D Sidebar 工具, description: 获取指定相机渲染图像每个像素对应的世界坐标, category: 3D View, } import bpy import numpy as np from mathutils import Vector # 属性组用于存储场景数据 class WorldCoordData(bpy.types.PropertyGroup): resolution: bpy.props.IntVectorProperty( name分辨率, size2, default(640, 480) ) hit_count: bpy.props.IntProperty( name命中点数, default0 ) step_size: bpy.props.IntProperty( name采样步长, default1 ) selected_camera: bpy.props.StringProperty( name选择相机, default ) class WORLD_COORD_OT_calculate(bpy.types.Operator): 计算指定相机视图的世界坐标映射 bl_idname world_coord.calculate bl_label 计算世界坐标 bl_options {REGISTER, UNDO} resolution_x: bpy.props.IntProperty( nameX分辨率, description输出图像的X分辨率, default640, min64, max4096 ) resolution_y: bpy.props.IntProperty( nameY分辨率, description输出图像的Y分辨率, default480, min64, max4096 ) step_size: bpy.props.IntProperty( name采样步长, description像素采样步长1每个像素2每2个像素以此类推, default2, min1, max10 ) save_to_file: bpy.props.BoolProperty( name保存到文件, description将结果保存到NPZ文件, defaultTrue ) camera_name: bpy.props.StringProperty( name相机, description选择要使用的相机, default ) def execute(self, context): 执行操作的主要函数 try: # 检查是否选择了相机 if not self.camera_name: self.report({ERROR}, 请选择一个相机) return {CANCELLED} # 获取选择的相机对象 camera_obj bpy.data.objects.get(self.camera_name) if not camera_obj or camera_obj.type ! CAMERA: self.report({ERROR}, 选择的相机无效或不存在) return {CANCELLED} # 执行射线投射计算 world_coords, hit_mask self.raycast_world_coordinates( camera_obj, self.resolution_x, self.resolution_y, self.step_size ) # 保存结果到场景属性 self.save_results_to_scene(context, camera_obj, world_coords, hit_mask) # 可选保存到文件 if self.save_to_file: self.save_to_asc(camera_obj, world_coords, hit_mask) self.report({INFO}, f计算完成相机 {camera_obj.name} 找到 {np.sum(hit_mask)} 个命中点) return {FINISHED} except Exception as e: self.report({ERROR}, f计算失败: {str(e)}) return {CANCELLED} def raycast_world_coordinates(self, camera_obj, res_x, res_y, step_size1): 通过射线投射获取世界坐标 scene bpy.context.scene depsgraph bpy.context.evaluated_depsgraph_get() # 初始化结果数组 world_coords np.full((res_y, res_x, 3), np.nan, dtypenp.float32) hit_mask np.zeros((res_y, res_x), dtypebool) # 获取相机矩阵 cam_matrix camera_obj.matrix_world cam_data camera_obj.data # 计算相机参数 aspect_ratio res_x / res_y sensor_width cam_data.sensor_width sensor_height sensor_width / aspect_ratio focal_length cam_data.lens # 进度更新 wm bpy.context.window_manager wm.progress_begin(0, res_y) try: # 遍历每个像素带步长 for y in range(0, res_y, step_size): if y % 10 0: # 每10行更新一次进度 wm.progress_update(y) # 新代码 if getattr(wm, is_modal, False) or getattr(wm, progress_abort, False): break for x in range(0, res_x, step_size): # 计算射线方向 ray_direction self.get_camera_ray_direction( camera_obj, x, y, res_x, res_y ) # 射线原点相机位置 ray_origin cam_matrix Vector((0, 0, 0)) # 执行射线投射 hit, location, normal, index, obj, matrix scene.ray_cast( depsgraph, ray_origin, ray_direction ) if hit: world_coords[y, x] np.array(location) hit_mask[y, x] True finally: wm.progress_end() return world_coords, hit_mask def get_camera_ray_direction(self, camera_obj, pixel_x, pixel_y, res_x, res_y): 计算相机射线方向 # 转换为标准化设备坐标 (-1 到 1) ndc_x (pixel_x / res_x) * 2.0 - 1.0 ndc_y 1.0 - (pixel_y / res_y) * 2.0 # Y轴翻转 # 考虑相机传感器和焦距 aspect_ratio res_x / res_y sensor_width camera_obj.data.sensor_width sensor_height sensor_width / aspect_ratio focal_length camera_obj.data.lens # 计算相机空间中的方向 if camera_obj.data.type PERSP: # 透视相机 direction Vector(( ndc_x * (sensor_width / 2) / focal_length, ndc_y * (sensor_height / 2) / focal_length, -1.0 # 相机看向-Z方向 )) else: # 正交相机 scale camera_obj.data.ortho_scale direction Vector(( ndc_x * scale / 2, ndc_y * scale / 2 / aspect_ratio, -1.0 )) # 转换到世界空间 direction_world camera_obj.matrix_world.to_3x3() direction direction_world.normalize() return direction_world def save_results_to_scene(self, context, camera_obj, world_coords, hit_mask): 保存结果到场景属性 scene context.scene # 更新场景属性 scene.world_coord_data.resolution (world_coords.shape[1], world_coords.shape[0]) scene.world_coord_data.hit_count int(np.sum(hit_mask)) scene.world_coord_data.step_size self.step_size scene.world_coord_data.selected_camera camera_obj.name # 保存原始数据可选如果需要后续访问 if not hasattr(scene, world_coord_raw_data): scene[world_coord_raw_data] {} scene[world_coord_raw_data] { camera_name: camera_obj.name, timestamp: bpy.context.scene.frame_current, resolution: (world_coords.shape[1], world_coords.shape[0]) } def save_to_asc(self, camera_obj, world_coords, hit_mask): 将命中点的世界坐标保存为 .asc 文件ASCII 点云 import os from datetime import datetime # 1) 输出目录 output_dir os.path.join(bpy.path.abspath(//), world_coords) os.makedirs(output_dir, exist_okTrue) # 2) 文件名 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename fworld_coords_{camera_obj.name}_{timestamp}.asc filepath os.path.join(output_dir, filename) # 3) 提取命中点坐标 y_idx, x_idx np.where(hit_mask) # 命中像素坐标 points world_coords[y_idx, x_idx] # Nx3 # 4) 写入 .asc try: with open(filepath, w) as f: # 可按需写表头CloudCompare 识别 f.write(f# .asc point cloud generated by Blender addon\n) f.write(f# camera: {camera_obj.name}\n) # 逐行写 xyz np.savetxt(f, points, fmt%.6f) self.report({INFO}, f已保存 asc: {filepath}) except Exception as e: self.report({ERROR}, f保存 asc 失败: {str(e)}) def save_to_npz(self, camera_obj, world_coords, hit_mask): 保存结果到NPZ文件 import os from datetime import datetime # 创建输出目录 output_dir os.path.join(bpy.path.abspath(//), world_coords) os.makedirs(output_dir, exist_okTrue) # 生成文件名 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename fworld_coords_{camera_obj.name}_{timestamp}.npz filepath os.path.join(output_dir, filename) # 保存数据 np.savez_compressed( filepath, world_coordsworld_coords, hit_maskhit_mask, camera_namecamera_obj.name, camera_matrixnp.array(camera_obj.matrix_world), resolutionnp.array([world_coords.shape[1], world_coords.shape[0]]), step_sizeself.step_size, timestamptimestamp ) self.report({INFO}, f数据已保存到: {filepath}) def invoke(self, context, event): 调用操作时显示属性对话框 # 设置默认相机如果场景有相机 if context.scene.camera and not self.camera_name: self.camera_name context.scene.camera.name return context.window_manager.invoke_props_dialog(self) def draw(self, context): 绘制操作属性对话框 layout self.layout # 相机选择下拉框 row layout.row() row.label(text选择相机:) row layout.row() row.prop(self, camera_name, text) # 分辨率设置 row layout.row() row.prop(self, resolution_x) row layout.row() row.prop(self, resolution_y) # 其他设置 row layout.row() row.prop(self, step_size) row layout.row() row.prop(self, save_to_file) # 显示当前选择的相机信息 camera_obj bpy.data.objects.get(self.camera_name) if camera_obj and camera_obj.type CAMERA: box layout.box() box.label(text相机信息:, iconCAMERA_DATA) box.label(textf名称: {camera_obj.name}) box.label(textf类型: {camera_obj.data.type}) box.label(textf焦距: {camera_obj.data.lens}mm) class WORLD_COORD_PT_panel(bpy.types.Panel): 创建UI面板 bl_label 世界坐标映射 bl_idname WORLD_COORD_PT_panel bl_space_type VIEW_3D bl_region_type UI bl_category 工具 def draw(self, context): layout self.layout scene context.scene # 相机选择 row layout.row() row.label(text选择相机:) row layout.row() row.prop_search(scene.world_coord_data, selected_camera, scene, objects, text, iconCAMERA_DATA) # 检查选择的相机是否有效 camera_obj None if scene.world_coord_data.selected_camera: camera_obj bpy.data.objects.get(scene.world_coord_data.selected_camera) if camera_obj and camera_obj.type CAMERA: # 显示相机信息 box layout.box() box.label(text相机信息:, iconINFO) box.label(textf名称: {camera_obj.name}) box.label(textf类型: {camera_obj.data.type}) box.label(textf焦距: {camera_obj.data.lens}mm) # 计算按钮 row layout.row() row.operator(world_coord.calculate, text计算世界坐标, iconCAMERA_DATA) else: # 警告信息 box layout.box() box.label(text请选择一个有效的相机, iconERROR) if scene.camera: box.label(textf场景相机: {scene.camera.name}) # 仍然显示计算按钮但会弹出设置对话框 row layout.row() op row.operator(world_coord.calculate, text计算世界坐标, iconCAMERA_DATA) # 显示上次计算结果 if hasattr(scene, world_coord_data) and scene.world_coord_data.hit_count 0: data scene.world_coord_data box layout.box() box.label(text上次计算结果:, iconTEXT) box.label(textf相机: {data.selected_camera}) box.label(textf分辨率: {data.resolution[0]} x {data.resolution[1]}) box.label(textf命中点数: {data.hit_count}) box.label(textf采样步长: {data.step_size}) # 可视化按钮 row layout.row() row.operator(world_coord.visualize, text可视化结果, iconHIDE_OFF) class WORLD_COORD_OT_visualize(bpy.types.Operator): 可视化世界坐标结果 bl_idname world_coord.visualize bl_label 可视化结果 bl_description 在3D视图中显示世界坐标点 def execute(self, context): scene context.scene # 检查是否有计算结果 if not hasattr(scene, world_coord_raw_data): self.report({WARNING}, 没有找到计算结果数据) return {CANCELLED} # 这里可以添加可视化代码 # 例如创建空物体表示坐标点或者绘制点云 self.report({INFO}, 开始可视化世界坐标点) # 简单的可视化示例在命中点位置创建空物体 try: # 清除之前的可视化对象 self.clear_visualization_objects(scene) # 这里可以添加具体的可视化代码 # 由于原始数据可能很大建议使用采样或简化表示 self.report({INFO}, 可视化完成) except Exception as e: self.report({ERROR}, f可视化失败: {str(e)}) return {FINISHED} def clear_visualization_objects(self, scene): 清除之前创建的可视化对象 # 删除名称以vis_开头的空物体 objects_to_remove [obj for obj in scene.objects if obj.name.startswith(vis_) and obj.type EMPTY] for obj in objects_to_remove: bpy.data.objects.remove(obj, do_unlinkTrue) # 场景中所有相机的列表属性用于UI def get_camera_objects(self, context): 获取场景中所有相机的列表 items [] cameras [obj for obj in context.scene.objects if obj.type CAMERA] for i, camera in enumerate(cameras): items.append((camera.name, camera.name, f相机: {camera.name}, CAMERA_DATA, i)) if not items: items.append((NONE, 无相机, 场景中没有相机, ERROR, 0)) return items # 注册和取消注册函数 def register(): bpy.utils.register_class(WorldCoordData) bpy.utils.register_class(WORLD_COORD_OT_calculate) bpy.utils.register_class(WORLD_COORD_PT_panel) bpy.utils.register_class(WORLD_COORD_OT_visualize) # 添加场景属性 bpy.types.Scene.world_coord_data bpy.props.PointerProperty(typeWorldCoordData) def unregister(): bpy.utils.unregister_class(WorldCoordData) bpy.utils.unregister_class(WORLD_COORD_OT_calculate) bpy.utils.unregister_class(WORLD_COORD_PT_panel) bpy.utils.unregister_class(WORLD_COORD_OT_visualize) # 清理场景属性 if hasattr(bpy.types.Scene, world_coord_data): del bpy.types.Scene.world_coord_data if __name__ __main__: register()

相关新闻

2026/8/11 18:37:07

能源物联网系统由什么组成

第一层:端(感知层)—— 人体的“神经末梢和五官”这是最底层的硬件设备,负责感知和执行,是数据的源头。感知类(测量):智能电表(不仅计费,还能记录电压/电流/功…

2026/8/11 18:37:07

瑞芯微RV1126B开发板(EASY-EAI-PI2) yolov8训练部署教程

Yolov8简介 YOLOv8 是 ultralytics 公司在 2023 年 1月 10 号开源的基于YOLOV5进行更新的 下一个重大更新版本,目前支持图像分类、物体检测和实例分割任务,鉴于Yolov5的良好表现,Yolov8在还没有开源时就收到了用户的广泛关注。其主要结构如下…

2026/8/11 18:37:07

Cortex-M 与 RTOS:先拆中断、任务还是驱动边界

Cortex-M 与 RTOS:先拆中断、任务还是驱动边界 拆 RTOS 功能时,先从数据如何进入、在哪里处理、怎样输出开始,而不是先按“一个模块一个任务”分工。任务拆得太早,往往只会增加队列、锁和排障难度。 找到不可阻塞的路径 先标出中断…

2026/8/11 19:22:26

什么是防爆门

防爆门:高危场所的安全屏障在化工、煤矿、储能、危化仓库等存在爆炸风险的场所,普通门窗无法抵御爆炸瞬间产生的冲击波、高温火焰与高速飞溅碎片,防爆门(抗爆门)作为特种防护构件,承担着抵御爆炸冲击、阻隔…

2026/8/11 19:22:26

相位差造就螺旋电场:一文读懂天线极化的底层奥秘

90相位差如何让直线振荡变为旋转螺旋——从线极化到圆极化的完整拆解在无线通信领域,频率、功率、增益是大家耳熟能详的关键参数,却很少有人重视天线极化这一决定通信成败的核心要素。小到无人机图传、GPS 定位,大到卫星通联、深空探测&#…

2026/8/11 19:22:26

Style2Paints V4.5:如何用AI将草图秒变专业彩图的终极指南

Style2Paints V4.5:如何用AI将草图秒变专业彩图的终极指南 【免费下载链接】style2paints sketch style paints :art: (TOG2018/SIGGRAPH2018ASIA) 项目地址: https://gitcode.com/gh_mirrors/st/style2paints 在数字绘画的世界里,你是否曾为繁…

2026/8/11 19:22:26

终极窗口管理方案:3分钟学会强制调整任意软件界面大小

终极窗口管理方案:3分钟学会强制调整任意软件界面大小 【免费下载链接】WindowResizer 一个可以强制调整应用程序窗口大小的工具 项目地址: https://gitcode.com/gh_mirrors/wi/WindowResizer 还在为老旧软件界面太小而烦恼吗?是否经常遇到游戏窗…

2026/8/11 3:03:40

如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南 【免费下载链接】chinese_license_plate_generator 中国车牌生成器 项目地址: https://gitcode.com/gh_mirrors/ch/chinese_license_plate_generator 中国车牌生成器是一个基于Python的开源项目&#xff0c…

2026/8/11 5:34:14

当 LLM 遇见大文档:主流开源项目如何处理上下文超限

从 Agentic Loop 到 Repo Map,七种策略与六类陷阱引言:128K vs 10MB 的硬冲突 2026 年的 LLM 上下文窗口已达到 128K ~ 1M token(≈ 0.5MB ~ 4MB 文本),但 LLM 想要处理的真实数据规模远远超过这个量级:真实…

2026/8/11 0:00:39

前后端分离项目中控制台与接口工具数据差异排查指南

1. 问题现象解析:控制台与Apifox的数据差异 最近在调试一个前后端分离项目时,遇到了一个典型问题:后端服务在本地开发环境控制台能正常输出查询数据,但通过Apifox测试时却返回空结果。这种"控制台有数据,接口工具…

2026/8/11 0:00:39

AI编程实战:从Claude Code踩坑到游戏开发入门

1. 从“AI能帮我做游戏”到“AI让我重新学编程”最近身边不少朋友,尤其是一些非技术背景、但对游戏开发有浓厚兴趣的朋友,都在问我同一个问题:“听说现在用Claude Code这种AI编程工具,小白也能做游戏了,是真的吗&#…

2026/8/10 11:20:30

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/11 17:06:59

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/11 3:05:11

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…