pythonpoint-cloudspdallaspy

Can't add custom attributes to LAS file


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
      }
    ]
  }
}

Solution

  • 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")