Terrain Output Workflow Implementation
This exercise uses the Script Editor to run a complete terrain-production workflow. The script reads the first point cloud already loaded in the current Mapping project, then performs subsampling, outlier removal, ground classification, DEM generation, DSM generation, and contour generation in sequence.
Before You Begin
- Open a Mapping project with the
.LiMMPextension. - Load at least one point cloud into the project. The script uses the first loaded point cloud and does not require a hard-coded data path.
- Confirm that the output location beside the project file is writable.
- Make sure the required point-cloud classification and Road Terrain functions are licensed and available.
- Save the project before running a long workflow.
The script creates a <project-name>_result folder beside the project file. It keeps the original point cloud, keeps the final classified point cloud, and removes intermediate point-cloud layers from the project after all six processing steps succeed. Files created on disk are not deleted.
Input point cloud loaded in the project
Understand the Complete Workflow
The six tools are not run as unrelated batch operations. Each point-cloud step passes its output to the next step. Subsampling feeds outlier removal, outlier removal feeds ground classification, and the final classified point cloud is then shared by DEM, DSM, and contour generation.
Current Mapping project
|
v
Read project information and check .LiMMP ----invalid----> Stop: wrong project type
|
v
Read project data and find first point cloud --missing----> Stop: load a point cloud
|
v
Create <project-name>_result output folder
|
v
PointCloud_Subsampling
|
v
PointCloud_Remove_Outliers
|
v
Classify_Classify_Ground_Points
|
+-------------------------+--------------------------+
| | |
v v v
Generate DEM Generate DSM Generate contours
Ground class 2 All classes 0-255 Ground class 2
| | |
v v v
Check DEM in project Check DSM in project Create or reuse contour layer
| |
Not loaded Not loaded
| |
v v
load_raster fallback load_raster fallback
| | |
+-------------------------+--------------------------+
|
v
Remove intermediate subsampling and SOR layers
|
v
Keep original cloud, final classified cloud, and terrain products
Any step raises an exception
|
v
Remove intermediate clouds already added to the project, keep the original cloud
and disk files, then report the error
This order first reduces the point count to control later processing cost, removes obvious outliers before classification, and makes all three terrain products use the same classified state instead of different intermediate results.
Add the Workflow Script
Create or open a Python Script Tool, open it in the Script Editor, and enter the following complete script.
"""Run the Standard LiDAR360MLS terrain workflow in the Python editor.
This standalone script reproduces the enabled six-stage processing sequence of
RoadTerrainWorkflowStartAction with the current gvscript Result V1 API. It uses
the first point cloud loaded in the active .LiMMP project and writes processing
outputs to the sibling <project-name>_result directory.
"""
import os
from pathlib import Path
from gvscript import mls
PRODUCT_NAME = "LiDAR360MLS Standard"
PROJECT_SUFFIX = ".limmp"
def path_key(path):
return os.path.normcase(os.path.normpath(str(path)))
def type_key(value):
return str(value).replace(" ", "").lower()
def project_point_clouds():
result = mls.project.list_loaded_data()
return [item for item in result.data.matchedData if type_key(item.type) == "pointcloud"]
def first_project_point_cloud():
point_clouds = project_point_clouds()
if not point_clouds:
raise RuntimeError("No point cloud is loaded in the current project.")
path = str(point_clouds[0].path)
if not Path(path).is_file():
raise FileNotFoundError("Input point cloud does not exist: {}".format(path))
return point_clouds[0], path
def project_output_dir():
result = mls.project.get_project_info()
if not result.data.isOpen or not result.data.projectPath:
raise RuntimeError("No mapping project is currently open.")
project_path = Path(result.data.projectPath)
if project_path.suffix.lower() != PROJECT_SUFFIX:
raise RuntimeError(
"{} requires a {} project, current project: {}".format(
PRODUCT_NAME, PROJECT_SUFFIX, project_path
)
)
output_dir = project_path.parent / (project_path.stem + "_result")
output_dir.mkdir(parents=True, exist_ok=True)
return str(output_dir).replace("\\", "/")
def point_cloud_outputs(step_name, result, fallback=None):
outputs = list(result.data.outputs.get("point_clouds") or [])
if outputs:
print("{} outputs: {}".format(step_name, outputs))
return outputs
if fallback is not None:
print("{} updated input point cloud in place: {}".format(step_name, fallback))
return list(fallback)
raise RuntimeError("{} returned no point-cloud output.".format(step_name))
def raster_outputs(step_name, result):
outputs = list(result.data.outputs.get("rasters") or [])
if not outputs:
raise RuntimeError("{} returned no raster output.".format(step_name))
print("{} outputs: {}".format(step_name, outputs))
return outputs
def loaded_paths(data_type):
result = mls.project.list_loaded_data()
return {
path_key(item.path)
for item in result.data.matchedData
if item.path and type_key(item.type) == type_key(data_type)
}
def ensure_rasters_loaded(paths):
loaded = loaded_paths("raster")
for path in paths:
if path_key(path) not in loaded:
mls.io.load_raster(path=path)
loaded.add(path_key(path))
def remove_from_project(paths):
loaded = {
path_key(item.path): item.id
for item in mls.project.list_loaded_data().data.matchedData
if item.path
}
for path in paths:
item_id = loaded.get(path_key(path))
if not item_id:
continue
result = mls.layer.io_remove_layer(id=item_id, raise_on_error=False)
if not result.ok:
print("Warning: unable to remove project item {}: {}".format(item_id, result.message))
def finish_point_cloud_outputs(generated_paths, final_paths):
final_keys = {path_key(path) for path in final_paths}
remove_paths = [path for path in generated_paths if path_key(path) not in final_keys]
remove_from_project(dict.fromkeys(remove_paths))
loaded = loaded_paths("pointcloud")
missing = [path for path in final_paths if path_key(path) not in loaded]
if missing:
mls.io.load_point_cloud(paths=missing)
def run_road_terrain_workflow():
input_item, input_path = first_project_point_cloud()
output_dir = project_output_dir()
generated_point_clouds = []
print("Product: {}".format(PRODUCT_NAME))
print("Input point cloud: {}".format(input_path))
print("Output directory: {}".format(output_dir))
try:
subsampling = mls.point_cloud.pointcloud_subsampling_parameters()
subsampling_result = mls.point_cloud.pointcloud_subsampling(
input_paths=[input_path], output_path=output_dir, Parameters=subsampling
)
current = point_cloud_outputs("PointCloud_Subsampling", subsampling_result)
generated_point_clouds.extend(current)
remove_outliers = mls.point_cloud.pointcloud_remove_outliers_parameters()
remove_outliers_result = mls.point_cloud.pointcloud_remove_outliers(
input_paths=current, output_path=output_dir, Parameters=remove_outliers
)
current = point_cloud_outputs("PointCloud_Remove_Outliers", remove_outliers_result)
generated_point_clouds.extend(current)
classify_ground = mls.classify.classify_classify_ground_points_parameters()
classify_result = mls.classify.classify_classify_ground_points(
input_paths=current, Parameters=classify_ground
)
current = point_cloud_outputs(
"Classify_Classify_Ground_Points", classify_result, fallback=current
)
generated_point_clouds.extend(current)
final_point_clouds = list(current)
dem = mls.road_terrain.pointcloud_to_dem_parameters()
dem.outPutDir = output_dir
dem_result = mls.road_terrain.pointcloud_to_dem(
input_paths=final_point_clouds, Parameters=dem
)
dem_outputs = raster_outputs("RoadTerrain_PointCloud_To_DEM", dem_result)
dsm = mls.road_terrain.pointcloud_to_dsm_parameters()
dsm.outPutDir = output_dir
dsm_result = mls.road_terrain.pointcloud_to_dsm(
input_paths=final_point_clouds, Parameters=dsm
)
dsm_outputs = raster_outputs("RoadTerrain_PointCloud_To_DSM", dsm_result)
contour = mls.road_terrain.pointcloud_to_contour_parameters()
contour_result = mls.road_terrain.pointcloud_to_contour(
input_paths=final_point_clouds, Parameters=contour
)
contour_outputs = list(contour_result.outputs)
print("RoadTerrain_PointCloud_To_Contour outputs: {}".format(contour_outputs))
finish_point_cloud_outputs(generated_point_clouds, final_point_clouds)
ensure_rasters_loaded(dem_outputs)
ensure_rasters_loaded(dsm_outputs)
summary = {
"product": PRODUCT_NAME,
"input_id": input_item.id,
"input_point_cloud": input_path,
"output_directory": output_dir,
"final_point_clouds": final_point_clouds,
"dem": dem_outputs,
"dsm": dsm_outputs,
"contour": contour_outputs,
}
print("Terrain workflow finished: {}".format(summary))
return summary
except Exception:
remove_from_project(dict.fromkeys(generated_point_clouds))
raise
WORKFLOW_RESULT = run_road_terrain_workflow()
Core Function Responsibilities
| Function | Responsibility | Description |
|---|---|---|
path_key() |
Normalizes path case and separators | Prevents the same file from being loaded twice because two path spellings compare differently. |
type_key() |
Removes spaces and converts a type name to lowercase | Accepts type text such as Point Cloud and pointcloud. |
project_point_clouds() |
Queries the current project and filters point clouds | Uses live project state without scanning disk folders or remembering a path from an earlier run. |
first_project_point_cloud() |
Selects the first cloud and verifies its file | Fails before any algorithm starts when the input is missing. |
project_output_dir() |
Checks project state and suffix, then creates the result folder | Restricts the example to a Standard LiDAR360MLS Mapping project and keeps outputs beside the project. |
point_cloud_outputs() |
Reads point-cloud outputs from Result V1 | Passes results between steps and uses fallback when classification updates its input in place. |
raster_outputs() |
Reads raster outputs from Result V1 | Stops immediately when DEM or DSM returns no raster. |
loaded_paths() |
Reads loaded paths for one data type | Provides the current project snapshot used by loading and cleanup functions. |
ensure_rasters_loaded() |
Loads only missing rasters | Avoids duplicate project-tree entries when DEM or DSM was already added automatically. |
remove_from_project() |
Resolves a layer ID by path and removes it from the project | Changes the project tree without deleting disk files; a cleanup failure is reported as a warning. |
finish_point_cloud_outputs() |
Removes non-final clouds and ensures the final cloud is loaded | Keeps the original and final classified clouds while removing subsampling and filtering layers. |
run_road_terrain_workflow() |
Orchestrates validation, six tool calls, and cleanup | Enforces one processing order and provides common cleanup when an exception occurs. |
generated_point_clouds records point clouds created during this run. current always holds the cloud passed to the next step, and final_point_clouds freezes the classified result. This prevents DEM, DSM, or contour generation from accidentally using an earlier subsampling or filtering result.
Processing Steps and Key Parameters
The script calls parameter factories to obtain the current defaults. To tune the workflow, change the parameter object after its factory call and before the corresponding tool call. Do not construct undeclared parameter dictionaries.
1. Point-cloud subsampling
pointcloud_subsampling_parameters() uses voxel sampling by default. This reduces the point count and controls the cost of outlier removal and classification.
| Parameter | Default | Meaning |
|---|---|---|
samplType |
0 |
Voxel sampling. Value 1 selects minimum-spacing sampling; value 2 selects rate sampling. |
VoxelSize |
0.5 |
Voxel size, used only when samplType=0. A larger value normally retains fewer points. |
PointsSpace |
1 |
Minimum point spacing, used only when samplType=1. |
Rate |
99.99 |
Sampling rate, used only when samplType=2. |
2. Outlier removal
pointcloud_remove_outliers_parameters() runs the SOR statistical outlier filter. It receives the subsampled output instead of reading the original cloud again.
| Parameter | Default | Meaning |
|---|---|---|
neighborPoints |
10 |
Number of neighboring points used for the distance statistic. |
mulStdDeviation |
5 |
Standard-deviation multiplier used to identify outliers. A smaller value is normally more restrictive. |
3. Ground classification
classify_classify_ground_points_parameters() uses TIN-based ground classification and writes detected ground points to class 2. Its result is shared by DEM, DSM, and contour generation.
| Parameter | Default | Meaning |
|---|---|---|
MaxBldSize |
20 |
Maximum building scale considered during classification. |
TerrAngle |
88 |
Terrain angle threshold. |
IterAngle |
8 |
Iteration angle threshold. |
IterDistance |
1.4 |
Iteration distance threshold. |
ToleranceAbove |
0.15 |
Tolerance above the terrain surface. |
ToleranceBelow |
0.15 |
Tolerance below the terrain surface. |
GridSize |
20 |
Classification grid size. |
GeomorType |
2 |
Current workflow default geomorphology type. |
toClass |
2 |
Writes detected ground points to class 2. |
The classification tool may create a new cloud or update the input in place. point_cloud_outputs(..., fallback=current) supports both result forms.
4. DEM generation
pointcloud_to_dem_parameters() uses only ground class 2 to produce a bare-earth elevation surface.
| Parameter | Default | Meaning |
|---|---|---|
fromClass |
[2] |
Uses only ground points. |
type |
1 |
Uses a point-cloud data source. |
resultType |
0 |
Produces a DEM. |
exportMode |
0 |
Exports by resolution. |
cellSize |
2 |
Output raster cell size. |
tileScale |
1000 |
Tile size. |
tileBuffer |
10 |
Tile buffer used to reduce edge effects. |
fillHole |
True |
Fills raster gaps that can be processed. |
asSingle |
False |
Does not force all tiles into one raster. |
5. DSM generation
pointcloud_to_dsm_parameters() uses all classes from 0 through 255 to produce a surface that includes object tops. It uses type=1 and resultType=1; its resolution, tiling, buffer, hole-filling, and merge defaults match the DEM step.
The DEM and DSM tools may add their rasters to the project automatically. ensure_rasters_loaded() calls mls.io.load_raster() only when a result is still missing.
6. Contour generation
pointcloud_to_contour_parameters() uses ground class 2 and creates or reuses the current project's contour layer.
| Parameter | Default | Meaning |
|---|---|---|
TerrainFromClass |
[2] |
Uses only ground points for the terrain. |
TriangleLength |
30 |
Allowed triangle edge length for terrain triangulation. |
minorSpacing |
2.5 |
Minor contour interval. |
basicSpacing |
5 |
Basic contour interval. |
majorSpacing |
25 |
Major contour interval. |
IsMeanSmooth |
True |
Enables mean smoothing. |
IsBezierSmooth |
True |
Enables Bezier smoothing. |
autoLabelContour |
True |
Adds contour labels automatically. |
labelInterval |
35 |
Label interval. |
labelAccuracy |
2 |
Numeric label precision. |
fileType |
0 |
Uses SHP output mode. |
contour25D |
False |
Does not generate 2.5D contours. |
These values are the current defaults used by this exercise, not fixed recommendations for every dataset. Tune sampling density, classification thresholds, raster resolution, and contour intervals for the point density, terrain relief, target map scale, and delivery requirements.
Terrain workflow script in the Script Editor
Check and Run the Script
- Select Check and resolve any reported syntax problem.
- Save the script.
- Select Run, or press
F5. - Wait until all six processing calls finish. Only one script can run at a time, and there is no Stop command in the Script Editor.
- Open the Output panel and confirm that the log contains output records for subsampling, outlier removal, ground classification, DEM, DSM, and contour generation.
Successful terrain workflow log
Verify the Results
After a successful run, inspect the project tree. It should retain the original point cloud, the final classified point cloud, a contour layer, and the generated DEM and DSM rasters. Subsampling and outlier-removal point-cloud layers are removed from the project tree, but their disk files remain in the result folder.
Final project data tree
Open the two raster results and compare their surface representation. The DEM is generated from ground class 2, while the DSM includes all point classes.
Generated DEM and DSM
Display the contour layer and verify that the lines follow the terrain surface.
Generated contour result
Troubleshooting
- No point cloud is loaded: Load a point cloud into the current project, then run the script again.
- The project suffix is rejected: Open a Standard LiDAR360MLS Mapping project with the
.LiMMPextension. - A processing call fails: Check the Output panel for the tool error code and message, then verify the license, plug-ins, input data, and available disk space.
- A raster is not visible: Refresh the project view. The script also loads a generated raster when the processing tool has not already added it.
- The result folder already exists: The script reuses the
<project-name>_resultfolder. Review existing outputs before running the workflow again.