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.
- {ign_pdal_tools-1.13.0/ign_pdal_tools.egg-info → ign_pdal_tools-1.15.0}/PKG-INFO +4 -1
- ign_pdal_tools-1.13.0/PKG-INFO → ign_pdal_tools-1.15.0/README.md +3 -9
- ign_pdal_tools-1.13.0/README.md → ign_pdal_tools-1.15.0/ign_pdal_tools.egg-info/PKG-INFO +12 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/ign_pdal_tools.egg-info/SOURCES.txt +5 -1
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/_version.py +1 -1
- ign_pdal_tools-1.15.0/pdaltools/color.py +223 -0
- ign_pdal_tools-1.13.0/pdaltools/color.py → ign_pdal_tools-1.15.0/pdaltools/download_image.py +55 -227
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_info.py +16 -0
- ign_pdal_tools-1.15.0/pdaltools/replace_area_in_pointcloud.py +212 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pyproject.toml +0 -1
- ign_pdal_tools-1.15.0/test/test_color.py +198 -0
- ign_pdal_tools-1.13.0/test/test_color.py → ign_pdal_tools-1.15.0/test/test_download_image.py +37 -119
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_add_buffer.py +1 -1
- ign_pdal_tools-1.13.0/test/test_pdal_custom.py → ign_pdal_tools-1.15.0/test/test_pdal.py +3 -2
- ign_pdal_tools-1.15.0/test/test_replace_area_in_pointcloud.py +339 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/LICENSE.md +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/ign_pdal_tools.egg-info/dependency_links.txt +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/ign_pdal_tools.egg-info/top_level.txt +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/add_points_in_pointcloud.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/create_random_laz.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_add_buffer.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_clip.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_comparison.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_merge.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_remove_dimensions.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/las_rename_dimension.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/pcd_info.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/replace_attribute_in_las.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/standardize_format.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/pdaltools/unlock_file.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/setup.cfg +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_add_points_in_pointcloud.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_create_random_laz.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_clip.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_comparison.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_info.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_merge.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_remove_dimensions.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_las_rename_dimension.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_pcd_info.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_replace_attribute_in_las.py +0 -0
- {ign_pdal_tools-1.13.0 → ign_pdal_tools-1.15.0}/test/test_standardize_format.py +0 -0
- {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.
|
|
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/
|
|
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
|
|
@@ -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
|
+
)
|
ign_pdal_tools-1.13.0/pdaltools/color.py → ign_pdal_tools-1.15.0/pdaltools/download_image.py
RENAMED
|
@@ -1,59 +1,36 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
105
|
-
""
|
|
106
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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())
|