ign-pdal-tools 1.13.0__py3-none-any.whl → 1.14.0__py3-none-any.whl
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.dist-info → ign_pdal_tools-1.14.0.dist-info}/METADATA +2 -1
- {ign_pdal_tools-1.13.0.dist-info → ign_pdal_tools-1.14.0.dist-info}/RECORD +9 -7
- pdaltools/_version.py +1 -1
- pdaltools/color.py +48 -189
- pdaltools/download_image.py +192 -0
- pdaltools/replace_area_in_pointcloud.py +79 -0
- {ign_pdal_tools-1.13.0.dist-info → ign_pdal_tools-1.14.0.dist-info}/WHEEL +0 -0
- {ign_pdal_tools-1.13.0.dist-info → ign_pdal_tools-1.14.0.dist-info}/licenses/LICENSE.md +0 -0
- {ign_pdal_tools-1.13.0.dist-info → ign_pdal_tools-1.14.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ign-pdal-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.14.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.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
ign_pdal_tools-1.
|
|
2
|
-
pdaltools/_version.py,sha256=
|
|
1
|
+
ign_pdal_tools-1.14.0.dist-info/licenses/LICENSE.md,sha256=iVzCFZTUXeiqP8bP474iuWZiWO_kDCD4SPh1Wiw125Y,1120
|
|
2
|
+
pdaltools/_version.py,sha256=eHT0NBFMCS431k6M3stEzwiZkw_EO8ROqR0MO0r5LM4,75
|
|
3
3
|
pdaltools/add_points_in_pointcloud.py,sha256=VM2HW2b1Ul_I8jtXaOpTsmyGjiEFgoSi8AmCLuj6gH8,12697
|
|
4
|
-
pdaltools/color.py,sha256=
|
|
4
|
+
pdaltools/color.py,sha256=s-_rmLK6fIK3UwkUzHVZPEkm6r1LliG5ftGr-jkqyjM,9549
|
|
5
5
|
pdaltools/create_random_laz.py,sha256=XuHH4G8Nrs8DB-F8bkcIeto7JtmrlrNGF_R66oxGCbQ,6069
|
|
6
|
+
pdaltools/download_image.py,sha256=DG9PunQsjw7Uyyf4YMVp8LMH0G3Uo4cahx5EZbdi3so,7846
|
|
6
7
|
pdaltools/las_add_buffer.py,sha256=rnFExAfi0KqlQpL4hDMh2aC08AcYdSHSB6WPG5RyFIc,11274
|
|
7
8
|
pdaltools/las_clip.py,sha256=GvEOYu8RXV68e35kU8i42GwSkbo4P9TvmS6rkrdPmFM,1034
|
|
8
9
|
pdaltools/las_comparison.py,sha256=B9hFGbmD0x4JEN4oHbiQFNbd0T-9P3mnAN67Czu0pZk,4505
|
|
@@ -11,10 +12,11 @@ pdaltools/las_merge.py,sha256=tcFVueV9X9nNEaoAl5zCduY5DETlBg63MAgP2SuKiNo,4121
|
|
|
11
12
|
pdaltools/las_remove_dimensions.py,sha256=f8imGhN6LNTuQ1GMJQRzIIV3Wab_oRPOyEnKi1CgfiM,2318
|
|
12
13
|
pdaltools/las_rename_dimension.py,sha256=FEWIcq0ZZiv9xWbCLDRE9Hzb5K0YYfoi3Z8IZFEs-uU,2887
|
|
13
14
|
pdaltools/pcd_info.py,sha256=NIAH5KGikVDQLlbCcw9FuaPqe20UZvRfkHsDZd5kmZA,3210
|
|
15
|
+
pdaltools/replace_area_in_pointcloud.py,sha256=SaF5NXjtMOVyU4XUfO0REZGBZoRp32zjmrb1ZodfEow,3319
|
|
14
16
|
pdaltools/replace_attribute_in_las.py,sha256=MHpIizSupgWtbizteoRH8FKDE049hrAh4v_OhmRmSPU,4318
|
|
15
17
|
pdaltools/standardize_format.py,sha256=I2oNiwhSMtr4e5ZK9qbB_yKmy3twOoO6QLiSFu4_AaI,3905
|
|
16
18
|
pdaltools/unlock_file.py,sha256=G2odk0cpp_X9r49Y90oK88v3qlihaMfg6acwmWqblik,1958
|
|
17
|
-
ign_pdal_tools-1.
|
|
18
|
-
ign_pdal_tools-1.
|
|
19
|
-
ign_pdal_tools-1.
|
|
20
|
-
ign_pdal_tools-1.
|
|
19
|
+
ign_pdal_tools-1.14.0.dist-info/METADATA,sha256=QIzL7Suc-iA8MNeYrwzmJX6mT3pOXlfcLx07UhvmPVA,5982
|
|
20
|
+
ign_pdal_tools-1.14.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
ign_pdal_tools-1.14.0.dist-info/top_level.txt,sha256=KvGW0ZzqQbhCKzB5_Tp_buWMZyIgiO2M2krWF_ecOZc,10
|
|
22
|
+
ign_pdal_tools-1.14.0.dist-info/RECORD,,
|
pdaltools/_version.py
CHANGED
pdaltools/color.py
CHANGED
|
@@ -1,196 +1,12 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import tempfile
|
|
3
|
-
import time
|
|
4
3
|
from math import ceil, floor
|
|
5
|
-
from pathlib import Path
|
|
6
4
|
from typing import Tuple
|
|
7
5
|
|
|
8
|
-
import numpy as np
|
|
9
6
|
import pdal
|
|
10
|
-
import requests
|
|
11
|
-
from osgeo import gdal, gdal_array
|
|
12
7
|
|
|
13
8
|
import pdaltools.las_info as las_info
|
|
14
|
-
from pdaltools.
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
return func(*args, **kwargs)
|
|
53
|
-
|
|
54
|
-
return newfn
|
|
55
|
-
|
|
56
|
-
return decorator
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def is_image_white(filename: str):
|
|
60
|
-
raster_array = gdal_array.LoadFile(filename)
|
|
61
|
-
band_is_white = [np.all(band == 255) for band in raster_array]
|
|
62
|
-
return np.all(band_is_white)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def download_image_from_geoplateforme(
|
|
66
|
-
proj, layer, minx, miny, maxx, maxy, width_pixels, height_pixels, outfile, timeout, check_images
|
|
67
|
-
):
|
|
68
|
-
"""
|
|
69
|
-
Download image using a wms request to geoplateforme.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
proj (int): epsg code for the projection of the downloaded image.
|
|
73
|
-
layer: which kind of image is downloaded (ORTHOIMAGERY.ORTHOPHOTOS, ORTHOIMAGERY.ORTHOPHOTOS.IRC, ...).
|
|
74
|
-
minx, miny, maxx, maxy: box of the downloaded image.
|
|
75
|
-
width_pixels: width in pixels of the downloaded image.
|
|
76
|
-
height_pixels: height in pixels of the downloaded image.
|
|
77
|
-
outfile: file name of the downloaded file
|
|
78
|
-
timeout: delay after which the request is canceled (in seconds)
|
|
79
|
-
check_images (bool): enable checking if the output image is not a white image
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
# for layer in layers:
|
|
83
|
-
URL_GPP = "https://data.geopf.fr/wms-r/wms?"
|
|
84
|
-
URL_FORMAT = "&EXCEPTIONS=text/xml&FORMAT=image/geotiff&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES="
|
|
85
|
-
URL_EPSG = "&CRS=EPSG:" + str(proj)
|
|
86
|
-
URL_BBOX = "&BBOX=" + str(minx) + "," + str(miny) + "," + str(maxx) + "," + str(maxy)
|
|
87
|
-
URL_SIZE = "&WIDTH=" + str(width_pixels) + "&HEIGHT=" + str(height_pixels)
|
|
88
|
-
|
|
89
|
-
URL = URL_GPP + "LAYERS=" + layer + URL_FORMAT + URL_EPSG + URL_BBOX + URL_SIZE
|
|
90
|
-
|
|
91
|
-
print(URL)
|
|
92
|
-
if timeout < 10:
|
|
93
|
-
print(f"Mode debug avec un timeout à {timeout} secondes")
|
|
94
|
-
|
|
95
|
-
req = requests.get(URL, allow_redirects=True, timeout=timeout)
|
|
96
|
-
req.raise_for_status()
|
|
97
|
-
print(f"Ecriture du fichier: {outfile}")
|
|
98
|
-
open(outfile, "wb").write(req.content)
|
|
99
|
-
|
|
100
|
-
if check_images and is_image_white(outfile):
|
|
101
|
-
raise ValueError(f"Downloaded image is white, with stream: {layer}")
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
return nb_pixels, nb_cells, cell_size_pixels
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@copy_and_hack_decorator
|
|
125
|
-
def download_image(proj, layer, minx, miny, maxx, maxy, pixel_per_meter, outfile, timeout, check_images, size_max_gpf):
|
|
126
|
-
"""
|
|
127
|
-
Download image using a wms request to geoplateforme with call of download_image_from_geoplateforme() :
|
|
128
|
-
image are downloaded in blocks then merged, in order to limit the size of geoplateforme requests.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
proj: projection of the downloaded image.
|
|
132
|
-
layer: which kind of image is downloaed (ORTHOIMAGERY.ORTHOPHOTOS, ORTHOIMAGERY.ORTHOPHOTOS.IRC, ...).
|
|
133
|
-
minx, miny, maxx, maxy: box of the downloaded image.
|
|
134
|
-
pixel_per_meter: resolution of the downloaded image.
|
|
135
|
-
outfile: file name of the downloaed file
|
|
136
|
-
timeout: time after the request is canceled
|
|
137
|
-
check_images: check if images is not a white image
|
|
138
|
-
size_max_gpf: block size of downloaded images. (in pixels)
|
|
139
|
-
|
|
140
|
-
return the number of effective requests
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
download_image_from_geoplateforme_retrying = retry(times=9, delay=5, factor=2)(download_image_from_geoplateforme)
|
|
144
|
-
|
|
145
|
-
size_x_p, nb_cells_x, cell_size_x = compute_cells_size(minx, maxx, pixel_per_meter, size_max_gpf)
|
|
146
|
-
size_y_p, nb_cells_y, cell_size_y = compute_cells_size(minx, maxx, pixel_per_meter, size_max_gpf)
|
|
147
|
-
|
|
148
|
-
# the image size is under SIZE_MAX_IMAGE_GPF
|
|
149
|
-
if (size_x_p <= size_max_gpf) and (size_y_p <= size_max_gpf):
|
|
150
|
-
download_image_from_geoplateforme_retrying(
|
|
151
|
-
proj, layer, minx, miny, maxx, maxy, cell_size_x, cell_size_y, outfile, timeout, check_images
|
|
152
|
-
)
|
|
153
|
-
return 1
|
|
154
|
-
|
|
155
|
-
# the image is bigger than the SIZE_MAX_IMAGE_GPF
|
|
156
|
-
# it's preferable to compute it by paving
|
|
157
|
-
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
158
|
-
tmp_gpg_ortho = []
|
|
159
|
-
for line in range(0, nb_cells_y):
|
|
160
|
-
for col in range(0, nb_cells_x):
|
|
161
|
-
# Cope for last line/col that can be slightly smaller than other cells
|
|
162
|
-
remaining_pixels_x = size_x_p - col * cell_size_x
|
|
163
|
-
remaining_pixels_y = size_y_p - line * cell_size_y
|
|
164
|
-
cell_size_x_local = min(cell_size_x, remaining_pixels_x)
|
|
165
|
-
cell_size_y_local = min(cell_size_y, remaining_pixels_y)
|
|
166
|
-
|
|
167
|
-
minx_cell = minx + col * cell_size_x / pixel_per_meter
|
|
168
|
-
maxx_cell = minx_cell + cell_size_x_local / pixel_per_meter
|
|
169
|
-
miny_cell = miny + line * cell_size_y / pixel_per_meter
|
|
170
|
-
maxy_cell = miny_cell + cell_size_y_local / pixel_per_meter
|
|
171
|
-
|
|
172
|
-
cells_ortho_paths = str(Path(tmp_dir)) + f"cell_{col}_{line}.tif"
|
|
173
|
-
download_image_from_geoplateforme_retrying(
|
|
174
|
-
proj,
|
|
175
|
-
layer,
|
|
176
|
-
minx_cell,
|
|
177
|
-
miny_cell,
|
|
178
|
-
maxx_cell,
|
|
179
|
-
maxy_cell,
|
|
180
|
-
cell_size_x_local,
|
|
181
|
-
cell_size_y_local,
|
|
182
|
-
cells_ortho_paths,
|
|
183
|
-
timeout,
|
|
184
|
-
check_images,
|
|
185
|
-
)
|
|
186
|
-
tmp_gpg_ortho.append(cells_ortho_paths)
|
|
187
|
-
|
|
188
|
-
# merge the cells
|
|
189
|
-
with tempfile.NamedTemporaryFile(suffix="_gpf.vrt") as tmp_vrt:
|
|
190
|
-
gdal.BuildVRT(tmp_vrt.name, tmp_gpg_ortho)
|
|
191
|
-
gdal.Translate(outfile, tmp_vrt.name)
|
|
192
|
-
|
|
193
|
-
return nb_cells_x * nb_cells_y
|
|
9
|
+
from pdaltools.download_image import download_image
|
|
194
10
|
|
|
195
11
|
|
|
196
12
|
def match_min_max_with_pixel_size(min_d: float, max_d: float, pixel_per_meter: float) -> Tuple[float, float]:
|
|
@@ -224,11 +40,47 @@ def color(
|
|
|
224
40
|
color_rvb_enabled=True,
|
|
225
41
|
color_ir_enabled=True,
|
|
226
42
|
veget_index_file="",
|
|
43
|
+
vegetation_dim="Deviation",
|
|
227
44
|
check_images=False,
|
|
228
45
|
stream_RGB="ORTHOIMAGERY.ORTHOPHOTOS",
|
|
229
46
|
stream_IRC="ORTHOIMAGERY.ORTHOPHOTOS.IRC",
|
|
230
47
|
size_max_gpf=5000,
|
|
231
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
|
+
"""
|
|
232
84
|
metadata = las_info.las_info_metadata(input_file)
|
|
233
85
|
minx, maxx, miny, maxy = las_info.get_bounds_from_header_info(metadata)
|
|
234
86
|
|
|
@@ -243,9 +95,9 @@ def color(
|
|
|
243
95
|
writer_extra_dims = "all"
|
|
244
96
|
|
|
245
97
|
if veget_index_file and veget_index_file != "":
|
|
246
|
-
print(f"Remplissage du champ
|
|
247
|
-
pipeline |= pdal.Filter.colorization(raster=veget_index_file, dimensions="
|
|
248
|
-
writer_extra_dims = ["
|
|
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"]
|
|
249
101
|
|
|
250
102
|
tmp_ortho = None
|
|
251
103
|
if color_rvb_enabled:
|
|
@@ -316,7 +168,13 @@ def parse_args():
|
|
|
316
168
|
parser.add_argument("--rvb", action="store_true", help="Colorize RVB")
|
|
317
169
|
parser.add_argument("--ir", action="store_true", help="Colorize IR")
|
|
318
170
|
parser.add_argument(
|
|
319
|
-
"--vegetation",
|
|
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"
|
|
320
178
|
)
|
|
321
179
|
parser.add_argument("--check-images", "-c", action="store_true", help="Check that downloaded image is not white")
|
|
322
180
|
parser.add_argument(
|
|
@@ -357,6 +215,7 @@ if __name__ == "__main__":
|
|
|
357
215
|
color_rvb_enabled=args.rvb,
|
|
358
216
|
color_ir_enabled=args.ir,
|
|
359
217
|
veget_index_file=args.vegetation,
|
|
218
|
+
vegetation_dim=args.vegetation_dim,
|
|
360
219
|
check_images=args.check_images,
|
|
361
220
|
stream_RGB=args.stream_RGB,
|
|
362
221
|
stream_IRC=args.stream_IRC,
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Tool to download an image from IGN geoplateform: https://geoservices.ign.fr/"""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
import time
|
|
5
|
+
from math import ceil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import requests
|
|
11
|
+
from osgeo import gdal, gdal_array
|
|
12
|
+
|
|
13
|
+
from pdaltools.unlock_file import copy_and_hack_decorator
|
|
14
|
+
|
|
15
|
+
|
|
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
|
|
19
|
+
|
|
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
|
|
25
|
+
|
|
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
|
|
32
|
+
|
|
33
|
+
return nb_pixels, nb_cells, cell_size_pixels
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_image_white(filename: str):
|
|
37
|
+
raster_array = gdal_array.LoadFile(filename)
|
|
38
|
+
band_is_white = [np.all(band == 255) for band in raster_array]
|
|
39
|
+
return np.all(band_is_white)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def download_image_from_geoplateforme(
|
|
43
|
+
proj, layer, minx, miny, maxx, maxy, width_pixels, height_pixels, outfile, timeout, check_images
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Download image using a wms request to geoplateforme.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
proj (int): epsg code for the projection of the downloaded image.
|
|
50
|
+
layer: which kind of image is downloaded (ORTHOIMAGERY.ORTHOPHOTOS, ORTHOIMAGERY.ORTHOPHOTOS.IRC, ...).
|
|
51
|
+
minx, miny, maxx, maxy: box of the downloaded image.
|
|
52
|
+
width_pixels: width in pixels of the downloaded image.
|
|
53
|
+
height_pixels: height in pixels of the downloaded image.
|
|
54
|
+
outfile: file name of the downloaded file
|
|
55
|
+
timeout: delay after which the request is canceled (in seconds)
|
|
56
|
+
check_images (bool): enable checking if the output image is not a white image
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# for layer in layers:
|
|
60
|
+
URL_GPP = "https://data.geopf.fr/wms-r/wms?"
|
|
61
|
+
URL_FORMAT = "&EXCEPTIONS=text/xml&FORMAT=image/geotiff&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&STYLES="
|
|
62
|
+
URL_EPSG = "&CRS=EPSG:" + str(proj)
|
|
63
|
+
URL_BBOX = "&BBOX=" + str(minx) + "," + str(miny) + "," + str(maxx) + "," + str(maxy)
|
|
64
|
+
URL_SIZE = "&WIDTH=" + str(width_pixels) + "&HEIGHT=" + str(height_pixels)
|
|
65
|
+
|
|
66
|
+
URL = URL_GPP + "LAYERS=" + layer + URL_FORMAT + URL_EPSG + URL_BBOX + URL_SIZE
|
|
67
|
+
|
|
68
|
+
print(URL)
|
|
69
|
+
if timeout < 10:
|
|
70
|
+
print(f"Mode debug avec un timeout à {timeout} secondes")
|
|
71
|
+
|
|
72
|
+
req = requests.get(URL, allow_redirects=True, timeout=timeout)
|
|
73
|
+
req.raise_for_status()
|
|
74
|
+
print(f"Ecriture du fichier: {outfile}")
|
|
75
|
+
open(outfile, "wb").write(req.content)
|
|
76
|
+
|
|
77
|
+
if check_images and is_image_white(outfile):
|
|
78
|
+
raise ValueError(f"Downloaded image is white, with stream: {layer}")
|
|
79
|
+
|
|
80
|
+
|
|
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)
|
|
95
|
+
|
|
96
|
+
|
|
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
|
|
115
|
+
|
|
116
|
+
return func(*args, **kwargs)
|
|
117
|
+
|
|
118
|
+
return newfn
|
|
119
|
+
|
|
120
|
+
return decorator
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@copy_and_hack_decorator
|
|
124
|
+
def download_image(proj, layer, minx, miny, maxx, maxy, pixel_per_meter, outfile, timeout, check_images, size_max_gpf):
|
|
125
|
+
"""
|
|
126
|
+
Download image using a wms request to geoplateforme with call of download_image_from_geoplateforme() :
|
|
127
|
+
image are downloaded in blocks then merged, in order to limit the size of geoplateforme requests.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
proj: projection of the downloaded image.
|
|
131
|
+
layer: which kind of image is downloaed (ORTHOIMAGERY.ORTHOPHOTOS, ORTHOIMAGERY.ORTHOPHOTOS.IRC, ...).
|
|
132
|
+
minx, miny, maxx, maxy: box of the downloaded image.
|
|
133
|
+
pixel_per_meter: resolution of the downloaded image.
|
|
134
|
+
outfile: file name of the downloaed file
|
|
135
|
+
timeout: time after the request is canceled
|
|
136
|
+
check_images: check if images is not a white image
|
|
137
|
+
size_max_gpf: block size of downloaded images. (in pixels)
|
|
138
|
+
|
|
139
|
+
return the number of effective requests
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
download_image_from_geoplateforme_retrying = retry(times=9, delay=5, factor=2)(download_image_from_geoplateforme)
|
|
143
|
+
|
|
144
|
+
size_x_p, nb_cells_x, cell_size_x = compute_cells_size(minx, maxx, pixel_per_meter, size_max_gpf)
|
|
145
|
+
size_y_p, nb_cells_y, cell_size_y = compute_cells_size(minx, maxx, pixel_per_meter, size_max_gpf)
|
|
146
|
+
|
|
147
|
+
# the image size is under SIZE_MAX_IMAGE_GPF
|
|
148
|
+
if (size_x_p <= size_max_gpf) and (size_y_p <= size_max_gpf):
|
|
149
|
+
download_image_from_geoplateforme_retrying(
|
|
150
|
+
proj, layer, minx, miny, maxx, maxy, cell_size_x, cell_size_y, outfile, timeout, check_images
|
|
151
|
+
)
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
# the image is bigger than the SIZE_MAX_IMAGE_GPF
|
|
155
|
+
# it's preferable to compute it by paving
|
|
156
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
157
|
+
tmp_gpg_ortho = []
|
|
158
|
+
for line in range(0, nb_cells_y):
|
|
159
|
+
for col in range(0, nb_cells_x):
|
|
160
|
+
# Cope for last line/col that can be slightly smaller than other cells
|
|
161
|
+
remaining_pixels_x = size_x_p - col * cell_size_x
|
|
162
|
+
remaining_pixels_y = size_y_p - line * cell_size_y
|
|
163
|
+
cell_size_x_local = min(cell_size_x, remaining_pixels_x)
|
|
164
|
+
cell_size_y_local = min(cell_size_y, remaining_pixels_y)
|
|
165
|
+
|
|
166
|
+
minx_cell = minx + col * cell_size_x / pixel_per_meter
|
|
167
|
+
maxx_cell = minx_cell + cell_size_x_local / pixel_per_meter
|
|
168
|
+
miny_cell = miny + line * cell_size_y / pixel_per_meter
|
|
169
|
+
maxy_cell = miny_cell + cell_size_y_local / pixel_per_meter
|
|
170
|
+
|
|
171
|
+
cells_ortho_paths = str(Path(tmp_dir)) + f"cell_{col}_{line}.tif"
|
|
172
|
+
download_image_from_geoplateforme_retrying(
|
|
173
|
+
proj,
|
|
174
|
+
layer,
|
|
175
|
+
minx_cell,
|
|
176
|
+
miny_cell,
|
|
177
|
+
maxx_cell,
|
|
178
|
+
maxy_cell,
|
|
179
|
+
cell_size_x_local,
|
|
180
|
+
cell_size_y_local,
|
|
181
|
+
cells_ortho_paths,
|
|
182
|
+
timeout,
|
|
183
|
+
check_images,
|
|
184
|
+
)
|
|
185
|
+
tmp_gpg_ortho.append(cells_ortho_paths)
|
|
186
|
+
|
|
187
|
+
# merge the cells
|
|
188
|
+
with tempfile.NamedTemporaryFile(suffix="_gpf.vrt") as tmp_vrt:
|
|
189
|
+
gdal.BuildVRT(tmp_vrt.name, tmp_gpg_ortho)
|
|
190
|
+
gdal.Translate(outfile, tmp_vrt.name)
|
|
191
|
+
|
|
192
|
+
return nb_cells_x * nb_cells_y
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import pdal
|
|
4
|
+
|
|
5
|
+
from pdaltools.las_info import get_writer_parameters_from_reader_metadata
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_args():
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
"Replace points in a pointcloud with points from another pointcloud based on a area"
|
|
11
|
+
)
|
|
12
|
+
parser.add_argument("--target_cloud", "-t", type=str, help="filepath of target cloud to be modified")
|
|
13
|
+
parser.add_argument("--source_cloud", "-s", type=str, help="filepath of source cloud to use for replacement")
|
|
14
|
+
parser.add_argument("--replacement_area_file", "-r", type=str, help="filepath of file containing areas to replace")
|
|
15
|
+
parser.add_argument("--filter", "-f", type=str, help="pdal filter expression to apply to target_cloud")
|
|
16
|
+
parser.add_argument("--outfile", "-o", type=str, help="output file")
|
|
17
|
+
return parser.parse_args()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_writer_params(input_file):
|
|
21
|
+
pipeline = pdal.Pipeline()
|
|
22
|
+
pipeline |= pdal.Reader.las(filename=input_file)
|
|
23
|
+
pipeline.execute()
|
|
24
|
+
params = get_writer_parameters_from_reader_metadata(pipeline.metadata)
|
|
25
|
+
return params
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def replace_area(target_cloud, source_cloud, replacement_area_file, outfile, writer_params, filter=""):
|
|
29
|
+
crops = []
|
|
30
|
+
pipeline_target = pdal.Pipeline()
|
|
31
|
+
pipeline_target |= pdal.Reader.las(filename=target_cloud)
|
|
32
|
+
pipeline_target |= pdal.Filter.ferry(dimensions="=> geometryFid")
|
|
33
|
+
# Assign -1 to all points because overlay replaces values from 0 and more
|
|
34
|
+
pipeline_target |= pdal.Filter.assign(assignment="geometryFid[:]=-1")
|
|
35
|
+
if filter:
|
|
36
|
+
pipeline_target |= pdal.Filter.expression(expression=filter)
|
|
37
|
+
pipeline_target |= pdal.Filter.overlay(column="fid", dimension="geometryFid", datasource=replacement_area_file)
|
|
38
|
+
# Keep only points out of the area
|
|
39
|
+
pipeline_target |= pdal.Filter.expression(expression="geometryFid==-1", tag="A")
|
|
40
|
+
pipeline_target.execute()
|
|
41
|
+
|
|
42
|
+
input_dimensions = list(pipeline_target.arrays[0].dtype.fields.keys())
|
|
43
|
+
# do not keep geometryFid
|
|
44
|
+
output_dimensions = [dim for dim in input_dimensions if dim not in "geometryFid"]
|
|
45
|
+
target_cloud_pruned = pipeline_target.arrays[0][output_dimensions]
|
|
46
|
+
crops.append(target_cloud_pruned)
|
|
47
|
+
|
|
48
|
+
pipeline_source = pdal.Pipeline()
|
|
49
|
+
pipeline_source |= pdal.Reader.las(filename=source_cloud)
|
|
50
|
+
pipeline_source |= pdal.Filter.ferry(dimensions="=> geometryFid")
|
|
51
|
+
pipeline_source |= pdal.Filter.assign(assignment="geometryFid[:]=-1")
|
|
52
|
+
pipeline_source |= pdal.Filter.overlay(column="fid", dimension="geometryFid", datasource=replacement_area_file)
|
|
53
|
+
# Keep only points in the area
|
|
54
|
+
pipeline_source |= pdal.Filter.expression(expression="geometryFid>=0", tag="B")
|
|
55
|
+
pipeline_source.execute()
|
|
56
|
+
|
|
57
|
+
# delete geometryFid from source_cloud
|
|
58
|
+
source_cloud_pruned = pipeline_source.arrays[0][output_dimensions]
|
|
59
|
+
crops.append(source_cloud_pruned)
|
|
60
|
+
|
|
61
|
+
# Merge
|
|
62
|
+
pipeline = pdal.Filter.merge().pipeline(*crops)
|
|
63
|
+
pipeline |= pdal.Writer.las(filename=outfile, **writer_params)
|
|
64
|
+
pipeline.execute()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main():
|
|
68
|
+
args = parse_args()
|
|
69
|
+
|
|
70
|
+
writer_parameters = get_writer_params(args.target_cloud)
|
|
71
|
+
# writer_parameters["extra_dims"] = "" # no extra-dim by default
|
|
72
|
+
|
|
73
|
+
replace_area(
|
|
74
|
+
args.target_cloud, args.source_cloud, args.replacement_area_file, args.outfile, writer_parameters, args.filter
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|