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.
[ ]: