The dataset used in this example notebook is from the Nakadake Sanroku Kiln Site Center in Japan. The data set is provided by Shinoto et al. under the CC-BY-4.0 license: DOI

Mapping segmentations to filter pipelines

When talking about adaptive ground point filtering in AFwizard we have two types of adaptivity in mind: Parameter adaptivity and spatial adaptivity. This notebook describes the details of how spatial adaptivity is implemented in AFwizard. It assumes that you have already created a suitable segmentation of your dataset into spatial features (e.g. in a GIS). We will then see how we can attach filter pipeline information to that segmentation file.

[1]:
import afwizard

We again work on our demonstrator dataset:

[2]:
ds = afwizard.DataSet(filename="nkd_pcl_epsg6670.laz", spatial_reference="EPSG:6670")

Next, we import the segmentation the GeoJSON file. It is assumed to contain a FeatureCollection in the sense of the GeoJSON standard where each features combines the geometric information of the segment (Polygon or Multipolygon) and a number of properties. One of these properties should contain your custom classification of the segments into classes.

[3]:
segmentation = afwizard.load_segmentation(
    "nkd_sgm_assigned_TF.geojson", spatial_reference="EPSG:6670"
)

As we are trying to map features to filter pipelines, we also need to load some filter pipelines. Here, we are directly opening these using load_filter. In practice, these should be selected from available filter libraries using e.g. the tools described in Working with filter libraries.

[4]:
pipelines = [
    afwizard.load_filter("nkd_fpl_paddy_LT.json"),
    afwizard.load_filter("nkd_fpl_slope_LT.json"),
    afwizard.load_filter("nkd_fpl_valley_TF.json"),
]
---------------------------------------------------------------------------
AFwizardError                             Traceback (most recent call last)
Cell In[4], line 2
      1 pipelines = [
----> 2     afwizard.load_filter("nkd_fpl_paddy_LT.json"),
      3     afwizard.load_filter("nkd_fpl_slope_LT.json"),
      4     afwizard.load_filter("nkd_fpl_valley_TF.json"),
      5 ]

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/filter.py:546, in load_filter(filename)
    543 filename = locate_filter(filename)
    545 with open(filename, "r") as f:
--> 546     return deserialize_filter(json.load(f))

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/filter.py:489, in deserialize_filter(data)
    487 data.pop("_major")
    488 data.pop("_minor")
--> 489 return type_._deserialize(data)

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/filter.py:251, in Filter._deserialize(cls, data)
    237 """Deserialize this filter.
    238
    239 Deserialize this objecte from a (nested) built-in data structure. This is
   (...)
    248     The deserialized filter instance
    249 """
    250 assert cls._identifier == data.pop("_backend")
--> 251 return cls(**data)

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/filter.py:371, in PipelineMixin.__init__(self, _variability, **kwargs)
    368         filters.append(f)
    369 kwargs["filters"] = filters
--> 371 self.config = kwargs
    372 self.variability = _variability

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/filter.py:79, in Filter.config(self, _config)
     76 # Validate the given configuration
     77 _config = pyrsistent.freeze(_config)
     78 jsonschema.validate(
---> 79     instance=pyrsistent.thaw(_config), schema=pyrsistent.thaw(self.schema())
     80 )
     82 # Store the validated config
     83 self._config = _config

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/filter.py:401, in PipelineMixin.schema(cls)
    399 @classmethod
    400 def schema(cls):
--> 401     return cls._schema_impl("schema")

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/filter.py:383, in PipelineMixin._schema_impl(cls, method)
    381 for ident, class_ in Filter._filter_impls.items():
    382     if Filter._filter_is_backend[ident]:
--> 383         if class_.enabled():
    384             bschema = getattr(class_, method)()
    385             backend_schemas.append(pyrsistent.thaw(bschema))

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/lastools.py:139, in LASToolsFilter.enabled(cls)
    137 @classmethod
    138 def enabled(cls):
--> 139     return lastools_is_present()

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/lastools.py:69, in lastools_is_present()
     66         return False
     68 # We return True iff a prefix was set
---> 69 return get_lastools_directory() is not None

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/lastools.py:55, in get_lastools_directory()
     53     dir = os.environ.get("LASTOOLS_DIR", None)
     54     if dir is not None:
---> 55         set_lastools_directory(dir)
     57 return _lastools_directory

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/lastools.py:44, in set_lastools_directory(dir)
     42 except AFwizardError as e:
     43     _lastools_directory = None
---> 44     raise e

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/lastools.py:41, in set_lastools_directory(dir)
     38 if dir is not None:
     39     try:
     40         # If this throws, we show a meaningful error where we looked for LASTools
---> 41         lasground_executable(base=dir)
     42     except AFwizardError as e:
     43         _lastools_directory = None

File ~/checkouts/readthedocs.org/user_builds/afwizard/conda/stable/lib/python3.11/site-packages/afwizard/lastools.py:84, in lasground_executable(base)
     82 fullpath = os.path.join(base, "bin", execname)
     83 if not os.path.exists(fullpath):
---> 84     raise AFwizardError(f"Executable {fullpath} was not found!")
     86 return fullpath

AFwizardError: Executable /home/docs/checkouts/readthedocs.org/user_builds/afwizard/checkouts/latest/LAStools/bin/lasground_new64.exe was not found!

The core task of assigning filter piplines is done by the assign_pipeline function which allows us to interactively set filter pipelines. On the right hand side, we can choose which property of our GeoJSON file contains the classification information. A feature can be highlighted on the map by clicking the button. For each class of segments, a pipeline can be selected from the dropdown menu:

[5]:
assigned_segmentation = afwizard.assign_pipeline(
    ds, segmentation=segmentation, pipelines=pipelines
)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[5], line 2
      1 assigned_segmentation = afwizard.assign_pipeline(
----> 2     ds, segmentation=segmentation, pipelines=pipelines
      3 )

NameError: name 'pipelines' is not defined

Once the pipelines are assigned, the returned segmentation object has a new property called “pipeline” that will direct the adaptivefiltering command line interface to the corresponding filter pipeline file. The modified file can be saved to disk by using the save method:

[6]:
assigned_segmentation.save("assigned_segmentation.geojson")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 assigned_segmentation.save("assigned_segmentation.geojson")

NameError: name 'assigned_segmentation' is not defined

It is worth noting that afwizard does not store the entire filter configuration in the GeoJSON object. This is done to allow further refinement of the used filter pipelines after the segmentation is created. Also, it does not store absolute or relative paths to your filter pipelines, because these could always change when moving to new hardware or when reorganizing your project. Instead it stores the metadata of your filter (in hashed form) and compares it against the metadata of the filter pipelines in your currently loaded filter libraries. If metadata is ambiguous across the given filter libraries, an error will be thrown.

[ ]: