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/10/18 20:45:32 阅读更多 →
瑞芯微RV1126B开发板(EASY-EAI-PI2) yolov8训练部署教程

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

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

2026/10/19 4:06:55 阅读更多 →
Cortex-M 与 RTOS:先拆中断、任务还是驱动边界

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

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

2026/10/18 21:51:17 阅读更多 →

最新新闻

达梦数据库大小写敏感机制详解:从原理到实践避坑

达梦数据库大小写敏感机制详解:从原理到实践避坑

1. 项目概述:达梦大小写敏感到底是个什么事做数据库开发或者运维的朋友,只要是第一次接触达梦数据库,十有八九会遇到一个让人摸不着头脑的现象:明明建表时写的是 "USER_INFO",去查询的时候写 "user_inf…

2026/10/19 20:52:07 阅读更多 →
C++ std::bind实战:占位符、引用陷阱与lambda取舍

C++ std::bind实战:占位符、引用陷阱与lambda取舍

1. 我为什么又回头研究bind:代码里最不起眼的粘连剂搞C时间一长,你会发现真正让代码"绕起来"的往往不是那些宏大设计,而是一堆不起眼的小工具。std::bind就是其中之一。最近接手的某调度模块里,回调参数七零八落&#x…

2026/10/19 20:52:07 阅读更多 →
双麦克风语音分离:基于GCC-PHAT与IVA的轻量级实时声源解耦方案

双麦克风语音分离:基于GCC-PHAT与IVA的轻量级实时声源解耦方案

简介:本资源是一套基于MATLAB实现的双麦克风语音分离完整源码工程,面向音频信号处理初学者、语音算法研究者及高校相关课程实践者,聚焦于利用TDOA与空间滤波技术从混合语音中分离目标声源,适用于噪声抑制、会议系统增强与语音识别…

2026/10/19 20:52:07 阅读更多 →
2026 最新抖音 SEO 推广全攻略,吃透抖音 SEO 视频排名逻辑

2026 最新抖音 SEO 推广全攻略,吃透抖音 SEO 视频排名逻辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 20:50:01 阅读更多 →
菲涅尔衍射模型解析WiFi呼吸检测:从原理到工程实践

菲涅尔衍射模型解析WiFi呼吸检测:从原理到工程实践

1. 从光学衍射到无线感知:这个项目到底在做什么 菲涅尔衍射和WiFi呼吸检测,乍一看是两个八竿子打不着的领域。一个是光学课本里的经典波动光学内容,一个是近几年火起来的无线感知应用。但把这两个东西放在一起,恰恰说明了一个很有…

2026/10/19 20:48:59 阅读更多 →
西门子1200与FactoryIO机械手仿真:状态机实现虚拟调试全流程

西门子1200与FactoryIO机械手仿真:状态机实现虚拟调试全流程

做工业自动化调试,最怕设备还没到场、程序就得交付;最怕机械没接线、电气没上电,写好的逻辑只能在脑子里跑。我前阵子捣鼓的一套FactoryIO机械手仿真取料程序,算是把这个困局撕开了一个口子:FactoryIO提供虚拟车间&…

2026/10/19 20:48:59 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →