点云数据处理--splat转3dtiles

文章目录

  • 处理流程简介
  • 核心功能实现
    • 数据读取与格式转换
      • 定义Point类
      • 数据读取
      • splat转gltf
    • 点云数据分割
      • 定义四叉树
      • 递归生成3dtiles瓦片
    • 生成tileset.json
      • 递归生成tileset.json
      • 计算box
    • 主函数调用
    • 渲染
  • 下一步工作
    • 性能优化
    • 渲染效果调优
    • 其他

源码地址: github

处理流程简介

基本流程:

  • 读取点云数据。
  • 制作tile
    • 构建四叉树
    • 分割点云
    • 将点云转换为glTF格式。
  • 生成配置文件tileset.json。

前置知识:

  • glTF教程
  • glTF2.0 高斯扩展
  • 3dtiles 1.1 规范

核心功能实现

数据读取与格式转换

定义Point类

class Point:def __init__(self, position: Tuple[float, float, float], color: Tuple[int, int, int, int],scale: Tuple[float, float, float], rotation: Tuple[int, int, int, int]):self.position = positionself.color = colorself.scale = scaleself.rotation = rotationdef to_bytes(self) -> bytes:"""将点数据打包为二进制格式"""return struct.pack('3f4B3f4B', *self.position, *self.color, *self.scale, *self.rotation)@classmethoddef from_bytes(cls, data: bytes):"""从二进制数据解析为点"""unpacked = struct.unpack('3f4B3f4B', data)position = unpacked[:3]color = unpacked[3:7]scale = unpacked[7:10]rotation = unpacked[10:]return cls(position, color, scale, rotation)

数据读取

def read_splat_file(file_path: str) -> List[Point]:"""读取二进制格式的 Splat 文件:param file_path: Splat 文件路径:return: 包含位置、缩放、颜色、旋转数据的 Point 对象列表"""points = []with open(file_path, 'rb') as f:while True:position_data = f.read(3 * 4)  # 3个 Float32,每个4字节if not position_data:breakposition = struct.unpack('3f', position_data)scale = struct.unpack('3f', f.read(3 * 4))color = struct.unpack('4B', f.read(4 * 1))rotation = struct.unpack('4B', f.read(4 * 1))points.append(Point(position, color, scale, rotation))return points

splat转gltf

遵循3dtiles 1.1 规范,在glTF 2.0 基础上,增加高斯扩展。

def splat_to_gltf_with_gaussian_extension(points: List[Point], output_path: str):"""将 Splat 数据转换为支持 KHR_gaussian_splatting 扩展的 glTF 文件:param points: Point 对象列表:param output_path: 输出的 glTF 文件路径"""# 提取数据positions = np.array([point.position for point in points], dtype=np.float32)colors = np.array([point.color for point in points], dtype=np.uint8)scales = np.array([point.scale for point in points], dtype=np.float32)rotations = np.array([point.rotation for point in points], dtype=np.uint8)normalized_rotations = rotations / 255.0# 创建 GLTF 对象gltf = GLTF2()gltf.extensionsUsed = ["KHR_gaussian_splatting"]# 创建 Bufferbuffer = Buffer()gltf.buffers.append(buffer)# 将数据转换为二进制positions_binary = positions.tobytes()colors_binary = colors.tobytes()scales_binary = scales.tobytes()rotations_binary = normalized_rotations.tobytes()# 创建 BufferView 和 Accessordef create_buffer_view(byte_offset: int, data: bytes, target: int = 34962) -> BufferView:return BufferView(buffer=0, byteOffset=byte_offset, byteLength=len(data), target=target)def create_accessor(buffer_view: int, component_type: int, count: int, type: str, max: List[float] = None, min: List[float] = None) -> Accessor:return Accessor(bufferView=buffer_view, componentType=component_type, count=count, type=type, max=max, min=min)buffer_views = [create_buffer_view(0, positions_binary),create_buffer_view(len(positions_binary), colors_binary),create_buffer_view(len(positions_binary) +len(colors_binary), rotations_binary),create_buffer_view(len(positions_binary) +len(colors_binary) + len(rotations_binary), scales_binary)]accessors = [create_accessor(0, 5126, len(positions), "VEC3", positions.max(axis=0).tolist(), positions.min(axis=0).tolist()),create_accessor(1, 5121, len(colors), "VEC4"),create_accessor(2, 5126, len(normalized_rotations), "VEC4"),create_accessor(3, 5126, len(scales), "VEC3")]gltf.bufferViews.extend(buffer_views)gltf.accessors.extend(accessors)# 创建 Mesh 和 Primitiveprimitive = Primitive(attributes={"POSITION": 0, "COLOR_0": 1, "_ROTATION": 2, "_SCALE": 3},mode=0,extensions={"KHR_gaussian_splatting": {"positions": 0, "colors": 1, "scales": 2, "rotations": 3}})mesh = Mesh(primitives=[primitive])gltf.meshes.append(mesh)# 创建 Node 和 Scenenode = Node(mesh=0)gltf.nodes.append(node)scene = Scene(nodes=[0])gltf.scenes.append(scene)gltf.scene = 0# 将二进制数据写入 Buffergltf.buffers[0].uri = "data:application/octet-stream;base64," + base64.b64encode(positions_binary + colors_binary + rotations_binary + scales_binary).decode("utf-8")gltf.save(output_path)print(f"glTF 文件已保存到: {output_path}")

点云数据分割

定义四叉树

定义四叉树类,包含基本方法,初始化、插入、分割、判断点是否在边界范围内等等。

#四叉树
class QuadTreeNode:def __init__(self, bounds: Tuple[float, float, float, float], capacity: int = 100000):"""初始化四叉树节点。:param bounds: 节点的边界 (min_x, min_y, max_x, max_y):param capacity: 节点容量(每个节点最多存储的点数)"""self.bounds = boundsself.capacity = capacityself.points: List[Point] = []  # 存储点数据self.children = Nonedef insert(self, point: Point) -> bool:"""将点插入四叉树。:param point: 要插入的点:return: 是否插入成功"""if not self._contains(point.position):return Falseif len(self.points) < self.capacity:self.points.append(point)return Trueelse:if self.children is None:self._subdivide()return any(child.insert(point) for child in self.children)def _contains(self, position: Tuple[float, float, float]) -> bool:"""检查点是否在节点边界内。:param position: 点的位置 (x, y, z):return: 是否在边界内"""x, y, _ = positionmin_x, min_y, max_x, max_y = self.boundsreturn min_x <= x < max_x and min_y <= y < max_ydef _subdivide(self):"""将节点划分为四个子节点。"""min_x, min_y, max_x, max_y = self.boundsmid_x = (min_x + max_x) / 2mid_y = (min_y + max_y) / 2self.children = [QuadTreeNode((min_x, min_y, mid_x, mid_y), self.capacity),QuadTreeNode((mid_x, min_y, max_x, mid_y), self.capacity),QuadTreeNode((min_x, mid_y, mid_x, max_y), self.capacity),QuadTreeNode((mid_x, mid_y, max_x, max_y), self.capacity)]for point in self.points:for child in self.children:if child.insert(point):breakself.points = []  # 清空当前节点的点数据def get_all_points(self) -> List[Point]:"""获取当前节点及其子节点中的所有点。:return: 所有点的列表"""points = self.points.copy()if self.children is not None:for child in self.children:points.extend(child.get_all_points())return points

递归生成3dtiles瓦片

def generate_3dtiles(node: QuadTreeNode, output_dir: str, tile_name: str):if node.children is not None:for i, child in enumerate(node.children):generate_3dtiles(child, output_dir, f"{tile_name}_{i}")elif len(node.points) > 0:points = node.get_all_points()splat_to_gltf_with_gaussian_extension(points, f"{output_dir}/{tile_name}.gltf")

生成tileset.json

递归生成tileset.json

generate_tileset_json

def generate_tileset_json(output_dir: str, root_node: QuadTreeNode, bounds: List[float], geometric_error: int = 100):def build_tile_structure(node: QuadTreeNode, tile_name: str, current_geometric_error: int) -> Dict:bounding_volume = {"region": compute_region([point.position for point in node.get_all_points()])} if is_geographic_coordinate else {"box": compute_box([point.position for point in node.get_all_points()])}content = {"uri": f"{tile_name}.gltf"} if not node.children else Nonechildren = [build_tile_structure(child, f"{tile_name}_{i}", current_geometric_error / 2)for i, child in enumerate(node.children)] if node.children else []tile_structure = {"boundingVolume": bounding_volume,"geometricError": current_geometric_error,"refine": "ADD","content": content}if children:tile_structure["children"] = childrendel tile_structure["content"]return tile_structuretileset = {"asset": {"version": "1.1", "gltfUpAxis": "Z"},"geometricError": geometric_error,"root": build_tile_structure(root_node, "tile_0", geometric_error)}with open(f"{output_dir}/tileset.json", "w") as f:json.dump(tileset, f, cls=NumpyEncoder, indent=4)

数据格式转换

class NumpyEncoder(json.JSONEncoder):def default(self, obj):if isinstance(obj, (np.int_, np.intc, np.intp, np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64)):return int(obj)elif isinstance(obj, (np.float_, np.float16, np.float32, np.float64)):return float(obj)elif isinstance(obj, np.ndarray):return obj.tolist()return json.JSONEncoder.default(self, obj)

计算box

def compute_box(points: np.ndarray) -> List[float]:center = np.mean(points, axis=0)half_size = (np.max(points, axis=0) - np.min(points, axis=0)) / 2return [center[0], center[1], center[2], half_size[0], 0, 0, 0, half_size[1], 0, 0, 0, half_size[2]]

主函数调用

def main(input_path: str, output_dir: str):# 读取 .splat 文件points = read_splat_file(input_path)# 创建四叉树根节点positions = np.array([point.position for point in points])min_x, min_y = np.min(positions[:, :2], axis=0)max_x, max_y = np.max(positions[:, :2], axis=0)root = QuadTreeNode((min_x, min_y, max_x, max_y), capacity=100000)# 将点插入四叉树for point in points:root.insert(point)# 生成 3D Tilesgenerate_3dtiles(root, output_dir, "tile_0")# 生成 tileset.jsonbounds = [min_x, min_y, np.min(positions[:, 2]), max_x, max_y, np.max(positions[:, 2])]generate_tileset_json(output_dir, root, bounds)if __name__ == "__main__":# 解析命令行参数parser = argparse.ArgumentParser(description="将 Splat 文件转换为 3D Tiles。")parser.add_argument("input_path", type=str, help="输入的 .splat 文件路径")parser.add_argument("output_dir", type=str, help="输出的 3D Tiles 目录路径")args = parser.parse_args()# 调用主函数main(args.input_path, args.output_dir)

渲染

编译cesium的splat-shader版本,参考示例代码3D Tiles Gaussian Splatting.html实现。

async function loadTileset() {try {const tileset = await Cesium.Cesium3DTileset.fromUrl("http://localhost:8081/data/outputs/model/tileset.json",{modelMatrix:computeModelMatrix(),maximumScreenSpaceError: 1,}).then((tileset) => {CesiumViewer.scene.primitives.add(tileset);setupCamera();});} catch (error) {console.error(`Error creating tileset: ${error}`);}
}

下一步工作

性能优化

  • 支持LOD 。
  • 支持多线程、多任务,分批处理 。
  • 切片方案优化,尝试构建其他空间索引,例如八叉树 。

渲染效果调优

目前渲染效果不理想,椭圆的某个轴长过大,问题排查中。

其他

其他待优化项。本文输出的是一个简易版的splat转3dtiles工具,供学习和交流使用,待优化的地方,若有精力后续会持续完善。

参考资料:
[1] https://github.com/KhronosGroup/glTF-Tutorials/tree/main/gltfTutorial
[2] https://github.com/CesiumGS/3d-tiles
[3] https://github.com/CesiumGS/glTF/tree/proposal-KHR_gaussian_splatting/extensions/2.0/Khronos/KHR_gaussian_splatting
[4] https://github.com/CesiumGS/cesium/tree/splat-shader

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/72939.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OneM2M:全球性的物联网标准-可应用于物联网中

OneM2M 是一个全球性的物联网(IoT)标准,旨在为物联网设备和服务提供统一的框架和接口,以实现设备之间的互操作性、数据共享和服务集成。OneM2M 由多个国际标准化组织(如 ETSI、TIA、TTC、ARIB 等)共同制定,目标是解决物联网领域的碎片化问题,提供一个通用的标准,支持跨…

【Python 入门基础】—— 人工智能“超级引擎”,AI界的“瑞士军刀”,

欢迎来到ZyyOvO的博客✨&#xff0c;一个关于探索技术的角落&#xff0c;记录学习的点滴&#x1f4d6;&#xff0c;分享实用的技巧&#x1f6e0;️&#xff0c;偶尔还有一些奇思妙想&#x1f4a1; 本文由ZyyOvO原创✍️&#xff0c;感谢支持❤️&#xff01;请尊重原创&#x1…

Java爬虫获取淘宝商品详情数据的完整指南

在电商领域&#xff0c;获取商品详情数据对于市场分析、价格监控、用户体验优化等场景具有重要意义。淘宝作为国内领先的电商平台&#xff0c;提供了丰富的API接口供开发者使用&#xff0c;其中item_get和item_get_pro接口可以用来获取商品的详细信息。本文将详细介绍如何使用J…

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_init_cycle 函数

nei声明在 src/core/ngx_cycle.h ngx_cycle_t *ngx_init_cycle(ngx_cycle_t *old_cycle);实现在 src/core/ngx_cycle.c ngx_cycle_t * ngx_init_cycle(ngx_cycle_t *old_cycle) {void *rv;char **senv;ngx_uint_t i, n;ngx_log_t …

qt 操作多个sqlite文件

qt 操作多个sqlite文件 Chapter1 qt 操作多个sqlite文件1. 引入必要的头文件2. 创建并连接多个SQLite数据库3. 代码说明4. 注意事项 Chapter2 qt 多线程操作sqlite多文件1. 引入必要的头文件2. 创建数据库操作的工作线程类3. 在主线程中创建并启动多个工作线程4. 代码说明5. 运…

最新版本WebContext构造函数-避坑

import org.thymeleaf.context.IWebContext; import org.thymeleaf.context.WebContext; 当你想把页面信息全部获取出来存到redis缓存中使用时&#xff0c;SpringWebContext在Spring5中报错 SpringWebContext ctx new SpringWebContext(request, response,request.getServlet…

用Python分割并高效处理PDF大文件

在处理大型PDF文件时&#xff0c;将它们分解成更小、更易于管理的块通常是有益的。这个过程称为分区&#xff0c;它可以提高处理效率&#xff0c;并使分析或操作文档变得更容易。在本文中&#xff0c;我们将讨论如何使用Python和为Unstructured.io库将PDF文件划分为更小的部分。…

neo4j随笔-将csv文件导入知识图谱

目录 导入前的准备 导入csv文件 导入nodes1.1.csv并动态为节点添加标签 ​编辑导入relations1.1.csv并动态为关系添加标签 结果 导入前的准备 我有两个csv文件 nodes1.1.csv存放节点信息,用记事本打开&#xff0c;能正常显示&#xff0c;且编码为UTF-8&#xff0c;就可以…

cpu 多级缓存L1、L2、L3 与主存关系

现代 CPU 的多级缓存&#xff08;L1、L2、L3&#xff09;和主存&#xff08;DRAM&#xff09;构成了一个层次化的内存系统&#xff0c;旨在通过减少内存访问延迟和提高数据访问速度来优化计算性能。以下是对多级缓存和主存的详细解析&#xff1a; 1. 缓存层次结构 现代 CPU 通…

C++:入门详解(关于C与C++基本差别)

目录 一.C的第一个程序 二.命名空间&#xff08;namespace&#xff09; 1.命名空间的定义与使用&#xff1a; &#xff08;1&#xff09;命名空间里可以定义变量&#xff0c;函数&#xff0c;结构体等多种类型 &#xff08;2&#xff09;命名空间调用&#xff08;&#xf…

Python的学习篇(七)--网页结构

七、网页&#xff08;HTML&#xff09;结构 7.1 HTML介绍 HTML(Hyper Text Markup Language)&#xff0c;超文本标记语言。 超文本&#xff1a;比文本的功能要强大&#xff0c;通过链接和交互式的方式来组织与呈现信息的文本形式。不仅仅有文本&#xff0c;还可以包含图片、…

*VulnHub-FristiLeaks:1.3暴力解法、细节解法,主打软硬都吃,隧道搭建、寻找exp、提权、只要你想没有做不到的姿势

*VulnHub-FristiLeaks:1.3暴力解法、细节解法&#xff0c;主打软硬都吃&#xff0c;隧道搭建、寻找exp、提权、只要你想没有做不到的姿势 一、信息收集 1、扫靶机ip 经典第一步&#xff0c;扫一下靶机ip arp-scan -l 扫描同网段 nmap -sP 192.168.122.0/242、指纹扫描、端口…

PHP:格式化JSON为PHP语法格式

1. 原生函数 $arr [1,2,3,4]; $str var_export($a,true); var_dump($str); 2. 自定义方法 class Export{private static $space;private static function do($a, string $prev){$res ;$next $prev . self::$space;if (is_array($a)) {$res . [;foreach ($a as $k > $…

【Python 数据结构 9.树】

我装作漠视一切&#xff0c;其实我在乎的太多&#xff0c;但我知道抓得越紧越容易失去 —— 25.3.6 一、树的基本概念 1.树的定义 树是n个结点的有限集合&#xff0c;n0时为空树。当n大于0的时候&#xff0c;满足如下两个条件&#xff1a; ① 有且仅有一个特定的结点&#xff…

pyqt联合designer的运用和设置

PyQt Designer 简介 PyQt Designer 是一个用于创建和设计 PyQt 应用程序用户界面的可视化工具。它允许用户通过拖放方式添加和排列各种控件,如按钮、文本框、滑块等,并设置它们的属性和样式,从而快速构建出美观且功能完整的 UI 界面。 Windows版本:【免费】安装包别管啊啊…

纯html文件实现目录和文档关联

目录结构 效果图 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>项目结题报告</title><style lang"scss">::-webkit-scrollbar {width: 6px;height: 6px;}::-webkit-scro…

python用户图形界面wxpython库安装与使用

要开始使用 wxPython 库来创建 Python 用户图形界面&#xff0c;首先需要安装这个库。在大多数情况下&#xff0c;你可以通过 pip 来安装 wxPython。下面我会指导你完成安装过程&#xff0c;并给出一个简单的例子来展示如何使用 wxPython 创建一个基本的窗口应用程序。 安装 w…

MongoDB winx64 msi包安装详细教程

首先我们可以从官网上选择对应版本和对应的包类型进行安装&#xff1a; 下载地址&#xff1a;Download MongoDB Community Server | MongoDB 这里可以根据自己的需求&#xff0c; 这里我选择的是8.0.5 msi的版本&#xff0c;采用的传统装软件的方式安装。无需配置命令。 下载…

如何借助 ArcGIS Pro 高效统计基站 10km 范围内的村庄数量?

在当今数字化时代&#xff0c;地理信息系统&#xff08;GIS&#xff09;技术在各个领域都发挥着重要作用。 特别是在通信行业&#xff0c;对于基站周边覆盖范围内的地理信息分析&#xff0c;能够帮助我们更好地进行网络规划、资源分配以及市场分析等工作。 今天&#xff0c;就…

saltstack通过master下发脚本批量修改minion_id,修改为IP

通过master下发脚本批量修改minion_id&#xff0c;以修改为IP为例 通过cmd.script远程执行shell脚本修改minion_id&#xff0c;步骤如下: # 下发脚本并执行 >> salt old_minion_id cmd.script salt://modify_minion_id.sh saltenvdev #输出结果 old_minion_id:Minion di…