ign-pdal-tools 1.13.0__tar.gz → 1.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {ign_pdal_tools-1.13.0/ign_pdal_tools.egg-info → ign_pdal_tools-1.15.0}/PKG-INFO +4 -1
  2. ign_pdal_tools-1.13.0/PKG-INFO → ign_pdal_tools-1.15.0/README.md +3 -9
  3. ign_pdal_tools-1.13.0/README.md → ign_pdal_tools-1.15.0/ign_pdal_tools.egg-info/PKG-INFO +12 -0
  4. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/ign_pdal_tools.egg-info/SOURCES.txt +5 -1
  5. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/_version.py +1 -1
  6. ign_pdal_tools-1.15.0/pdaltools/color.py +223 -0
  7. ign_pdal_tools-1.13.0/pdaltools/color.py → ign_pdal_tools-1.15.0/pdaltools/download_image.py +55 -227
  8. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_info.py +16 -0
  9. ign_pdal_tools-1.15.0/pdaltools/replace_area_in_pointcloud.py +212 -0
  10. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pyproject.toml +0 -1
  11. ign_pdal_tools-1.15.0/test/test_color.py +198 -0
  12. ign_pdal_tools-1.13.0/test/test_color.py → ign_pdal_tools-1.15.0/test/test_download_image.py +37 -119
  13. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_add_buffer.py +1 -1
  14. ign_pdal_tools-1.13.0/test/test_pdal_custom.py → ign_pdal_tools-1.15.0/test/test_pdal.py +3 -2
  15. ign_pdal_tools-1.15.0/test/test_replace_area_in_pointcloud.py +339 -0
  16. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/LICENSE.md +0 -0
  17. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/ign_pdal_tools.egg-info/dependency_links.txt +0 -0
  18. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/ign_pdal_tools.egg-info/top_level.txt +0 -0
  19. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/add_points_in_pointcloud.py +0 -0
  20. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/create_random_laz.py +0 -0
  21. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_add_buffer.py +0 -0
  22. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_clip.py +0 -0
  23. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_comparison.py +0 -0
  24. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_merge.py +0 -0
  25. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_remove_dimensions.py +0 -0
  26. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_rename_dimension.py +0 -0
  27. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/pcd_info.py +0 -0
  28. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/replace_attribute_in_las.py +0 -0
  29. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/standardize_format.py +0 -0
  30. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/unlock_file.py +0 -0
  31. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/setup.cfg +0 -0
  32. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_add_points_in_pointcloud.py +0 -0
  33. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_create_random_laz.py +0 -0
  34. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_clip.py +0 -0
  35. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_comparison.py +0 -0
  36. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_info.py +0 -0
  37. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_merge.py +0 -0
  38. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_remove_dimensions.py +0 -0
  39. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_rename_dimension.py +0 -0
  40. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_pcd_info.py +0 -0
  41. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_replace_attribute_in_las.py +0 -0
  42. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_standardize_format.py +0 -0
  43. {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_unlock.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ign-pdal-tools
3
- Version: 1.13.0
3
+ Version: 1.15.0
4
4
  Summary: Library for common LAS files manipulation with PDAL
5
5
  Author-email: Guillaume Liegard <guillaume.liegard@ign.fr>
6
6
  Description-Content-Type: text/markdown
@@ -68,6 +68,7 @@ pcd_infos.get_pointcloud_origin_from_tile_width(points, tile_width=1000)
68
68
  * [las_clip.py](pdaltools/las_clip.py): crop a LAS file using 2d bounding box
69
69
  * [las_merge.py](pdaltools/las_merge.py): merge a LAS file with its neighbors according to their filenames
70
70
  * [las_add_buffer.py](pdaltools/las_add_buffer.py): add points to a LAS file from a buffer (border) from its neighbors (using filenames to locate neighbors)
71
+ * [replace_area_in_pointcloud.py](pdaltools/replace_area_in_pointcloud.py): replace the points from a LAS file by the points of another LAS file inside areas defined in a vector file (geojson, shapefile)
71
72
 
72
73
  **WARNING**: In `las_merge.py` and `las_add_buffer.py`, filenames are used to get the LAS files extents
73
74
  and to find neighbors.
@@ -127,3 +128,5 @@ To generate a pip package and deploy it on pypi, use the [Makefile](Makefile) at
127
128
  To build a docker image with the library installed: `make docker-build`
128
129
 
129
130
  To test the docker image: `make docker-test`
131
+
132
+ To build a docker image with a custom version of PDAL: `make docker-build-custom-pdal` ; the custom version is defined in the Makefile (see Makefile for details)
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: ign-pdal-tools
3
- Version: 1.13.0
4
- Summary: Library for common LAS files manipulation with PDAL
5
- Author-email: Guillaume Liegard <guillaume.liegard@ign.fr>
6
- Description-Content-Type: text/markdown
7
- License-File: LICENSE.md
8
- Dynamic: license-file
9
-
10
1
  # ign-pdal-tools
11
2
 
12
3
  This repo contains various python tools based on [PDAL](https://pdal.io/) that are used to work on
@@ -68,6 +59,7 @@ pcd_infos.get_pointcloud_origin_from_tile_width(points, tile_width=1000)
68
59
  * [las_clip.py](pdaltools/las_clip.py): crop a LAS file using 2d bounding box
69
60
  * [las_merge.py](pdaltools/las_merge.py): merge a LAS file with its neighbors according to their filenames
70
61
  * [las_add_buffer.py](pdaltools/las_add_buffer.py): add points to a LAS file from a buffer (border) from its neighbors (using filenames to locate neighbors)
62
+ * [replace_area_in_pointcloud.py](pdaltools/replace_area_in_pointcloud.py): replace the points from a LAS file by the points of another LAS file inside areas defined in a vector file (geojson, shapefile)
71
63
 
72
64
  **WARNING**: In `las_merge.py` and `las_add_buffer.py`, filenames are used to get the LAS files extents
73
65
  and to find neighbors.
@@ -127,3 +119,5 @@ To generate a pip package and deploy it on pypi, use the [Makefile](Makefile) at
127
119
  To build a docker image with the library installed: `make docker-build`
128
120
 
129
121
  To test the docker image: `make docker-test`
122
+
123
+ To build a docker image with a custom version of PDAL: `make docker-build-custom-pdal` ; the custom version is defined in the Makefile (see Makefile for details)
@@ -1,3 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: ign-pdal-tools
3
+ Version: 1.15.0
4
+ Summary: Library for common LAS files manipulation with PDAL
5
+ Author-email: Guillaume Liegard <guillaume.liegard@ign.fr>
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE.md
8
+ Dynamic: license-file
9
+
1
10
  # ign-pdal-tools
2
11
 
3
12
  This repo contains various python tools based on [PDAL](https://pdal.io/) that are used to work on
@@ -59,6 +68,7 @@ pcd_infos.get_pointcloud_origin_from_tile_width(points, tile_width=1000)
59
68
  * [las_clip.py](pdaltools/las_clip.py): crop a LAS file using 2d bounding box
60
69
  * [las_merge.py](pdaltools/las_merge.py): merge a LAS file with its neighbors according to their filenames
61
70
  * [las_add_buffer.py](pdaltools/las_add_buffer.py): add points to a LAS file from a buffer (border) from its neighbors (using filenames to locate neighbors)
71
+ * [replace_area_in_pointcloud.py](pdaltools/replace_area_in_pointcloud.py): replace the points from a LAS file by the points of another LAS file inside areas defined in a vector file (geojson, shapefile)
62
72
 
63
73
  **WARNING**: In `las_merge.py` and `las_add_buffer.py`, filenames are used to get the LAS files extents
64
74
  and to find neighbors.
@@ -118,3 +128,5 @@ To generate a pip package and deploy it on pypi, use the [Makefile](Makefile) at
118
128
  To build a docker image with the library installed: `make docker-build`
119
129
 
120
130
  To test the docker image: `make docker-test`
131
+
132
+ To build a docker image with a custom version of PDAL: `make docker-build-custom-pdal` ; the custom version is defined in the Makefile (see Makefile for details)
@@ -9,6 +9,7 @@ pdaltools/_version.py
9
9
  pdaltools/add_points_in_pointcloud.py
10
10
  pdaltools/color.py
11
11
  pdaltools/create_random_laz.py
12
+ pdaltools/download_image.py
12
13
  pdaltools/las_add_buffer.py
13
14
  pdaltools/las_clip.py
14
15
  pdaltools/las_comparison.py
@@ -17,12 +18,14 @@ pdaltools/las_merge.py
17
18
  pdaltools/las_remove_dimensions.py
18
19
  pdaltools/las_rename_dimension.py
19
20
  pdaltools/pcd_info.py
21
+ pdaltools/replace_area_in_pointcloud.py
20
22
  pdaltools/replace_attribute_in_las.py
21
23
  pdaltools/standardize_format.py
22
24
  pdaltools/unlock_file.py
23
25
  test/test_add_points_in_pointcloud.py
24
26
  test/test_color.py
25
27
  test/test_create_random_laz.py
28
+ test/test_download_image.py
26
29
  test/test_las_add_buffer.py
27
30
  test/test_las_clip.py
28
31
  test/test_las_comparison.py
@@ -31,7 +34,8 @@ test/test_las_merge.py
31
34
  test/test_las_remove_dimensions.py
32
35
  test/test_las_rename_dimension.py
33
36
  test/test_pcd_info.py
34
- test/test_pdal_custom.py
37
+ test/test_pdal.py
38
+ test/test_replace_area_in_pointcloud.py
35
39
  test/test_replace_attribute_in_las.py
36
40
  test/test_standardize_format.py
37
41
  test/test_unlock.py
@@ -1,4 +1,4 @@
1
- __version__ = "1.13.0"
1
+ __version__ = "1.15.0"
2
2
 
3
3
 
4
4
  if __name__ == "__main__":
@@ -0,0 +1,223 @@
1
+ import argparse
2
+ import tempfile
3
+ from math import ceil, floor
4
+ from typing import Tuple
5
+
6
+ import pdal
7
+
8
+ import pdaltools.las_info as las_info
9
+ from pdaltools.download_image import download_image
10
+
11
+
12
+ def match_min_max_with_pixel_size(min_d: float, max_d: float, pixel_per_meter: float) -> Tuple[float, float]:
13
+ """Round min/max values along one dimension to the closest multiple of 1 / pixel_per_meter
14
+ It should prevent having to interpolate during a request to the geoplateforme
15
+ in case we use a native resolution.
16
+
17
+ Args:
18
+ min_d (float): minimum value along the dimension, in meters
19
+ max_d (float): maximum value along the dimension, in meters
20
+ pixel_per_meter (float): resolution (in number of pixels per meter)
21
+
22
+ Returns:
23
+ Tuple[float, float]: adapted min / max value
24
+ """
25
+ # Use ceil - 1 instead of ceil to make sure that
26
+ # no point of the pointcloud is on the limit of the first pixel
27
+ min_d = (ceil(min_d * pixel_per_meter) - 1) / pixel_per_meter
28
+ # Use floor + 1 instead of ceil to make sure that no point of the pointcloud is on the limit of the last pixel
29
+ max_d = (floor(max_d * pixel_per_meter) + 1) / pixel_per_meter
30
+
31
+ return min_d, max_d
32
+
33
+
34
+ def color(
35
+ input_file: str,
36
+ output_file: str,
37
+ proj="",
38
+ pixel_per_meter=5,
39
+ timeout_second=300,
40
+ color_rvb_enabled=True,
41
+ color_ir_enabled=True,
42
+ veget_index_file="",
43
+ vegetation_dim="Deviation",
44
+ check_images=False,
45
+ stream_RGB="ORTHOIMAGERY.ORTHOPHOTOS",
46
+ stream_IRC="ORTHOIMAGERY.ORTHOPHOTOS.IRC",
47
+ size_max_gpf=5000,
48
+ ):
49
+ """Colorize a las file with any of the following methods:
50
+ - R, G, B values from an image retrieved from ign geoplateform via the "stream_RGB" data feed name
51
+ - Infrared from another image retrieved from ign geoplateform via the "stream_IRC" data feed name
52
+ - any field "vegetation_dim" from another image stored locally at "veget_index_file"
53
+
54
+
55
+ Args:
56
+ input_file (str): Path to the las file to colorize
57
+ output_file (str): Path to the output colorized file
58
+ proj (str, optional): EPSG value of the SRS to apply to the output file (if not provided, use the one from
59
+ the input file). Eg. "2154" to use "EPSG:2154". Defaults to "".
60
+ pixel_per_meter (int, optional): Stream image resolution (for RGB and IRC) in pixels per meter. Defaults to 5.
61
+ timeout_second (int, optional): Timeout for the geoplateform request. Defaults to 300.
62
+ color_rvb_enabled (bool, optional): If true, apply R, G, B dimensions colorization. Defaults to True.
63
+ color_ir_enabled (bool, optional): If true, apply Infrared dimension colorization. Defaults to True.
64
+ veget_index_file (str, optional): Path to the tiff tile to use for "vegetation_dim" colorization.
65
+ Defaults to "".
66
+ vegetation_dim (str, optional): Name of the dimension to use to store the values of "veget_index_file".
67
+ Defaults to "Deviation".
68
+ check_images (bool, optional): If true, check if images from the geoplateform data feed are white
69
+ (and raise and error in this case). Defaults to False.
70
+ stream_RGB (str, optional): WMS raster stream for RGB colorization:
71
+ Default stream (ORTHOIMAGERY.ORTHOPHOTOS) let the server choose the resolution
72
+ for 20cm resolution rasters, use HR.ORTHOIMAGERY.ORTHOPHOTOS
73
+ for 50 cm resolution rasters, use ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO
74
+ Defaults to ORTHOIMAGERY.ORTHOPHOTOS
75
+ stream_IRC (str, optional):WMS raster stream for IRC colorization.
76
+ Documentation about possible stream : https://geoservices.ign.fr/services-web-experts-ortho.
77
+ Defaults to "ORTHOIMAGERY.ORTHOPHOTOS.IRC".
78
+ size_max_gpf (int, optional): Maximum edge size (in pixels) of downloaded images. If input file needs more,
79
+ several images are downloaded and merged. Defaults to 5000.
80
+
81
+ Returns:
82
+ Paths to the temporary files that store the streamed images (tmp_ortho, tmp_ortho_irc)
83
+ """
84
+ metadata = las_info.las_info_metadata(input_file)
85
+ minx, maxx, miny, maxy = las_info.get_bounds_from_header_info(metadata)
86
+
87
+ minx, maxx = match_min_max_with_pixel_size(minx, maxx, pixel_per_meter)
88
+ miny, maxy = match_min_max_with_pixel_size(miny, maxy, pixel_per_meter)
89
+
90
+ if proj == "":
91
+ proj = las_info.get_epsg_from_header_info(metadata)
92
+
93
+ pipeline = pdal.Reader.las(filename=input_file)
94
+
95
+ writer_extra_dims = "all"
96
+
97
+ if veget_index_file and veget_index_file != "":
98
+ print(f"Remplissage du champ {vegetation_dim} à partir du fichier {veget_index_file}")
99
+ pipeline |= pdal.Filter.colorization(raster=veget_index_file, dimensions=f"{vegetation_dim}:1:256.0")
100
+ writer_extra_dims = [f"{vegetation_dim}=ushort"]
101
+
102
+ tmp_ortho = None
103
+ if color_rvb_enabled:
104
+ tmp_ortho = tempfile.NamedTemporaryFile(suffix="_rvb.tif")
105
+ download_image(
106
+ proj,
107
+ stream_RGB,
108
+ minx,
109
+ miny,
110
+ maxx,
111
+ maxy,
112
+ pixel_per_meter,
113
+ tmp_ortho.name,
114
+ timeout_second,
115
+ check_images,
116
+ size_max_gpf,
117
+ )
118
+ # Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
119
+ # which turns it to a 0 to 255*256 range.
120
+ # It is kept this way because of other dependencies that have been tuned to fit this range
121
+ pipeline |= pdal.Filter.colorization(
122
+ raster=tmp_ortho.name, dimensions="Red:1:256.0, Green:2:256.0, Blue:3:256.0"
123
+ )
124
+
125
+ tmp_ortho_irc = None
126
+ if color_ir_enabled:
127
+ tmp_ortho_irc = tempfile.NamedTemporaryFile(suffix="_irc.tif")
128
+ download_image(
129
+ proj,
130
+ stream_IRC,
131
+ minx,
132
+ miny,
133
+ maxx,
134
+ maxy,
135
+ pixel_per_meter,
136
+ tmp_ortho_irc.name,
137
+ timeout_second,
138
+ check_images,
139
+ size_max_gpf,
140
+ )
141
+ # Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
142
+ # which turns it to a 0 to 255*256 range.
143
+ # It is kept this way because of other dependencies that have been tuned to fit this range
144
+ pipeline |= pdal.Filter.colorization(raster=tmp_ortho_irc.name, dimensions="Infrared:1:256.0")
145
+
146
+ pipeline |= pdal.Writer.las(
147
+ filename=output_file, extra_dims=writer_extra_dims, minor_version="4", dataformat_id="8", forward="all"
148
+ )
149
+
150
+ print("Traitement du nuage de point")
151
+ pipeline.execute()
152
+
153
+ # The orthoimages files will be deleted only when their reference are lost.
154
+ # To keep them, make a copy (with e.g. shutil.copy(...))
155
+ # See: https://docs.python.org/2/library/tempfile.html#tempfile.TemporaryFile
156
+ return tmp_ortho, tmp_ortho_irc
157
+
158
+
159
+ def parse_args():
160
+ parser = argparse.ArgumentParser("Colorize tool", formatter_class=argparse.RawTextHelpFormatter)
161
+ parser.add_argument("--input", "-i", type=str, required=True, help="Input file")
162
+ parser.add_argument("--output", "-o", type=str, default="", help="Output file")
163
+ parser.add_argument(
164
+ "--proj", "-p", type=str, default="", help="Projection, default will use projection from metadata input"
165
+ )
166
+ parser.add_argument("--resolution", "-r", type=float, default=5, help="Resolution, in pixel per meter")
167
+ parser.add_argument("--timeout", "-t", type=int, default=300, help="Timeout, in seconds")
168
+ parser.add_argument("--rvb", action="store_true", help="Colorize RVB")
169
+ parser.add_argument("--ir", action="store_true", help="Colorize IR")
170
+ parser.add_argument(
171
+ "--vegetation",
172
+ type=str,
173
+ default="",
174
+ help="Vegetation file (raster), value will be stored in 'vegetation_dim' field",
175
+ )
176
+ parser.add_argument(
177
+ "--vegetation_dim", type=str, default="Deviation", help="name of the extra_dim uses for the vegetation value"
178
+ )
179
+ parser.add_argument("--check-images", "-c", action="store_true", help="Check that downloaded image is not white")
180
+ parser.add_argument(
181
+ "--stream-RGB",
182
+ type=str,
183
+ default="ORTHOIMAGERY.ORTHOPHOTOS",
184
+ help="""WMS raster stream for RGB colorization:
185
+ default stream (ORTHOIMAGERY.ORTHOPHOTOS) let the server choose the resolution
186
+ for 20cm resolution rasters, use HR.ORTHOIMAGERY.ORTHOPHOTOS
187
+ for 50 cm resolution rasters, use ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO""",
188
+ )
189
+ parser.add_argument(
190
+ "--stream-IRC",
191
+ type=str,
192
+ default="ORTHOIMAGERY.ORTHOPHOTOS.IRC",
193
+ help="""WMS raster stream for IRC colorization. Default to ORTHOIMAGERY.ORTHOPHOTOS.IRC
194
+ Documentation about possible stream : https://geoservices.ign.fr/services-web-experts-ortho""",
195
+ )
196
+ parser.add_argument(
197
+ "--size-max-GPF",
198
+ type=int,
199
+ default=5000,
200
+ help="Maximum edge size (in pixels) of downloaded images."
201
+ " If input file needs more, several images are downloaded and merged.",
202
+ )
203
+
204
+ return parser.parse_args()
205
+
206
+
207
+ if __name__ == "__main__":
208
+ args = parse_args()
209
+ color(
210
+ input_file=args.input,
211
+ output_file=args.output,
212
+ proj=args.proj,
213
+ pixel_per_meter=args.resolution,
214
+ timeout_second=args.timeout,
215
+ color_rvb_enabled=args.rvb,
216
+ color_ir_enabled=args.ir,
217
+ veget_index_file=args.vegetation,
218
+ vegetation_dim=args.vegetation_dim,
219
+ check_images=args.check_images,
220
+ stream_RGB=args.stream_RGB,
221
+ stream_IRC=args.stream_IRC,
222
+ size_max_gpf=args.size_max_GPF,
223
+ )
@@ -1,59 +1,36 @@
1
- import argparse
1
+ """Tool to download an image from IGN geoplateform: https://geoservices.ign.fr/"""
2
+
2
3
  import tempfile
3
4
  import time
4
- from math import ceil, floor
5
+ from math import ceil
5
6
  from pathlib import Path
6
7
  from typing import Tuple
7
8
 
8
9
  import numpy as np
9
- import pdal
10
10
  import requests
11
11
  from osgeo import gdal, gdal_array
12
12
 
13
- import pdaltools.las_info as las_info
14
13
  from pdaltools.unlock_file import copy_and_hack_decorator
15
14
 
16
15
 
17
- def pretty_time_delta(seconds):
18
- sign_string = "-" if seconds < 0 else ""
19
- seconds = abs(int(seconds))
20
- days, seconds = divmod(seconds, 86400)
21
- hours, seconds = divmod(seconds, 3600)
22
- minutes, seconds = divmod(seconds, 60)
23
- if days > 0:
24
- return "%s%dd%dh%dm%ds" % (sign_string, days, hours, minutes, seconds)
25
- elif hours > 0:
26
- return "%s%dh%dm%ds" % (sign_string, hours, minutes, seconds)
27
- elif minutes > 0:
28
- return "%s%dm%ds" % (sign_string, minutes, seconds)
29
- else:
30
- return "%s%ds" % (sign_string, seconds)
31
-
32
-
33
- def retry(times, delay, factor, debug=False):
34
- def decorator(func):
35
- def newfn(*args, **kwargs):
36
- attempt = 1
37
- new_delay = delay
38
- while attempt <= times:
39
- need_retry = False
40
- try:
41
- return func(*args, **kwargs)
42
- except requests.exceptions.RequestException as err:
43
- print("Connection Error:", err)
44
- need_retry = True
45
- if need_retry:
46
- print(f"{attempt}/{times} Nouvel essai après une pause de {pretty_time_delta(new_delay)} .. ")
47
- if not debug:
48
- time.sleep(new_delay)
49
- new_delay = new_delay * factor
50
- attempt += 1
16
+ def compute_cells_size(mind: float, maxd: float, pixel_per_meter: float, size_max_gpf: int) -> Tuple[int, int, int]:
17
+ """Compute cell size to have cells of almost equal size, but phased the same way as
18
+ if there had been no paving by forcing cell_size (in pixels) to be an integer
51
19
 
52
- return func(*args, **kwargs)
20
+ Args:
21
+ mind (float): minimum value along the dimension, in meters
22
+ maxd (float): maximum value along the dimension, in meters
23
+ pixel_per_meter (float): resolution (in number of pixels per meter)
24
+ size_max_gpf (int): maximum image size in pixels
53
25
 
54
- return newfn
26
+ Returns:
27
+ Tuple[int, int, int]: number of pixels in total, number of cells along the dimension, cell size in pixels
28
+ """
29
+ nb_pixels = ceil((maxd - mind) * pixel_per_meter)
30
+ nb_cells = ceil(nb_pixels / size_max_gpf)
31
+ cell_size_pixels = ceil(nb_pixels / nb_cells) # Force cell size to be an integer
55
32
 
56
- return decorator
33
+ return nb_pixels, nb_cells, cell_size_pixels
57
34
 
58
35
 
59
36
  def is_image_white(filename: str):
@@ -101,24 +78,46 @@ def download_image_from_geoplateforme(
101
78
  raise ValueError(f"Downloaded image is white, with stream: {layer}")
102
79
 
103
80
 
104
- def compute_cells_size(mind: float, maxd: float, pixel_per_meter: float, size_max_gpf: int) -> Tuple[int, int, int]:
105
- """Compute cell size to have cells of almost equal size, but phased the same way as
106
- if there had been no paving by forcing cell_size (in pixels) to be an integer
81
+ def pretty_time_delta(seconds):
82
+ sign_string = "-" if seconds < 0 else ""
83
+ seconds = abs(int(seconds))
84
+ days, seconds = divmod(seconds, 86400)
85
+ hours, seconds = divmod(seconds, 3600)
86
+ minutes, seconds = divmod(seconds, 60)
87
+ if days > 0:
88
+ return "%s%dd%dh%dm%ds" % (sign_string, days, hours, minutes, seconds)
89
+ elif hours > 0:
90
+ return "%s%dh%dm%ds" % (sign_string, hours, minutes, seconds)
91
+ elif minutes > 0:
92
+ return "%s%dm%ds" % (sign_string, minutes, seconds)
93
+ else:
94
+ return "%s%ds" % (sign_string, seconds)
107
95
 
108
- Args:
109
- mind (float): minimum value along the dimension, in meters
110
- maxd (float): maximum value along the dimension, in meters
111
- pixel_per_meter (float): resolution (in number of pixels per meter)
112
- size_max_gpf (int): maximum image size in pixels
113
96
 
114
- Returns:
115
- Tuple[int, int, int]: number of pixels in total, number of cells along the dimension, cell size in pixels
116
- """
117
- nb_pixels = ceil((maxd - mind) * pixel_per_meter)
118
- nb_cells = ceil(nb_pixels / size_max_gpf)
119
- cell_size_pixels = ceil(nb_pixels / nb_cells) # Force cell size to be an integer
97
+ def retry(times, delay, factor, debug=False):
98
+ def decorator(func):
99
+ def newfn(*args, **kwargs):
100
+ attempt = 1
101
+ new_delay = delay
102
+ while attempt <= times:
103
+ need_retry = False
104
+ try:
105
+ return func(*args, **kwargs)
106
+ except requests.exceptions.RequestException as err:
107
+ print("Connection Error:", err)
108
+ need_retry = True
109
+ if need_retry:
110
+ print(f"{attempt}/{times} Nouvel essai après une pause de {pretty_time_delta(new_delay)} .. ")
111
+ if not debug:
112
+ time.sleep(new_delay)
113
+ new_delay = new_delay * factor
114
+ attempt += 1
120
115
 
121
- return nb_pixels, nb_cells, cell_size_pixels
116
+ return func(*args, **kwargs)
117
+
118
+ return newfn
119
+
120
+ return decorator
122
121
 
123
122
 
124
123
  @copy_and_hack_decorator
@@ -191,174 +190,3 @@ def download_image(proj, layer, minx, miny, maxx, maxy, pixel_per_meter, outfile
191
190
  gdal.Translate(outfile, tmp_vrt.name)
192
191
 
193
192
  return nb_cells_x * nb_cells_y
194
-
195
-
196
- def match_min_max_with_pixel_size(min_d: float, max_d: float, pixel_per_meter: float) -> Tuple[float, float]:
197
- """Round min/max values along one dimension to the closest multiple of 1 / pixel_per_meter
198
- It should prevent having to interpolate during a request to the geoplateforme
199
- in case we use a native resolution.
200
-
201
- Args:
202
- min_d (float): minimum value along the dimension, in meters
203
- max_d (float): maximum value along the dimension, in meters
204
- pixel_per_meter (float): resolution (in number of pixels per meter)
205
-
206
- Returns:
207
- Tuple[float, float]: adapted min / max value
208
- """
209
- # Use ceil - 1 instead of ceil to make sure that
210
- # no point of the pointcloud is on the limit of the first pixel
211
- min_d = (ceil(min_d * pixel_per_meter) - 1) / pixel_per_meter
212
- # Use floor + 1 instead of ceil to make sure that no point of the pointcloud is on the limit of the last pixel
213
- max_d = (floor(max_d * pixel_per_meter) + 1) / pixel_per_meter
214
-
215
- return min_d, max_d
216
-
217
-
218
- def color(
219
- input_file: str,
220
- output_file: str,
221
- proj="",
222
- pixel_per_meter=5,
223
- timeout_second=300,
224
- color_rvb_enabled=True,
225
- color_ir_enabled=True,
226
- veget_index_file="",
227
- check_images=False,
228
- stream_RGB="ORTHOIMAGERY.ORTHOPHOTOS",
229
- stream_IRC="ORTHOIMAGERY.ORTHOPHOTOS.IRC",
230
- size_max_gpf=5000,
231
- ):
232
- metadata = las_info.las_info_metadata(input_file)
233
- minx, maxx, miny, maxy = las_info.get_bounds_from_header_info(metadata)
234
-
235
- minx, maxx = match_min_max_with_pixel_size(minx, maxx, pixel_per_meter)
236
- miny, maxy = match_min_max_with_pixel_size(miny, maxy, pixel_per_meter)
237
-
238
- if proj == "":
239
- proj = las_info.get_epsg_from_header_info(metadata)
240
-
241
- pipeline = pdal.Reader.las(filename=input_file)
242
-
243
- writer_extra_dims = "all"
244
-
245
- if veget_index_file and veget_index_file != "":
246
- print(f"Remplissage du champ Deviation à partir du fichier {veget_index_file}")
247
- pipeline |= pdal.Filter.colorization(raster=veget_index_file, dimensions="Deviation:1:256.0")
248
- writer_extra_dims = ["Deviation=ushort"]
249
-
250
- tmp_ortho = None
251
- if color_rvb_enabled:
252
- tmp_ortho = tempfile.NamedTemporaryFile(suffix="_rvb.tif")
253
- download_image(
254
- proj,
255
- stream_RGB,
256
- minx,
257
- miny,
258
- maxx,
259
- maxy,
260
- pixel_per_meter,
261
- tmp_ortho.name,
262
- timeout_second,
263
- check_images,
264
- size_max_gpf,
265
- )
266
- # Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
267
- # which turns it to a 0 to 255*256 range.
268
- # It is kept this way because of other dependencies that have been tuned to fit this range
269
- pipeline |= pdal.Filter.colorization(
270
- raster=tmp_ortho.name, dimensions="Red:1:256.0, Green:2:256.0, Blue:3:256.0"
271
- )
272
-
273
- tmp_ortho_irc = None
274
- if color_ir_enabled:
275
- tmp_ortho_irc = tempfile.NamedTemporaryFile(suffix="_irc.tif")
276
- download_image(
277
- proj,
278
- stream_IRC,
279
- minx,
280
- miny,
281
- maxx,
282
- maxy,
283
- pixel_per_meter,
284
- tmp_ortho_irc.name,
285
- timeout_second,
286
- check_images,
287
- size_max_gpf,
288
- )
289
- # Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
290
- # which turns it to a 0 to 255*256 range.
291
- # It is kept this way because of other dependencies that have been tuned to fit this range
292
- pipeline |= pdal.Filter.colorization(raster=tmp_ortho_irc.name, dimensions="Infrared:1:256.0")
293
-
294
- pipeline |= pdal.Writer.las(
295
- filename=output_file, extra_dims=writer_extra_dims, minor_version="4", dataformat_id="8", forward="all"
296
- )
297
-
298
- print("Traitement du nuage de point")
299
- pipeline.execute()
300
-
301
- # The orthoimages files will be deleted only when their reference are lost.
302
- # To keep them, make a copy (with e.g. shutil.copy(...))
303
- # See: https://docs.python.org/2/library/tempfile.html#tempfile.TemporaryFile
304
- return tmp_ortho, tmp_ortho_irc
305
-
306
-
307
- def parse_args():
308
- parser = argparse.ArgumentParser("Colorize tool", formatter_class=argparse.RawTextHelpFormatter)
309
- parser.add_argument("--input", "-i", type=str, required=True, help="Input file")
310
- parser.add_argument("--output", "-o", type=str, default="", help="Output file")
311
- parser.add_argument(
312
- "--proj", "-p", type=str, default="", help="Projection, default will use projection from metadata input"
313
- )
314
- parser.add_argument("--resolution", "-r", type=float, default=5, help="Resolution, in pixel per meter")
315
- parser.add_argument("--timeout", "-t", type=int, default=300, help="Timeout, in seconds")
316
- parser.add_argument("--rvb", action="store_true", help="Colorize RVB")
317
- parser.add_argument("--ir", action="store_true", help="Colorize IR")
318
- parser.add_argument(
319
- "--vegetation", type=str, default="", help="Vegetation file, value will be stored in Deviation field"
320
- )
321
- parser.add_argument("--check-images", "-c", action="store_true", help="Check that downloaded image is not white")
322
- parser.add_argument(
323
- "--stream-RGB",
324
- type=str,
325
- default="ORTHOIMAGERY.ORTHOPHOTOS",
326
- help="""WMS raster stream for RGB colorization:
327
- default stream (ORTHOIMAGERY.ORTHOPHOTOS) let the server choose the resolution
328
- for 20cm resolution rasters, use HR.ORTHOIMAGERY.ORTHOPHOTOS
329
- for 50 cm resolution rasters, use ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO""",
330
- )
331
- parser.add_argument(
332
- "--stream-IRC",
333
- type=str,
334
- default="ORTHOIMAGERY.ORTHOPHOTOS.IRC",
335
- help="""WMS raster stream for IRC colorization. Default to ORTHOIMAGERY.ORTHOPHOTOS.IRC
336
- Documentation about possible stream : https://geoservices.ign.fr/services-web-experts-ortho""",
337
- )
338
- parser.add_argument(
339
- "--size-max-GPF",
340
- type=int,
341
- default=5000,
342
- help="Maximum edge size (in pixels) of downloaded images."
343
- " If input file needs more, several images are downloaded and merged.",
344
- )
345
-
346
- return parser.parse_args()
347
-
348
-
349
- if __name__ == "__main__":
350
- args = parse_args()
351
- color(
352
- input_file=args.input,
353
- output_file=args.output,
354
- proj=args.proj,
355
- pixel_per_meter=args.resolution,
356
- timeout_second=args.timeout,
357
- color_rvb_enabled=args.rvb,
358
- color_ir_enabled=args.ir,
359
- veget_index_file=args.vegetation,
360
- check_images=args.check_images,
361
- stream_RGB=args.stream_RGB,
362
- stream_IRC=args.stream_IRC,
363
- size_max_gpf=args.size_max_GPF,
364
- )
@@ -254,3 +254,19 @@ def get_epsg_from_las(filename: str) -> str:
254
254
  return None # Return None if CRS is not defined
255
255
  epsg_code = crs.to_epsg()
256
256
  return f"EPSG:{epsg_code}" if epsg_code else None
257
+
258
+
259
+ def list_dims(las_filename):
260
+ """List dimensions
261
+
262
+ Args:
263
+ las_file (_type_): _description_
264
+
265
+ Returns:
266
+ List<String>: Dimensions names
267
+ """
268
+ pipeline = pdal.Pipeline()
269
+ pipeline |= pdal.Reader.las(filename=las_filename)
270
+ pipeline.execute()
271
+
272
+ return list(pipeline.arrays[0].dtype.fields.keys())