ptcfextract computes per-point local geometry features from point clouds stored in LAS/LAZ, useful for feature extraction research pipelines:
- Linearity
- Planarity
- Scattering
It can be used as:
- a library (feature extraction only, integrate with your own pipeline)
- a ready-to-run pipeline (downsample/outlier removal + visualization + two outputs)
- a CLI tool (
ptcfextract)
For each point, neighbors within a radius are used to compute a covariance matrix and eigenvalues. The outputs are:
linearity = (e2 - e1) / e2planarity = (e1 - e0) / e2scattering = e0 / e2
Robust handling:
- If a point has too few neighbors → outputs are
NaN - If covariance is degenerate or division by zero would occur → outputs are
NaN - Small negative eigenvalues due to numeric noise are clipped to 0
If your output base is out.laz, the tool writes:
out_rgbLPS_overwriteRGB.laz
- RGB is overwritten with normalized
[L,P,S]- R = Linearity
- G = Planarity
- B = Scattering
- All other original point dimensions are preserved
This is for instant visualization in CloudCompare / other viewers.
out_addLPS_extraFields_keepOriginalRGB.laz
- Original RGB remains unchanged
- Adds three ExtraBytes scalar fields (float32):
linearityplanarityscattering
- All other original point dimensions are preserved
This is for analysis workflows where you want to keep original appearance.
Note: If the input has no RGB fields and you request Output 1, ptcfextract upgrades point format to write RGB and prints a warning (some vendor-specific extra dims may not be preserved).
pip install ptcfextractpip install "ptcfextract[laz]"ptcfextract -i input.laz -o out.lazptcfextract --helpptcfextract -i input.laz -o out.laz --radius 0.5 --min-neighbors 15ptcfextract -i input.laz -o out.laz --downsample --voxel 0.1ptcfextract -i input.laz -o out.laz --outlier noneptcfextract -i input.laz -o out.laz --outlier sor --sor-nn 20 --sor-std 2.0ptcfextract -i input.laz -o out.laz --outlier ror --ror-np 6 --ror-r 0.5ptcfextract -i input.laz -o out.laz --no-vizptcfextract -i input.laz -o out.laz --viz linearity
ptcfextract -i input.laz -o out.laz --viz planarity
ptcfextract -i input.laz -o out.laz --viz scatteringptcfextract -i input.laz -o out.laz --viz rgbptcfextract -i input.laz -o out.laz --no-overwrite-rgbptcfextract -i input.laz -o out.laz --no-add-dimsUse this if you want your own:
- downsampling strategy (e.g., random, uniform, octree)
- visualization (CloudCompare, PyVista, Open3D custom)
- ML pipeline / feature engineering
import laspy
import numpy as np
from ptcfextract import compute_lps_features
las = laspy.read("input.laz")
xyz = np.vstack([las.x, las.y, las.z]).T
features = compute_lps_features(xyz, radius=0.5, min_neighbors=10)
linearity = features[:, 0]
planarity = features[:, 1]
scattering = features[:, 2]import laspy
import numpy as np
from ptcfextract import voxel_downsample_indices, compute_lps_features
las = laspy.read("input.laz")
xyz = np.vstack([las.x, las.y, las.z]).T
idx = voxel_downsample_indices(xyz, voxel_size=0.1)
xyz_ds = xyz[idx]
features_ds = compute_lps_features(xyz_ds, radius=0.5, min_neighbors=10)import laspy
import numpy as np
from ptcfextract import outlier_removal_indices, compute_lps_features
las = laspy.read("input.laz")
xyz = np.vstack([las.x, las.y, las.z]).T
inliers = outlier_removal_indices(
xyz,
mode="sor",
sor_nb_neighbors=20,
sor_std_ratio=2.0,
ror_nb_points=6,
ror_radius=0.5,
)
xyz_in = xyz[inliers]
features_in = compute_lps_features(xyz_in, radius=0.5, min_neighbors=10)from ptcfextract import features_to_rgb
rgb01 = features_to_rgb(features_in) # Nx3 float in [0,1]from ptcfextract import scalar_to_colormap_rgb
rgb_L = scalar_to_colormap_rgb(features_in[:, 0]) # linearity colormap
rgb_P = scalar_to_colormap_rgb(features_in[:, 1]) # planarity colormap
rgb_S = scalar_to_colormap_rgb(features_in[:, 2]) # scattering colormapfrom ptcfextract import process_pointcloud
process_pointcloud(
input_path="input.laz",
output_base="out.laz",
radius=0.5,
min_neighbors=10,
use_downsample=True,
voxel_size=0.1,
outlier_mode="sor",
sor_nb_neighbors=20,
sor_std_ratio=2.0,
visualize=True,
visualize_mode="rgb",
export_rgb_lps_overwrite=True,
export_add_dims_keep_original_rgb=True,
)If you computed features yourself and want to export using the same preservation logic:
import laspy
import numpy as np
from ptcfextract import write_las_preserve_all, compute_lps_features, features_to_rgb
base = laspy.read("input.laz")
xyz = np.vstack([base.x, base.y, base.z]).T
features = compute_lps_features(xyz, radius=0.5, min_neighbors=10)
rgb01 = features_to_rgb(features)
orig_idx = np.arange(xyz.shape[0], dtype=np.int64)
# Output 1: overwrite RGB with LPS
write_las_preserve_all(
out_path="out_rgbLPS_overwriteRGB.laz",
base_las=base,
kept_orig_indices=orig_idx,
rgb01=rgb01,
add_lps_dims=False,
)
# Output 2: keep original RGB, add extra dims
write_las_preserve_all(
out_path="out_addLPS_extraFields_keepOriginalRGB.laz",
base_las=base,
kept_orig_indices=orig_idx,
rgb01=None,
add_lps_dims=True,
lps_features=features,
)radius,voxel, andror-rare in the same units as the LAS coordinates.- If your cloud is in feet, typical values often need to be larger than meter-based defaults.
You’ll see a NaN count printed (pipeline mode). If it’s high:
- Increase
--radius - Decrease
--min-neighbors - Downsample less aggressively (smaller
--voxel) - If the cloud is sparse, a small radius may not find enough neighbors
- Enable downsampling (
--downsample --voxel 0.1or larger) - Disable visualization (
--no-viz) - Consider outlier removal only after downsampling
-
Output 2 still works (extra dims added, original dims preserved).
-
Output 1 requires RGB, so ptcfextract upgrades point format to write RGB and prints a warning.
- This can drop some unusual vendor-specific dimensions depending on the LAS point format and library limitations.
-
Output 1: visualize using RGB.
-
Output 2: visualize using scalar fields:
linearity,planarity,scattering- In CloudCompare: open the scalar field dropdown to choose which to view.
If you use ptcfextract in academic work, cite the Zenodo DOI associated with the GitHub release.
"# ptcfextract"