使用Python读取las点云,写入las点云,无损坐标精度

时间:2025-05-06 07:08:17
""" update: 2025/3/7 已经发布到 **** :/qq_27816785/article/details/138634496 使用 laspy v2.3 1. 注意: * 实测版本 laspy v2.3,版本应该都可用,但不适合版本。 * las 存储数据时,需要设置 scales 和 offsets,否则会出现精度问题。 * las 存储颜色时,数值类型为 16 位无符号整型。rgb = (normal_rgb * 65535).astype(np.uint16) 2. las 格式原生支持的属性字段,与las版本密切相关。官方说明:/en/latest/#point-records 3. 对 scales 和 offsets 的理解: /questions/346679/if-converting-xyz-to-las-using-e-g-laspy-then-how-to-specify-offset-and-scal 比例scales 表明数据的准确性。 0.001 是毫米精度。这意味着如果您的坐标是 0.123456,它将被限制为 0.123。 偏移offset 的目的是避免整数溢出。假设您要存储 123456789.123。在 LAS 文件中,您实际上将存储一个整数:123456789123, 该整数将在读取时使用比例因子转换为 123456789.123。但 123456789123 比 32 位整数所能存储的要大得多。 因此存储时,将该值偏移 123450000,实际存的是6789123。(6789123 * 0.001 + 123450000 = 123456789.123) 所以 xmin、ymax、zmin 是有效的偏移值选择,但我们通常更喜欢将这些值四舍五入到最接近的整数米/公里 4. 请注意,读取las后,再重新保存las时,最好使用原文件的 scales 和 offsets,以保持精度。因为这2个参数都会影响新处理的点是否与原始点一致。 scales 保证了在 0.001 毫米级别下的精度一致。 但 offsets 可能会导致毫米级别以下的精度损失,因此造成小于毫米级的精度损失。 """ import warnings import laspy import numpy as np def read_las_fit(filename, user_attrs=None, ignore_default=True): """ 读取 las 文件,获取三维坐标 xyz, 颜色 rgb, 属性 attr_dict。 当文件没有 RGB 信息时,返回全0的 RGB 信息 当 user_attrs 缺省时,返回所有的属性(忽略无效值) Args: filename: <str> las 文件路径 user_attrs: <list> 需要额外获取的属性信息 如 ['label'] Returns: xyz, rgb, attr_dict """ # 读取点云数据 inFile = laspy.read(filename) # 读取header信息 version, point_format = get_las_info(filename) # 获取支持的属性字段 all_fields = get_las_header_attrs(point_format, version) print(f" --读取 {filename.split('/')[-1]} 文件,版本={version}, 点格式={point_format}") print(f" --支持的属性字段:{all_fields}") if user_attrs is None: # 获取所有可用的维度信息,但不包括 X Y Z red green blue not_need = ["X", "Y", "Z", "red", "green", "blue"] user_attrs = [dim.name for dim in inFile.point_format.dimensions if dim.name not in not_need] N_points = len(inFile) x = np.reshape(inFile.x, (N_points, 1)) y = np.reshape(inFile.y, (N_points, 1)) z = np.reshape(inFile.z, (N_points, 1)) xyz = np.hstack((x, y, z)) ''' 注意。如果是大写的 X Y Z,需要转换后才是真实坐标: real_x = scale[0] * + offset[0] ''' # 初始化 rgb 全是 0 rgb = np.zeros((N_points, 3), dtype=np.uint16) if hasattr(inFile, "red") and hasattr(inFile, "green") and hasattr(inFile, "blue"): r = np.reshape(inFile.red, (N_points, 1)) g = np.reshape(inFile.green, (N_points, 1)) b = np.reshape(inFile.blue, (N_points, 1)) # i = (, (N_points, 1)) rgb = np.hstack((r, g, b)) else: print(f"注意:{filename.split('/')[-1]} 没有RGB信息,返回全0的RGB信息!") # 无论 user_attrs 是否需要,都会返回 point_format、version、scales、offsets attr_dict = { "point_format": point_format, "version": version, "scales": inFile.header.scale, "offsets": inFile.header.offset } for attr in user_attrs: value = None # 先判断 是否为额外属性 if hasattr(inFile, attr): value = getattr(inFile, attr) print(f" -获取有效属性:{attr}") # 包括固有属性,如 gps_time, intensity, 以及额外属性,如 label, pred # 再判断 header 中是否有该属性 elif hasattr(inFile.header, attr): value = getattr(inFile.header, attr) print(f" --获取header属性:{attr}") else: warnings.warn(f"{filename.split('/')[-1]} 没有属性= {attr} 的信息!", category=Warning) # 使用 warnning 警告 # 获取到值后,如果是 n * 1 的 ndarray 时,转换为 1 维数组 if hasattr(value, "array"): value = np.array(value) # 判断值是否有效,如果是无效值,则不添加到 attr_dict 中 if ignore_default: if value is not None and not np.all(value == 0) and user_attrs is not None: attr_dict[attr] = value else: attr_dict[attr] = value print() return xyz, rgb, attr_dict def write_las_fit(out_file, xyz, rgb=None, attrs=None): """ 将点云数据写入 las 文件,支持写入 坐标xyz, 颜色rgb, 属性attrs Args: out_file: 输出文件路径 xyz: 点云坐标 ndarray (N, 3) rgb: 点云颜色 ndarray (N, 3) attrs: 字典{key:value, ...} 固有属性:file_source_id, gps_time, Intensity, Number of Returns, .... 额外属性:label, pred, ... 注意:如果不传入 scales 和 offsets,则会自动计算 Returns: """ if attrs is None: attrs = {} ''' 创建 las 文件头。point_format和version决定了las支持哪些固有属性 ''' # 详情见 /en/latest/?highlight=red#point-records point_format = attrs.pop("point_format") if "point_format" in attrs else 7 # 认为 7 支持rgb version = attrs.pop("version") if "version" in attrs else "1.4" # 默认 1.4 header = laspy.LasHeader(point_format=point_format, version=version) ''' 自动计算 scales 和 offsets,确保坐标精度无损 ''' # /questions/77308057/conversion-accuracy-issues-of-e57-to-las-in-python-using-pye57-and-laspy header.scale = attrs.pop("scales") if "scales" in attrs else [0.001, 0.001, 0.001] # 0.001 是毫米精度 # 如果保存时与原las文件的offsets不同,可能会出现偏移(偏移精度 < scales精度限制) header.offset = attrs.pop("offsets") if "offsets" in attrs else np.floor(np.min(xyz, axis=0)) ''' 初始化一些需要保存的属性值。 ''' extra_attr = [] # 如果是额外属性,添加到 header 中, 后续赋值 # 获取当前版本的保留字段(与 point_format 和 version 有关) all_reserved_fields = get_las_header_attrs(point_format, version) for attr, value in attrs.items(): if attr not in all_reserved_fields: # 添加额外属性,在 las 初始化后赋值,如 label, pred header.add_extra_dim(laspy.ExtraBytesParams(name=attr, type=np.float32)) extra_attr.append(attr) ''' 创建 las 文件对象 ''' las = laspy.LasData(header) # 添加xyz坐标 las.x = xyz[:, 0] las.y = xyz[:, 1] las.z = xyz[:, 2] ''' 添加颜色信息 ''' # 如果RGB全是0, 则不存储颜色。如果是归一化的颜色,则需要乘以 65535,转为 uint16 if (rgb is not None) and (np.max(rgb) > 0): if np.max(rgb) <= 1: rgb = (rgb * 65535).astype(np.uint16) # 65535 = 2^16 - 1, las存储颜色是16位无符号整型 elif np.max(rgb) <= 255: rgb = (rgb / 255 * 65535).astype(np.uint16) las.red = rgb[:, 0] las.green = rgb[:, 1] las.blue = rgb[:, 2] ''' 设置固有属性 ''' for attr, value in attrs.items(): if attr in all_reserved_fields: # 如果是保留字段,则不添加额外属性。如 X, Y, Z, Red, Green, Blue if value.ndim == 2 and value.shape[1] == 1: value = value.flatten() las[attr] = value ''' 设置额外属性 ''' for attr in extra_attr: # 当 value 是 n * 1 的 ndarray 时,转换为 1 维数组 value = attrs[attr] if value.ndim == 2 and value.shape[1] == 1: value = value.flatten() las[attr] = value ''' 写入文件 ''' las.write(out_file) def get_las_info(las_file): """ 读取 LAS 文件的格式和版本信息(适用于 laspy ) :param las_file: LAS 文件路径 :return: 版本号 () 和 点格式 ID """ with laspy.open(las_file) as las: # 以只读方式打开 LAS 文件 header = las.header # 读取头部信息 # 获取 LAS 版本号 version_major = header.version.major version_minor = header.version.minor version = f"{version_major}.{version_minor}" # 获取点格式(point format ID) point_format = header.point_format.id return version, point_format def get_las_header_attrs(point_format=7, version="1.4"): """ 根据 point_format 和 version 获取 las 文件的 header 属性 说明文档:/en/latest/#point-records Args: point_format: 点格式 version: 版本 Returns: """ dimensions = [] header = laspy.LasHeader(point_format=point_format, version=version) # 7 支持rgb for dim in header.point_format.dimensions: dimensions.append(dim.name) return dimensions if __name__ == '__main__': # 测试1: 获取 las 文件头属性 fields = get_las_header_attrs(7, "1.4") print(f"point_format=7, version=1.4, 头文件包含字段: {fields}") # 测试2: 读取LAS文件 read_las_path = "/home/gisleung/dataset2/Epoch_March2018/LiDAR/Mar18_test_small.las" xyz_, rgb_, attrs_ = read_las_fit(read_las_path, ["scales", "offsets", "leung"]) print(attrs_) # 测试3: 写入LAS文件 # save_las_path = "/home/gisleung/dataset2/Epoch_March2018/LiDAR/Mar18_test_small2_fit.las" # write_las_fit(save_las_path, xyz_, rgb_, { # # "scales": attrs_["scales"], # # "offsets": attrs_["offsets"] # })