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 .LiMMP extension.
  • 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

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

Terrain workflow script in the Script Editor

Check and Run the Script

  1. Select Check and resolve any reported syntax problem.
  2. Save the script.
  3. Select Run, or press F5.
  4. 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.
  5. 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

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

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

Generated DEM and DSM

Display the contour layer and verify that the lines follow the terrain surface.

Generated contour result

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 .LiMMP extension.
  • 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>_result folder. Review existing outputs before running the workflow again.

results matching ""

    No results matching ""