I'm trying to convert a txt
format pointcloud to las
to be able to view it in Potree. I'm using the following python script:
import laspy
import numpy as np
def read_points3D(file_path):
points = []
with open(file_path, 'r') as file:
for line in file:
if line.startswith('#') or line.strip() == "":
continue
parts = line.strip().split()
point_id = int(parts[0]) # Read the 3D point identifier
x, y, z = map(float, parts[1:4])
r, g, b = map(int, parts[4:7])
error = float(parts[7]) # Read `error`
building_id = int(parts[8]) # Extract `building_id`
points.append((point_id, x, y, z, r, g, b, error, building_id))
return np.array(points)
def write_las(output_path, points):
# Set up header with format 3 (supports RGB) and version 1.2
header = laspy.LasHeader(point_format=3, version="1.2")
header.x_scale = header.y_scale = header.z_scale = 0.01
header.x_offset = header.y_offset = header.z_offset = 0.0
las = laspy.LasData(header)
las.x = points[:, 1] # x coordinates
las.y = points[:, 2] # y coordinates
las.z = points[:, 3] # z coordinates
las.red = points[:, 4] # red color
las.green = points[:, 5] # green color
las.blue = points[:, 6] # blue color
# Add 'point_id', 'error', and 'building_id' as extra dimensions
las.add_extra_dim(laspy.ExtraBytesParams(name="point_id", type="int32"))
las.add_extra_dim(laspy.ExtraBytesParams(name="error", type="float32"))
las.add_extra_dim(laspy.ExtraBytesParams(name="building_id", type="int32"))
# Assign values to extra dimensions
las['point_id'] = points[:, 0]
las['error'] = points[:, 7]
las['building_id'] = points[:, 8]
# Debug: Verify the values of 'point_id', 'error', and 'building_id'
print("Point IDs:", las['point_id'][:10]) # Print first 10 values for verification
print("Error values:", las['error'][:10]) # Print first 10 values for verification
print("Building IDs:", las['building_id'][:10]) # Print first 10 values for verification
las.write(output_path)
print(f'LAS file created at {output_path}')
# Replace with the paths to your COLMAP points3D.txt file and output LAS file
input_txt_path = 'frames_8dir_turn/sparse/points3D_modified.txt'
output_las_path = 'frames_8dir_turn/sparse/points3D_modified.las'
points3D = read_points3D(input_txt_path)
write_las(output_las_path, points3D)
No matter how I modify the assignment, I can't get the custom attributes to show up in the LAS file when I analyze it with pdal
, and of course, can't view these properties in potree.
I've also tried assignment with setattr()
, like this:
# Define extra dimensions with setattr
extra_dimensions = {
'point_id': points[:, 0],
'error': points[:, 7],
'building_id': points[:, 8]
}
for name, data in extra_dimensions.items():
# Add each extra dimension as a new attribute to `las`
las.add_extra_dim(laspy.ExtraBytesParams(name=name, type="int32" if name != 'error' else "float32"))
setattr(las, name, data)
But it still doesn't work. One workaround I've tried using is assigning the data into other existing fields of the laspy format, and it works somewhat, but potree has expected usage of these fields which is really annoying to use, and I know it should support custom attributes (which is why I use it in the first place).
For reference, pdal info points3d_modifies.las --all
returns:
{
"boundary":
{
"area": 28561.51668,
"avg_pt_per_sq_unit": 2.009703593,
"avg_pt_spacing": 0.5283232902,
"boundary": "POLYGON ((72.412391 -117.52643,105.56859 54.758215,6.1 112.18643,-126.52478 -2.67,72.412391 -117.52643))",
"boundary_json": { "type": "Polygon", "coordinates": [ [ [ 72.412390669999994, -117.52642985 ], [ 105.568586010000004, 54.75821492 ], [ 6.1, 112.186429849999996 ], [ -126.524781349999998, -2.67 ], [ 72.412390669999994, -117.52642985 ] ] ] },
"density": 3.582617868,
"edge_length": 0,
"estimated_edge": 114.8564298,
"hex_offsets": "MULTIPOINT (0 0, -33.1562 57.4282, 0 114.856, 66.3124 114.856, 99.4686 57.4282, 66.3124 0)",
"sample_size": 5000,
"threshold": 15
},
"file_size": 4707807,
"filename": "points3D_modified.las",
"metadata":
{
"comp_spatialreference": "",
"compressed": false,
"copc": false,
"count": 102325,
"creation_doy": 313,
"creation_year": 2024,
"dataformat_id": 3,
"dataoffset": 857,
"filesource_id": 0,
"global_encoding": 0,
"global_encoding_base64": "AAA=",
"header_size": 227,
"major_version": 1,
"maxx": 129.58,
"maxy": 9.13,
"maxz": 152.34,
"minor_version": 2,
"minx": -138.28,
"miny": -41.56,
"minz": -148.58,
"offset_x": 0,
"offset_y": 0,
"offset_z": 0,
"point_length": 46,
"project_id": "00000000-0000-0000-0000-000000000000",
"scale_x": 0.01,
"scale_y": 0.01,
"scale_z": 0.01,
"software_id": "laspy 2.5.4",
"spatialreference": "",
"srs":
{
"compoundwkt": "",
"horizontal": "",
"isgeocentric": false,
"isgeographic": false,
"json":
{
},
"prettycompoundwkt": "",
"prettywkt": "",
"proj4": "",
"units":
{
"horizontal": "unknown",
"vertical": ""
},
"vertical": "",
"wkt": ""
},
"system_id": "OTHER",
"vlr_0":
{
"data": "AAAGAHBvaW50X2lkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAGVycm9yAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAGJ1aWxkaW5nX2lkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"description": "Extra Bytes Record",
"record_id": 4,
"user_id": "LASF_Spec"
}
},
"now": "2024-11-08T18:02:34+0900",
"pdal_version": "2.8.1 (git-version: Release)",
"reader": "readers.las",
"schema":
{
"dimensions":
[
{
"name": "X",
"size": 8,
"type": "floating"
},
{
"name": "Y",
"size": 8,
"type": "floating"
},
{
"name": "Z",
"size": 8,
"type": "floating"
},
{
"name": "Intensity",
"size": 2,
"type": "unsigned"
},
{
"name": "ReturnNumber",
"size": 1,
"type": "unsigned"
},
{
"name": "NumberOfReturns",
"size": 1,
"type": "unsigned"
},
{
"name": "ScanDirectionFlag",
"size": 1,
"type": "unsigned"
},
{
"name": "EdgeOfFlightLine",
"size": 1,
"type": "unsigned"
},
{
"name": "Classification",
"size": 1,
"type": "unsigned"
},
{
"name": "Synthetic",
"size": 1,
"type": "unsigned"
},
{
"name": "KeyPoint",
"size": 1,
"type": "unsigned"
},
{
"name": "Withheld",
"size": 1,
"type": "unsigned"
},
{
"name": "Overlap",
"size": 1,
"type": "unsigned"
},
{
"name": "ScanAngleRank",
"size": 4,
"type": "floating"
},
{
"name": "UserData",
"size": 1,
"type": "unsigned"
},
{
"name": "PointSourceId",
"size": 2,
"type": "unsigned"
},
{
"name": "GpsTime",
"size": 8,
"type": "floating"
},
{
"name": "Red",
"size": 2,
"type": "unsigned"
},
{
"name": "Green",
"size": 2,
"type": "unsigned"
},
{
"name": "Blue",
"size": 2,
"type": "unsigned"
}
]
},
"stac":
{
"message": "Failed to create STAC Feature with missing key. 'EPSG:4326'",
"status": "error"
},
"stats":
{
"bbox":
{
"native":
{
"bbox":
{
"maxx": 129.58,
"maxy": 9.13,
"maxz": 152.34,
"minx": -138.28,
"miny": -41.56,
"minz": -148.58
},
"boundary": { "type": "Polygon", "coordinates": [ [ [ -138.28, -41.56, -148.58 ], [ -138.28, 9.13, -148.58 ], [ 129.58, 9.13, 152.34 ], [ 129.58, -41.56, 152.34 ], [ -138.28, -41.56, -148.58 ] ] ] }
}
},
"statistic":
[
{
"average": 0.9007860249,
"count": 102325,
"maximum": 129.58,
"minimum": -138.28,
"name": "X",
"position": 0,
"stddev": 8.111961245,
"variance": 65.80391524
},
{
"average": -1.400667677,
"count": 102325,
"maximum": 9.13,
"minimum": -41.56,
"name": "Y",
"position": 1,
"stddev": 2.239800826,
"variance": 5.016707742
},
{
"average": 0.6305325189,
"count": 102325,
"maximum": 152.34,
"minimum": -148.58,
"name": "Z",
"position": 2,
"stddev": 8.202147334,
"variance": 67.27522088
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "Intensity",
"position": 3,
"stddev": 0,
"variance": 0
},
{
"average": 1,
"count": 102325,
"maximum": 1,
"minimum": 1,
"name": "ReturnNumber",
"position": 4,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "NumberOfReturns",
"position": 5,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "ScanDirectionFlag",
"position": 6,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "EdgeOfFlightLine",
"position": 7,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "Classification",
"position": 8,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "ScanAngleRank",
"position": 9,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "UserData",
"position": 10,
"stddev": 0,
"variance": 0
},
{
"average": 48818.02142,
"count": 102325,
"maximum": 65535,
"minimum": 1,
"name": "PointSourceId",
"position": 11,
"stddev": 21073.40222,
"variance": 444088281.2
},
{
"average": 87.2854923,
"count": 102325,
"maximum": 255,
"minimum": 0,
"name": "Red",
"position": 12,
"stddev": 61.9076842,
"variance": 3832.561363
},
{
"average": 95.50267286,
"count": 102325,
"maximum": 255,
"minimum": 0,
"name": "Green",
"position": 13,
"stddev": 61.30552525,
"variance": 3758.367426
},
{
"average": 89.79526997,
"count": 102325,
"maximum": 255,
"minimum": 0,
"name": "Blue",
"position": 14,
"stddev": 62.65696759,
"variance": 3925.895588
},
{
"average": 0.5833813223,
"count": 102325,
"maximum": 3.87784185,
"minimum": 4.444691769e-05,
"name": "GpsTime",
"position": 15,
"stddev": 0.5300718622,
"variance": 0.2809761791
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "Synthetic",
"position": 16,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "KeyPoint",
"position": 17,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "Withheld",
"position": 18,
"stddev": 0,
"variance": 0
},
{
"average": 0,
"count": 102325,
"maximum": 0,
"minimum": 0,
"name": "Overlap",
"position": 19,
"stddev": 0,
"variance": 0
}
]
}
}
I figured it out, using add_extra_dim()
was modified in the past and only works well with las.version==(1,4)
, so:
header = laspy.LasHeader(point_format=3, version="1.4")