Skip to content

Commit

Permalink
PLY translation added
Browse files Browse the repository at this point in the history
  • Loading branch information
Joffreybvn committed Dec 12, 2020
1 parent 4b0dad7 commit 0a75552
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 48 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Install and update using pip:
pip install lidario
```

Lidario depends on Rasterio, which depend on many other Python and C libraries. In case of problem, please refer to the [Rasterio installation instructions](https://rasterio.readthedocs.io/en/latest/installation.html).

### Quick start

**lidario.Translator** transform a given data structure (ie: *a raster*), to a point cloud (ie: *a numpy array*).
Expand All @@ -30,9 +32,12 @@ In this example, we initialize a **Translator** object to convert a *geotiff* fi
Then, we use this object to effectively convert a *tif* file.

### Going further
Transform Rasterio mask and GeoTiff files into numpy array, pandas dataframe, CSV, PLY, and many other format:

Read the [documentation on ReadTheDocs.io](https://lidario.readthedocs.io/).

## About the author
**Joffrey Bienvenu**, Machine Learning student @ [Becode](https://becode.org/).
- Website: https://joffreybvn.be
- Twitter: [@joffreybvn](https://twitter.com/Joffreybvn)
- Github: https://github.com/Joffreybvn
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
### Done ✓

- [x] **lidario.Translator**: .tiff/mask to csv/np/df/dict/list/tuple
- [x] **lidario.Translator**: .tiff/mask to ply
- [x] **lidario.Translator**: Return metadata if asked
- [x] **lidario.MetadataReader**: Return metadata from a tiff/mask
4 changes: 3 additions & 1 deletion docs/source/started/about.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ About the author
**Joffrey Bienvenu**, Machine Learning student @ Becode_.

- Website: https://joffreybvn.be
- Twitter: joffreybvn_
- Github: https://github.com/Joffreybvn

.. _Becode: https://becode.org/
.. _Becode: https://becode.org/
.. _joffreybvn: https://twitter.com/Joffreybvn
2 changes: 1 addition & 1 deletion docs/source/started/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Installation
Dependencies
------------

Lidario depends on Rasterio, which depend on many other Python and C dependencies. In case of problem, please refer to the `Rasterio installation instructions`_.
Lidario depends on Rasterio, which depend on many other Python and C libraries. In case of problem, please refer to the `Rasterio installation instructions`_.

.. _`Rasterio installation instructions`: https://rasterio.readthedocs.io/en/latest/installation.html

Expand Down
20 changes: 20 additions & 0 deletions docs/source/tutorials/translator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ Transform a raster (**.tif**) file into a CSV without applying the affine geo-tr
In this case, the point_cloud is None, because we save the values to a CSV file.

Translate to PLY file
---------------------

Transform a raster (**.tif**) file into a PLY (.ply) file.

.. code-block:: python
import lidario as lio
# Instantiate a Translator object which take a tif file
# and the point cloud to a PLY file.
translator = lio.Translator("geotiff", "ply")
# Translate the tif to a binary .ply file
translator.translate("/path/to/file.tif", out_format="binary")
# Translate the tif to a text .ply file (may be slow !)
translator.translate("/path/to/file.tif", out_format="ascii")
.. _lidario.Translator: ../api/translator.html
Expand Down
110 changes: 80 additions & 30 deletions lidario/io/output_handler.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@

import pandas as pd
import numpy as np
from plyfile import PlyData, PlyElement
from typing import Callable, Iterable


class OutputHandler:

def __init__(self, output_type):
self.saver = self.__create_saver(output_type)
self.saver: Callable = self.__create_saver(output_type)

def save(self, x, y, z, out_file, transpose):
def save(self, x, y, z, out_file, out_format, transpose):
"""
Execute the save function.
"""
return self.saver(x, y, z, out_file=out_file, transpose=transpose)
return self.saver(x, y, z, out_file=out_file, out_format=out_format,
transpose=transpose)

def __create_saver(self, output_type):
def __create_saver(self, output_type) -> Callable:
"""
Associate the right saver function to the "save" function of
this class. This function is executed during the initialization
Expand All @@ -28,7 +31,9 @@ def __create_saver(self, output_type):
"""

savers = {
# Files
"csv": self.__save_csv,
"ply": self.__save_ply,

# Pandas dataframe
"dataframe": self.__save_dataframe,
Expand All @@ -45,24 +50,66 @@ def __create_saver(self, output_type):
"dictionary": self.__save_dictionary,
"dict": self.__save_dictionary,

# Python data structures
# Other Python data structures
"list": self.__save_list,
"tuple": self.__save_tuple
}

return savers[output_type]

def __save_csv(self, x, y, z, transpose=False, out_file="output.csv"):
# File savings
# -------------------------------------------------------------------------

def __save_csv(self, x, y, z, transpose=False, out_file="output", **kwargs) -> None:
"""
Create a CSV file from a given x, y, z point cloud.
:return: None
"""

# Create a pandas dataframe and save it to CSV
out_file += ".csv"
self.__save_dataframe(x, y, z, transpose).to_csv(out_file, index=False)

def __save_dictionary(self, x, y, z, transpose=False, **kwargs):
def __save_ply(self, x, y, z, out_file="output", out_format="binary", **kwargs) -> None:
"""
Create a .ply file from a given x, y, z point cloud.
:return: None
"""

def to_tuples(array: np.array) -> Iterable:
"""Return an iterable of tuple frm a given (n, 3) np.array"""
shape = array.shape[0]

for i in range(shape):
yield tuple(array[i])

# Create a numpy array and set a custom dtype
raw_array = self.__save_numpy(x, y, z)

# Transform it with a custom dtype
np_array = np.fromiter(
to_tuples(raw_array),
dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4')]
)

# Instantiate a PlyElement with the numpy array
out_file += ".ply"
ply = PlyElement.describe(np_array, out_file)

# If out_type is "ascii", write a text file
if out_format == "ascii":
PlyData([ply], text=True).write(out_file)

# Else, write a binary file
else:
PlyData([ply]).write(out_file)

# Python data structure savings
# -------------------------------------------------------------------------

def __save_dictionary(self, x, y, z, transpose=False, **kwargs) -> dict:
"""
Create a dictionary of points, from a previously created
pandas dataframe. This function can be very slow.
Expand All @@ -74,27 +121,7 @@ def __save_dictionary(self, x, y, z, transpose=False, **kwargs):
# Return a dictionary of x, y and z
return self.__save_dataframe(x, y, z, transpose).to_dict()

def __save_dataframe(self, x, y, z, transpose=False, **kwargs):
"""
Create a (n, 3) pandas dataframe of the points. By default,
each point [x, y, z] is written on a new row. If transpose is
set to True, points are written on columns.
:return: A pandas dataframe of the point cloud.
:rtype: pd.dataframe
"""

# Get a numpy array of [x, y, z]
data = self.__save_numpy(x, y, z, transpose)

# If transpose, set each [x, y, z] point on columns (horizontally)
if transpose:
return pd.DataFrame(data=data, index=['x', 'y', 'z'])

# If not, set each [x, y, z] point on rows (vertically)
return pd.DataFrame(data=data, columns=['x', 'y', 'z'])

def __save_list(self, x, y, z, transpose=False, **kwargs):
def __save_list(self, x, y, z, transpose=False, **kwargs) -> list:
"""
Create a (n, 3) np.array and convert it into a pure Python list of
points [x, y, z]. If "transpose" is set to True, return a list of
Expand All @@ -107,7 +134,7 @@ def __save_list(self, x, y, z, transpose=False, **kwargs):
# Create a numpy array and transform it into a list
return self.__save_numpy(x, y, z, transpose).tolist()

def __save_tuple(self, x, y, z, transpose=False, **kwargs):
def __save_tuple(self, x, y, z, transpose=False, **kwargs) -> tuple:
"""
Create a (n, 3) np.array and convert it into a pure Python tuple of
points (x, y, z). If "transpose" is set to True, return a tuple of
Expand All @@ -121,8 +148,31 @@ def __save_tuple(self, x, y, z, transpose=False, **kwargs):
# https://www.geeksforgeeks.org/python-convert-list-of-lists-to-tuple-of-tuples/
return tuple(map(tuple, self.__save_numpy(x, y, z, transpose)))

# C-based data structure savings
# -------------------------------------------------------------------------

def __save_dataframe(self, x, y, z, transpose=False, **kwargs) -> pd.DataFrame:
"""
Create a (n, 3) pandas dataframe of the points. By default,
each point [x, y, z] is written on a new row. If transpose is
set to True, points are written on columns.
:return: A pandas dataframe of the point cloud.
:rtype: pd.DataFrame
"""

# Get a numpy array of [x, y, z]
data = self.__save_numpy(x, y, z, transpose)

# If transpose, set each [x, y, z] point on columns (horizontally)
if transpose:
return pd.DataFrame(data=data, index=['x', 'y', 'z'])

# If not, set each [x, y, z] point on rows (vertically)
return pd.DataFrame(data=data, columns=['x', 'y', 'z'])

@staticmethod
def __save_numpy(x, y, z, transpose=False, **kwargs):
def __save_numpy(x, y, z, transpose=False, **kwargs) -> np.array:
"""
Create a numpy array of shape (n, 3), filled with x, y and z.
If "transpose" is set to True, return a numpy array of shape (3, n).
Expand Down
19 changes: 13 additions & 6 deletions lidario/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Translator:
"**numpy**", "**pandas**", "**dictionary**", "**list**", "**tuple**".
- "csv": a CSV file.
- "ply": a .ply file.
- "numpy": a Numpy array. Alternatives: "np", "array".
- "dataframe": A Pandas dataframe: Alternatives: "pandas", "pd", "df".
- "dictionary": A pure Python dictionary: Alternative: "dict".
Expand Down Expand Up @@ -45,7 +46,8 @@ def __init__(self, input_type, output_type, affine_transform=True, metadata=Fals
self.affine_transform = affine_transform
self.return_metadata = metadata

def translate(self, input_values, out_file="output.csv", no_data=None, decimal=None, transpose=False, band=1):
def translate(self, input_values, out_file="output1.csv", out_format="binary",
no_data=None, decimal=None, transpose=False, band=1):
"""
Translate a given "input_values" into a X, Y, Z point cloud.
Expand All @@ -55,9 +57,13 @@ def translate(self, input_values, out_file="output.csv", no_data=None, decimal=N
- For a "**geotiff**": Takes the path to your .tif file (string).
- For a "**mask**": Takes the np.array returned by a rasterio.mask.mask() method.
:param out_file: Pathname of the CSV file to save the point cloud.
Used only if the Translator's "output_type" is "csv". Optional,
default: "output.csv".
:param out_file: Name of the file to save the point cloud.
Used only if the Translator's "output_type" is a file type: "csv", "ply".
Optional, default: "output.csv".
:param out_format: Data format to save the file: "**binary**" (default)
or "**ascii**" (not recommended, may be slow). Used only when "ply"
is specified as "output_type". Optional.
:param no_data: Value to exclude from the translation.
Expand All @@ -72,6 +78,7 @@ def translate(self, input_values, out_file="output.csv", no_data=None, decimal=N
:type input_values: str or np.array
:type out_file: str, optional
:param out_format: str, optional
:type no_data: int, optional
:type decimal: int, optional
:type transpose: bool, optional
Expand Down Expand Up @@ -101,9 +108,9 @@ def translate(self, input_values, out_file="output.csv", no_data=None, decimal=N
x, y, z = self.__round(x, y, z, decimal)

# Save the point cloud
point_cloud = self.output_handler.save(x, y, z, out_file, transpose)
point_cloud = self.output_handler.save(x, y, z, out_file, out_format, transpose)

# If "self.return_metadata" is True, return the metadata
# If self.return_metadata is True, return the metadata
if self.return_metadata:
return point_cloud, metadata

Expand Down
12 changes: 6 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@

if __name__ == '__main__':

"""
reader = lio.MetadataReader("tif")
metadata = reader.get_metadata("./tests/assets/1.tif")
print(metadata)

"""
shape = [{'type': 'Polygon', 'coordinates': [[(182545.32672299084, 162803.11793349683), (182540.70250971318, 162801.0220066402), (182539.92463073373, 162803.0260931747), (182537.93350985483, 162802.24429725204), (182534.55473017017, 162809.74636963103), (182541.08707224843, 162812.70320118777), (182545.32672299084, 162803.11793349683)]]}]
mask_values = rasterio.mask.mask(dtm, shapes=shape, all_touched=True, crop=True)
#shape = [{'type': 'Polygon', 'coordinates': [[(182545.32672299084, 162803.11793349683), (182540.70250971318, 162801.0220066402), (182539.92463073373, 162803.0260931747), (182537.93350985483, 162802.24429725204), (182534.55473017017, 162809.74636963103), (182541.08707224843, 162812.70320118777), (182545.32672299084, 162803.11793349683)]]}]
#mask_values = rasterio.mask.mask(dtm, shapes=shape, all_touched=True, crop=True)

translator = lio.Translator("mask", "df", metadata=True)
result = translator.translate(mask_values)
translator = lio.Translator("tif", "ply", metadata=False)
result = translator.translate("./tests/assets/1.tif", "points_ascii", "ascii")

print(result)
"""
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pandas
pytz
numpy
rasterio
rasterio
plyfile==0.7.2
9 changes: 6 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# This call to setup() does all the work
setup(
name="lidario",
version="0.2.1",
version="0.3.0",
description="High-level python library to manipulate LIDAR raster and point cloud",
long_description=long_description,
long_description_content_type="text/markdown",
Expand All @@ -25,7 +25,9 @@
author_email="[email protected]",
license="MIT",
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
Expand All @@ -36,9 +38,10 @@
"Programming Language :: Python :: 3.9",
"Operating System :: OS Independent",
"Topic :: Scientific/Engineering :: GIS",
"Topic :: Scientific/Engineering :: Image Processing"
"Topic :: Utilities"
],
packages=["lidario", "lidario.io"],
include_package_data=True,
install_requires=["pandas", "numpy", "rasterio"]
)
install_requires=["pandas", "numpy", "rasterio", "plyfile", "pytz"]
)

0 comments on commit 0a75552

Please sign in to comment.