cars 1.0.0rc3__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
Files changed (220) hide show
  1. cars/__init__.py +74 -0
  2. cars/applications/__init__.py +40 -0
  3. cars/applications/application.py +117 -0
  4. cars/applications/application_constants.py +29 -0
  5. cars/applications/application_template.py +146 -0
  6. cars/applications/auxiliary_filling/__init__.py +29 -0
  7. cars/applications/auxiliary_filling/abstract_auxiliary_filling_app.py +105 -0
  8. cars/applications/auxiliary_filling/auxiliary_filling_algo.py +475 -0
  9. cars/applications/auxiliary_filling/auxiliary_filling_from_sensors_app.py +632 -0
  10. cars/applications/auxiliary_filling/auxiliary_filling_wrappers.py +90 -0
  11. cars/applications/dem_generation/__init__.py +30 -0
  12. cars/applications/dem_generation/abstract_dem_generation_app.py +116 -0
  13. cars/applications/dem_generation/bulldozer_config/base_config.yaml +46 -0
  14. cars/applications/dem_generation/bulldozer_dem_app.py +641 -0
  15. cars/applications/dem_generation/bulldozer_memory.py +55 -0
  16. cars/applications/dem_generation/dem_generation_algo.py +107 -0
  17. cars/applications/dem_generation/dem_generation_constants.py +32 -0
  18. cars/applications/dem_generation/dem_generation_wrappers.py +323 -0
  19. cars/applications/dense_match_filling/__init__.py +30 -0
  20. cars/applications/dense_match_filling/abstract_dense_match_filling_app.py +242 -0
  21. cars/applications/dense_match_filling/fill_disp_algo.py +113 -0
  22. cars/applications/dense_match_filling/fill_disp_constants.py +39 -0
  23. cars/applications/dense_match_filling/fill_disp_wrappers.py +83 -0
  24. cars/applications/dense_match_filling/zero_padding_app.py +302 -0
  25. cars/applications/dense_matching/__init__.py +30 -0
  26. cars/applications/dense_matching/abstract_dense_matching_app.py +261 -0
  27. cars/applications/dense_matching/census_mccnn_sgm_app.py +1461 -0
  28. cars/applications/dense_matching/cpp/__init__.py +0 -0
  29. cars/applications/dense_matching/cpp/dense_matching_cpp.cpython-313-x86_64-linux-gnu.so +0 -0
  30. cars/applications/dense_matching/cpp/dense_matching_cpp.py +94 -0
  31. cars/applications/dense_matching/cpp/includes/dense_matching.hpp +58 -0
  32. cars/applications/dense_matching/cpp/meson.build +9 -0
  33. cars/applications/dense_matching/cpp/src/bindings.cpp +13 -0
  34. cars/applications/dense_matching/cpp/src/dense_matching.cpp +207 -0
  35. cars/applications/dense_matching/dense_matching_algo.py +401 -0
  36. cars/applications/dense_matching/dense_matching_constants.py +89 -0
  37. cars/applications/dense_matching/dense_matching_wrappers.py +951 -0
  38. cars/applications/dense_matching/disparity_grid_algo.py +597 -0
  39. cars/applications/dense_matching/loaders/__init__.py +23 -0
  40. cars/applications/dense_matching/loaders/config_census_sgm_default.json +31 -0
  41. cars/applications/dense_matching/loaders/config_census_sgm_homogeneous.json +30 -0
  42. cars/applications/dense_matching/loaders/config_census_sgm_mountain_and_vegetation.json +30 -0
  43. cars/applications/dense_matching/loaders/config_census_sgm_shadow.json +30 -0
  44. cars/applications/dense_matching/loaders/config_census_sgm_sparse.json +36 -0
  45. cars/applications/dense_matching/loaders/config_census_sgm_urban.json +30 -0
  46. cars/applications/dense_matching/loaders/config_mapping.json +13 -0
  47. cars/applications/dense_matching/loaders/config_mccnn.json +28 -0
  48. cars/applications/dense_matching/loaders/global_land_cover_map.tif +0 -0
  49. cars/applications/dense_matching/loaders/pandora_loader.py +593 -0
  50. cars/applications/dsm_filling/__init__.py +32 -0
  51. cars/applications/dsm_filling/abstract_dsm_filling_app.py +101 -0
  52. cars/applications/dsm_filling/border_interpolation_app.py +278 -0
  53. cars/applications/dsm_filling/bulldozer_config/base_config.yaml +44 -0
  54. cars/applications/dsm_filling/bulldozer_filling_app.py +288 -0
  55. cars/applications/dsm_filling/exogenous_filling_app.py +341 -0
  56. cars/applications/dsm_merging/__init__.py +28 -0
  57. cars/applications/dsm_merging/abstract_dsm_merging_app.py +101 -0
  58. cars/applications/dsm_merging/weighted_fusion_app.py +639 -0
  59. cars/applications/grid_correction/__init__.py +30 -0
  60. cars/applications/grid_correction/abstract_grid_correction_app.py +103 -0
  61. cars/applications/grid_correction/grid_correction_app.py +557 -0
  62. cars/applications/grid_generation/__init__.py +30 -0
  63. cars/applications/grid_generation/abstract_grid_generation_app.py +142 -0
  64. cars/applications/grid_generation/epipolar_grid_generation_app.py +327 -0
  65. cars/applications/grid_generation/grid_generation_algo.py +388 -0
  66. cars/applications/grid_generation/grid_generation_constants.py +46 -0
  67. cars/applications/grid_generation/transform_grid.py +88 -0
  68. cars/applications/ground_truth_reprojection/__init__.py +30 -0
  69. cars/applications/ground_truth_reprojection/abstract_ground_truth_reprojection_app.py +137 -0
  70. cars/applications/ground_truth_reprojection/direct_localization_app.py +629 -0
  71. cars/applications/ground_truth_reprojection/ground_truth_reprojection_algo.py +275 -0
  72. cars/applications/point_cloud_outlier_removal/__init__.py +30 -0
  73. cars/applications/point_cloud_outlier_removal/abstract_outlier_removal_app.py +385 -0
  74. cars/applications/point_cloud_outlier_removal/outlier_removal_algo.py +392 -0
  75. cars/applications/point_cloud_outlier_removal/outlier_removal_constants.py +43 -0
  76. cars/applications/point_cloud_outlier_removal/small_components_app.py +522 -0
  77. cars/applications/point_cloud_outlier_removal/statistical_app.py +528 -0
  78. cars/applications/rasterization/__init__.py +30 -0
  79. cars/applications/rasterization/abstract_pc_rasterization_app.py +183 -0
  80. cars/applications/rasterization/rasterization_algo.py +534 -0
  81. cars/applications/rasterization/rasterization_constants.py +38 -0
  82. cars/applications/rasterization/rasterization_wrappers.py +639 -0
  83. cars/applications/rasterization/simple_gaussian_app.py +1152 -0
  84. cars/applications/resampling/__init__.py +28 -0
  85. cars/applications/resampling/abstract_resampling_app.py +187 -0
  86. cars/applications/resampling/bicubic_resampling_app.py +760 -0
  87. cars/applications/resampling/resampling_algo.py +590 -0
  88. cars/applications/resampling/resampling_constants.py +36 -0
  89. cars/applications/resampling/resampling_wrappers.py +309 -0
  90. cars/applications/sensors_subsampling/__init__.py +32 -0
  91. cars/applications/sensors_subsampling/abstract_subsampling_app.py +109 -0
  92. cars/applications/sensors_subsampling/rasterio_subsampling_app.py +420 -0
  93. cars/applications/sensors_subsampling/subsampling_algo.py +108 -0
  94. cars/applications/sparse_matching/__init__.py +30 -0
  95. cars/applications/sparse_matching/abstract_sparse_matching_app.py +599 -0
  96. cars/applications/sparse_matching/sift_app.py +724 -0
  97. cars/applications/sparse_matching/sparse_matching_algo.py +360 -0
  98. cars/applications/sparse_matching/sparse_matching_constants.py +66 -0
  99. cars/applications/sparse_matching/sparse_matching_wrappers.py +282 -0
  100. cars/applications/triangulation/__init__.py +32 -0
  101. cars/applications/triangulation/abstract_triangulation_app.py +227 -0
  102. cars/applications/triangulation/line_of_sight_intersection_app.py +1243 -0
  103. cars/applications/triangulation/pc_transform.py +552 -0
  104. cars/applications/triangulation/triangulation_algo.py +371 -0
  105. cars/applications/triangulation/triangulation_constants.py +38 -0
  106. cars/applications/triangulation/triangulation_wrappers.py +259 -0
  107. cars/bundleadjustment.py +750 -0
  108. cars/cars.py +179 -0
  109. cars/conf/__init__.py +23 -0
  110. cars/conf/geoid/egm96.grd +0 -0
  111. cars/conf/geoid/egm96.grd.hdr +15 -0
  112. cars/conf/input_parameters.py +156 -0
  113. cars/conf/mask_cst.py +35 -0
  114. cars/core/__init__.py +23 -0
  115. cars/core/cars_logging.py +402 -0
  116. cars/core/constants.py +191 -0
  117. cars/core/constants_disparity.py +50 -0
  118. cars/core/datasets.py +140 -0
  119. cars/core/geometry/__init__.py +27 -0
  120. cars/core/geometry/abstract_geometry.py +1130 -0
  121. cars/core/geometry/shareloc_geometry.py +604 -0
  122. cars/core/inputs.py +568 -0
  123. cars/core/outputs.py +176 -0
  124. cars/core/preprocessing.py +722 -0
  125. cars/core/projection.py +843 -0
  126. cars/core/roi_tools.py +215 -0
  127. cars/core/tiling.py +774 -0
  128. cars/core/utils.py +164 -0
  129. cars/data_structures/__init__.py +23 -0
  130. cars/data_structures/cars_dataset.py +1544 -0
  131. cars/data_structures/cars_dict.py +74 -0
  132. cars/data_structures/corresponding_tiles_tools.py +186 -0
  133. cars/data_structures/dataframe_converter.py +185 -0
  134. cars/data_structures/format_transformation.py +297 -0
  135. cars/devibrate.py +689 -0
  136. cars/extractroi.py +264 -0
  137. cars/orchestrator/__init__.py +23 -0
  138. cars/orchestrator/achievement_tracker.py +125 -0
  139. cars/orchestrator/cluster/__init__.py +37 -0
  140. cars/orchestrator/cluster/abstract_cluster.py +250 -0
  141. cars/orchestrator/cluster/abstract_dask_cluster.py +381 -0
  142. cars/orchestrator/cluster/dask_cluster_tools.py +103 -0
  143. cars/orchestrator/cluster/dask_config/README.md +94 -0
  144. cars/orchestrator/cluster/dask_config/dask.yaml +21 -0
  145. cars/orchestrator/cluster/dask_config/distributed.yaml +70 -0
  146. cars/orchestrator/cluster/dask_config/jobqueue.yaml +26 -0
  147. cars/orchestrator/cluster/dask_config/reference_confs/dask-schema.yaml +137 -0
  148. cars/orchestrator/cluster/dask_config/reference_confs/dask.yaml +26 -0
  149. cars/orchestrator/cluster/dask_config/reference_confs/distributed-schema.yaml +1009 -0
  150. cars/orchestrator/cluster/dask_config/reference_confs/distributed.yaml +273 -0
  151. cars/orchestrator/cluster/dask_config/reference_confs/jobqueue.yaml +212 -0
  152. cars/orchestrator/cluster/dask_jobqueue_utils.py +204 -0
  153. cars/orchestrator/cluster/local_dask_cluster.py +116 -0
  154. cars/orchestrator/cluster/log_wrapper.py +728 -0
  155. cars/orchestrator/cluster/mp_cluster/__init__.py +27 -0
  156. cars/orchestrator/cluster/mp_cluster/mp_factorizer.py +212 -0
  157. cars/orchestrator/cluster/mp_cluster/mp_objects.py +535 -0
  158. cars/orchestrator/cluster/mp_cluster/mp_tools.py +93 -0
  159. cars/orchestrator/cluster/mp_cluster/mp_wrapper.py +505 -0
  160. cars/orchestrator/cluster/mp_cluster/multiprocessing_cluster.py +986 -0
  161. cars/orchestrator/cluster/mp_cluster/multiprocessing_profiler.py +399 -0
  162. cars/orchestrator/cluster/pbs_dask_cluster.py +207 -0
  163. cars/orchestrator/cluster/sequential_cluster.py +139 -0
  164. cars/orchestrator/cluster/slurm_dask_cluster.py +234 -0
  165. cars/orchestrator/memory_tools.py +47 -0
  166. cars/orchestrator/orchestrator.py +755 -0
  167. cars/orchestrator/orchestrator_constants.py +29 -0
  168. cars/orchestrator/registry/__init__.py +23 -0
  169. cars/orchestrator/registry/abstract_registry.py +143 -0
  170. cars/orchestrator/registry/compute_registry.py +106 -0
  171. cars/orchestrator/registry/id_generator.py +116 -0
  172. cars/orchestrator/registry/replacer_registry.py +213 -0
  173. cars/orchestrator/registry/saver_registry.py +363 -0
  174. cars/orchestrator/registry/unseen_registry.py +118 -0
  175. cars/orchestrator/tiles_profiler.py +279 -0
  176. cars/pipelines/__init__.py +26 -0
  177. cars/pipelines/conf_resolution/conf_final_resolution.yaml +5 -0
  178. cars/pipelines/conf_resolution/conf_first_resolution.yaml +4 -0
  179. cars/pipelines/conf_resolution/conf_intermediate_resolution.yaml +2 -0
  180. cars/pipelines/default/__init__.py +26 -0
  181. cars/pipelines/default/default_pipeline.py +1095 -0
  182. cars/pipelines/filling/__init__.py +26 -0
  183. cars/pipelines/filling/filling.py +981 -0
  184. cars/pipelines/formatting/__init__.py +26 -0
  185. cars/pipelines/formatting/formatting.py +190 -0
  186. cars/pipelines/merging/__init__.py +26 -0
  187. cars/pipelines/merging/merging.py +439 -0
  188. cars/pipelines/parameters/__init__.py +0 -0
  189. cars/pipelines/parameters/advanced_parameters.py +256 -0
  190. cars/pipelines/parameters/advanced_parameters_constants.py +68 -0
  191. cars/pipelines/parameters/application_parameters.py +72 -0
  192. cars/pipelines/parameters/depth_map_inputs.py +0 -0
  193. cars/pipelines/parameters/dsm_inputs.py +349 -0
  194. cars/pipelines/parameters/dsm_inputs_constants.py +25 -0
  195. cars/pipelines/parameters/output_constants.py +52 -0
  196. cars/pipelines/parameters/output_parameters.py +435 -0
  197. cars/pipelines/parameters/sensor_inputs.py +859 -0
  198. cars/pipelines/parameters/sensor_inputs_constants.py +51 -0
  199. cars/pipelines/parameters/sensor_loaders/__init__.py +29 -0
  200. cars/pipelines/parameters/sensor_loaders/basic_classif_loader.py +86 -0
  201. cars/pipelines/parameters/sensor_loaders/basic_image_loader.py +98 -0
  202. cars/pipelines/parameters/sensor_loaders/pivot_classif_loader.py +90 -0
  203. cars/pipelines/parameters/sensor_loaders/pivot_image_loader.py +105 -0
  204. cars/pipelines/parameters/sensor_loaders/sensor_loader.py +93 -0
  205. cars/pipelines/parameters/sensor_loaders/sensor_loader_template.py +71 -0
  206. cars/pipelines/parameters/sensor_loaders/slurp_classif_loader.py +86 -0
  207. cars/pipelines/pipeline.py +119 -0
  208. cars/pipelines/pipeline_constants.py +38 -0
  209. cars/pipelines/pipeline_template.py +135 -0
  210. cars/pipelines/subsampling/__init__.py +26 -0
  211. cars/pipelines/subsampling/subsampling.py +358 -0
  212. cars/pipelines/surface_modeling/__init__.py +26 -0
  213. cars/pipelines/surface_modeling/surface_modeling.py +2098 -0
  214. cars/pipelines/tie_points/__init__.py +26 -0
  215. cars/pipelines/tie_points/tie_points.py +536 -0
  216. cars/starter.py +167 -0
  217. cars-1.0.0rc3.dist-info/METADATA +289 -0
  218. cars-1.0.0rc3.dist-info/RECORD +220 -0
  219. cars-1.0.0rc3.dist-info/WHEEL +6 -0
  220. cars-1.0.0rc3.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,843 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf8
3
+ #
4
+ # Copyright (c) 2020 Centre National d'Etudes Spatiales (CNES).
5
+ #
6
+ # This file is part of CARS
7
+ # (see https://github.com/CNES/cars).
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+ # you may not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing, software
16
+ # distributed under the License is distributed on an "AS IS" BASIS,
17
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ # See the License for the specific language governing permissions and
19
+ # limitations under the License.
20
+ #
21
+ """
22
+ Projection module:
23
+ contains some general purpose functions using polygons and data projections
24
+ """
25
+ # pylint: disable=C0302(too-many-lines)
26
+
27
+ # Standard imports
28
+ import logging
29
+ import os
30
+ from typing import List, Tuple
31
+
32
+ # Third party imports
33
+ import numpy as np
34
+ import pandas
35
+ import pyproj
36
+ import rasterio as rio
37
+ import xarray as xr
38
+ from pyproj import CRS
39
+ from rasterio.features import shapes
40
+ from shapely.geometry import Polygon, shape
41
+ from shapely.ops import transform
42
+
43
+ from cars.core import constants as cst
44
+ from cars.core import inputs, outputs, utils
45
+ from cars.orchestrator.cluster.log_wrapper import cars_profile
46
+
47
+
48
+ def compute_dem_intersection_with_poly( # noqa: C901
49
+ srtm_file: str, ref_poly: Polygon, ref_epsg: int
50
+ ) -> Polygon:
51
+ """
52
+ Compute the intersection polygon between the defined dem regions
53
+ and the reference polygon in input
54
+
55
+ :raise Exception: when the input dem doesn't intersect the reference polygon
56
+
57
+ :param srtm_file: srtm file
58
+ :param ref_poly: reference polygon
59
+ :param ref_epsg: reference epsg code
60
+ :return: The intersection polygon between the defined dem regions
61
+ and the reference polygon in input
62
+ """
63
+ dem_poly = None
64
+
65
+ if os.path.isdir(os.path.abspath(srtm_file)):
66
+ raise RuntimeError("srtm input should be a file and not a directory")
67
+
68
+ if inputs.rasterio_can_open(srtm_file):
69
+ with rio.open(srtm_file) as data:
70
+ xmin = min(data.bounds.left, data.bounds.right)
71
+ ymin = min(data.bounds.bottom, data.bounds.top)
72
+ xmax = max(data.bounds.left, data.bounds.right)
73
+ ymax = max(data.bounds.bottom, data.bounds.top)
74
+
75
+ try:
76
+ file_epsg = data.crs.to_epsg()
77
+ file_bb = Polygon(
78
+ [
79
+ (xmin, ymin),
80
+ (xmin, ymax),
81
+ (xmax, ymax),
82
+ (xmax, ymin),
83
+ (xmin, ymin),
84
+ ]
85
+ )
86
+
87
+ # transform polygon if needed
88
+ if ref_epsg != file_epsg:
89
+ file_bb = polygon_projection(file_bb, file_epsg, ref_epsg)
90
+
91
+ left, bottom, right, top = ref_poly.bounds
92
+
93
+ # To ensure that the window used for extracting the SRTM
94
+ # polygons completely contains the ref_poly,
95
+ # a margin must be applied. Using the exact bounds of
96
+ # ref_poly could potentially lead to issues.
97
+ spatial_ref = CRS.from_epsg(ref_epsg)
98
+ if spatial_ref.is_geographic:
99
+ margin = 0.001
100
+ else:
101
+ margin = 50
102
+
103
+ left = left - margin
104
+ right = right + margin
105
+ bottom = bottom - margin
106
+ top = top + margin
107
+
108
+ min_row, min_col = data.index(left, top)
109
+ max_row, max_col = data.index(right, bottom)
110
+
111
+ window = rio.windows.Window(
112
+ col_off=int(min_col),
113
+ row_off=int(min_row),
114
+ width=int(max_col - min_col),
115
+ height=int(max_row - min_row),
116
+ )
117
+
118
+ # if the srtm tile intersects the reference polygon
119
+ if file_bb.intersects(ref_poly):
120
+ local_dem_poly = None
121
+
122
+ # retrieve valid polygons
123
+ for poly, val in shapes(
124
+ data.read(1, window=window),
125
+ data.dataset_mask(window=window),
126
+ transform=data.window_transform(window),
127
+ ):
128
+ if val != 0:
129
+ poly = shape(poly)
130
+ poly = poly.buffer(0)
131
+ if ref_epsg != file_epsg:
132
+ poly = polygon_projection(
133
+ poly, file_epsg, ref_epsg
134
+ )
135
+
136
+ # combine valid polygons
137
+ if local_dem_poly is None:
138
+ local_dem_poly = poly
139
+ else:
140
+ local_dem_poly = poly.union(local_dem_poly)
141
+
142
+ # combine the tile valid polygon to the other
143
+ # tiles' ones
144
+ if dem_poly is None:
145
+ dem_poly = local_dem_poly
146
+ else:
147
+ dem_poly = dem_poly.union(local_dem_poly)
148
+
149
+ except AttributeError as attribute_error:
150
+ logging.warning(
151
+ "Impossible to read the SRTM"
152
+ "tile epsg code: {}".format(attribute_error)
153
+ )
154
+
155
+ # compute dem coverage polygon over the reference polygon
156
+ if dem_poly is None or not dem_poly.intersects(ref_poly):
157
+ raise RuntimeError("The input DEM does not intersect the useful zone")
158
+
159
+ dem_cover = dem_poly.intersection(ref_poly)
160
+
161
+ area_cover = dem_cover.area
162
+ area_inter = ref_poly.area
163
+
164
+ return dem_cover, area_cover / area_inter * 100.0
165
+
166
+
167
+ def polygon_projection(poly: Polygon, epsg_in: int, epsg_out: int) -> Polygon:
168
+ """
169
+ Projects a polygon from an initial epsg code to another
170
+
171
+ :param poly: poly to project
172
+ :param epsg_in: initial epsg code
173
+ :param epsg_out: final epsg code
174
+ :return: The polygon in the final projection
175
+ """
176
+ # Get CRS from input EPSG codes
177
+ crs_in = pyproj.CRS.from_epsg(epsg_in)
178
+ crs_out = pyproj.CRS.from_epsg(epsg_out)
179
+ # Project polygon between CRS (keep always_xy for compatibility)
180
+ project = pyproj.Transformer.from_crs(crs_in, crs_out, always_xy=True)
181
+ poly = transform(project.transform, poly)
182
+
183
+ return poly
184
+
185
+
186
+ def polygon_projection_crs(poly: Polygon, crs_in: CRS, crs_out: CRS) -> Polygon:
187
+ """
188
+ Projects a polygon from an initial crs to another
189
+
190
+ :param poly: poly to project
191
+ :param crs_in: initial crs
192
+ :param crs_out: final crs
193
+ :return: The polygon in the final projection
194
+ """
195
+ # Project polygon between CRS (keep always_xy for compatibility)
196
+ project = pyproj.Transformer.from_crs(crs_in, crs_out, always_xy=True)
197
+ poly = transform(project.transform, poly)
198
+
199
+ return poly
200
+
201
+
202
+ def geo_to_ecef(
203
+ lat: np.ndarray, lon: np.ndarray, alt: np.ndarray
204
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
205
+ """
206
+ Point transformation from Geodetic of ellipsoid WGS-84) to ECEF
207
+ ECEF: Earth-centered, Earth-fixed
208
+
209
+ :param lat: input geodetic latitude (angle in degree)
210
+ :param lon: input geodetic longitude (angle in degree)
211
+ :param alt: input altitude above geodetic ellipsoid (meters)
212
+ :return: ECEF (Earth centered, Earth fixed) x, y, z coordinates tuple
213
+ (in meters)
214
+ """
215
+ epsg_in = 4979 # EPSG code for Geocentric WGS84 in lat, lon, alt (degree)
216
+ epsg_out = 4978 # EPSG code for ECEF WGS84 in x, y, z (meters)
217
+
218
+ return point_cloud_conversion(
219
+ np.array([[lon, lat, alt]]), epsg_in, epsg_out
220
+ )[0]
221
+
222
+
223
+ def ecef_to_enu( # pylint: disable=too-many-positional-arguments
224
+ x_ecef: np.ndarray,
225
+ y_ecef: np.ndarray,
226
+ z_ecef: np.ndarray,
227
+ lat0: np.ndarray,
228
+ lon0: np.ndarray,
229
+ alt0: np.ndarray,
230
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
231
+ """
232
+ Coordinates conversion from ECEF Earth Centered to
233
+ East North Up Coordinate from a reference point (lat0, lon0, alt0)
234
+
235
+ See Wikipedia page for details:
236
+ https://en.wikipedia.org/wiki/Geographic_coordinate_conversion
237
+
238
+ :param x_ecef: target x ECEF coordinate (meters)
239
+ :param y_ecef: target y ECEF coordinate (meters)
240
+ :param z_ecef: target z ECEF coordinate (meters)
241
+ :param lat0: Reference geodetic latitude
242
+ :param lon0: Reference geodetic longitude
243
+ :param alt0: Reference altitude above geodetic ellipsoid (meters)
244
+ :return: ENU (xEast, yNorth zUp) target coordinates tuple (meters)
245
+ """
246
+ # Intermediate computing for ENU conversion
247
+ cos_lat0 = np.cos(np.radians(lat0))
248
+ sin_lat0 = np.sin(np.radians(lat0))
249
+
250
+ cos_long0 = np.cos(np.radians(lon0))
251
+ sin_long0 = np.sin(np.radians(lon0))
252
+
253
+ # Determine ECEF coordinates from reference geodetic
254
+ x0_ecef, y0_ecef, z0_ecef = geo_to_ecef(lat0, lon0, alt0)
255
+
256
+ x_east = (-(x_ecef - x0_ecef) * sin_long0) + (
257
+ (y_ecef - y0_ecef) * cos_long0
258
+ )
259
+ y_north = (
260
+ (-cos_long0 * sin_lat0 * (x_ecef - x0_ecef))
261
+ - (sin_lat0 * sin_long0 * (y_ecef - y0_ecef))
262
+ + (cos_lat0 * (z_ecef - z0_ecef))
263
+ )
264
+ z_up = (
265
+ (cos_lat0 * cos_long0 * (x_ecef - x0_ecef))
266
+ + (cos_lat0 * sin_long0 * (y_ecef - y0_ecef))
267
+ + (sin_lat0 * (z_ecef - z0_ecef))
268
+ )
269
+
270
+ return x_east, y_north, z_up
271
+
272
+
273
+ def geo_to_enu( # pylint: disable=too-many-positional-arguments
274
+ lat: np.ndarray,
275
+ lon: np.ndarray,
276
+ alt: np.ndarray,
277
+ lat0: np.ndarray,
278
+ lon0: np.ndarray,
279
+ alt0: np.ndarray,
280
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
281
+ """
282
+ Point transformation from WGS-84 Geodetic coordinates to to ENU.
283
+ Use geo_to_ecef and ecef_to_enu functions.
284
+
285
+ :param lat: input geodetic latitude (angle in degree)
286
+ :param lon: input geodetic longitude (angle in degree)
287
+ :param alt: input altitude above geodetic ellipsoid (meters)
288
+ :param lat0: Reference geodetic latitude
289
+ :param lon0: Reference geodetic longitude
290
+ :param alt0: Reference altitude above geodetic ellipsoid (meters)
291
+ :return: ENU (xEast, yNorth zUp) target coordinates tuple (meters)
292
+ """
293
+ x_ecef, y_ecef, z_ecef = geo_to_ecef(lat, lon, alt)
294
+ return ecef_to_enu(x_ecef, y_ecef, z_ecef, lat0, lon0, alt0)
295
+
296
+
297
+ def enu_to_aer(
298
+ x_east: np.ndarray, y_north: np.ndarray, z_up: np.ndarray
299
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
300
+ """
301
+ ENU coordinates to Azimuth, Elevation angle, Range from ENU origin
302
+ Beware: Elevation angle is not the altitude.
303
+
304
+ :param x_east: ENU East coordinate (meters)
305
+ :param y_north: ENU North coordinate (meters)
306
+ :param z_up: ENU Up coordinate (meters)
307
+ :return: Azimuth, Elevation Angle, Slant Range (degrees, degrees, meters)
308
+ """
309
+
310
+ xy_range = np.hypot(x_east, y_north) # Distance of e, n vector
311
+ xyz_range = np.hypot(xy_range, z_up) # Distance of e, n, u vector
312
+ elevation = np.arctan2(z_up, xy_range)
313
+ azimuth = np.arctan2(x_east, y_north) % (2 * np.pi)
314
+ # From [-pi,+pi] to [0,2pi]
315
+
316
+ azimuth = np.degrees(azimuth)
317
+ elevation = np.degrees(elevation)
318
+
319
+ return azimuth, elevation, xyz_range
320
+
321
+
322
+ def geo_to_aer( # pylint: disable=too-many-positional-arguments
323
+ lat: np.ndarray,
324
+ lon: np.ndarray,
325
+ alt: np.ndarray,
326
+ lat0: np.ndarray,
327
+ lon0: np.ndarray,
328
+ alt0: np.ndarray,
329
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
330
+ """
331
+ Gives Azimuth, Elevation angle and Slant Range
332
+ from a Reference to a Point with geodetic coordinates.
333
+
334
+ :param lat: input geodetic latitude (angle in degree)
335
+ :param lon: input geodetic longitude (angle in degree)
336
+ :param alt: input altitude above geodetic ellipsoid (meters)
337
+ :param lat0: Reference geodetic latitude
338
+ :param lon0: Reference geodetic longitude
339
+ :param alt0: Reference altitude above geodetic ellipsoid (meters)
340
+ :return: Azimuth, Elevation Angle, Slant Range (degrees, degrees, meters)
341
+ """
342
+ x_east, y_north, z_up = geo_to_enu(lat, lon, alt, lat0, lon0, alt0)
343
+ return enu_to_aer(x_east, y_north, z_up)
344
+
345
+
346
+ def point_cloud_conversion(
347
+ cloud_in: np.ndarray, epsg_in: int, epsg_out: int
348
+ ) -> np.ndarray:
349
+ """
350
+ Convert a point cloud from a SRS to another one.
351
+
352
+ :param cloud_in: cloud to project
353
+ :param epsg_in: EPSG code of the input SRS
354
+ :param epsg_out: EPSG code of the output SRS
355
+ :return: Projected point cloud
356
+ """
357
+ # Get CRS from input EPSG codes
358
+ crs_in = pyproj.CRS.from_epsg(epsg_in)
359
+ crs_out = pyproj.CRS.from_epsg(epsg_out)
360
+
361
+ # Project point cloud between CRS (keep always_xy for compatibility)
362
+ cloud_in = np.array(cloud_in).T
363
+ transformer = pyproj.Transformer.from_crs(crs_in, crs_out, always_xy=True)
364
+
365
+ cloud_in = transformer.transform(*cloud_in)
366
+ cloud_in = np.array(cloud_in).T
367
+
368
+ return cloud_in
369
+
370
+
371
+ def point_cloud_conversion_crs(
372
+ cloud_in: np.ndarray, crs_in: int, crs_out: int
373
+ ) -> np.ndarray:
374
+ """
375
+ Convert a point cloud from a SRS to another one.
376
+
377
+ :param cloud_in: cloud to project
378
+ :param crs_in: crs of the input SRS
379
+ :param crs_out: crs of the output SRS
380
+ :return: Projected point cloud
381
+ """
382
+ # Project point cloud between CRS (keep always_xy for compatibility)
383
+ cloud_in = np.array(cloud_in).T
384
+ transformer = pyproj.Transformer.from_crs(crs_in, crs_out, always_xy=True)
385
+
386
+ cloud_in = transformer.transform(*cloud_in)
387
+ cloud_in = np.array(cloud_in).T
388
+
389
+ return cloud_in
390
+
391
+
392
+ def get_xyz_np_array_from_dataset(
393
+ cloud_in: xr.Dataset,
394
+ ) -> Tuple[np.array, List[int]]:
395
+ """
396
+ Get a numpy array of size (nb_points, 3) with the columns
397
+ being the x, y and z coordinates from a dataset as given
398
+ in output of the triangulation.
399
+
400
+ The original epipolar geometry shape is also given in output
401
+ in order to reshape the output numpy array in its
402
+ original geometry if necessary.
403
+
404
+ :param cloud_in: input xarray dataset
405
+ :return: a tuple composed of the xyz numàpy array and its original shape
406
+ """
407
+ xyz = np.stack(
408
+ (cloud_in[cst.X].values, cloud_in[cst.Y].values, cloud_in[cst.Z]),
409
+ axis=-1,
410
+ )
411
+ xyz_shape = xyz.shape
412
+ xyz = np.reshape(xyz, (-1, 3))
413
+
414
+ return xyz, xyz_shape
415
+
416
+
417
+ def get_converted_xy_np_arrays_from_dataset(
418
+ cloud_in: xr.Dataset, epsg_out: int
419
+ ) -> Tuple[np.array, np.array]:
420
+ """
421
+ Get the x and y coordinates as numpy array
422
+ in the new referential indicated by epsg_out.
423
+ TODO: add test
424
+
425
+ :param cloud_in: input xarray dataset
426
+ :param epsg_out: target epsg code
427
+ :return: a tuple composed of the x and y numpy arrays
428
+ """
429
+ xyz, xyz_shape = get_xyz_np_array_from_dataset(cloud_in)
430
+ epsg = int(cloud_in.attrs[cst.EPSG])
431
+ xyz = point_cloud_conversion(xyz, epsg, epsg_out)
432
+ xyz = xyz.reshape(xyz_shape)
433
+ if isinstance(cloud_in, xr.Dataset):
434
+ proj_x = xyz[:, :, 0]
435
+ proj_y = xyz[:, :, 1]
436
+ else:
437
+ proj_x = xyz[:, 0]
438
+ proj_y = xyz[:, 1]
439
+ return proj_x, proj_y
440
+
441
+
442
+ def point_cloud_conversion_dataset(cloud: xr.Dataset, epsg_out: int):
443
+ """
444
+ Convert a point cloud as an xarray.Dataset to another epsg (inplace)
445
+ TODO: add test
446
+
447
+ :param cloud: cloud to project
448
+ :param epsg_out: EPSG code of the output SRS
449
+ """
450
+
451
+ if cloud.attrs[cst.EPSG] != epsg_out:
452
+ xyz, xyz_shape = get_xyz_np_array_from_dataset(cloud)
453
+
454
+ xyz = point_cloud_conversion(xyz, cloud.attrs[cst.EPSG], epsg_out)
455
+ xyz = xyz.reshape(xyz_shape)
456
+ xyz[xyz == np.inf] = np.nan
457
+ if isinstance(cloud, xr.Dataset):
458
+ # # Update cloud_in x, y and z values
459
+ cloud[cst.X].values = xyz[:, :, 0]
460
+ cloud[cst.Y].values = xyz[:, :, 1]
461
+ cloud[cst.Z].values = xyz[:, :, 2]
462
+
463
+ # # Update EPSG code
464
+ cloud.attrs[cst.EPSG] = epsg_out
465
+ elif isinstance(cloud, pandas.DataFrame):
466
+ cloud[cst.X] = xyz[:, 0]
467
+ cloud[cst.Y] = xyz[:, 1]
468
+ cloud[cst.Z] = xyz[:, 2]
469
+ cloud.attrs[cst.EPSG] = epsg_out
470
+ else:
471
+ logging.error(
472
+ "point_cloud_conversion_dataset error: point cloud is unknown"
473
+ )
474
+
475
+
476
+ def point_cloud_conversion_dataframe(
477
+ cloud: pandas.DataFrame, epsg_in: int, epsg_out: int
478
+ ):
479
+ """
480
+ Convert a point cloud as a panda.DataFrame to another epsg (inplace)
481
+
482
+ :param cloud: cloud to project
483
+ :param epsg_in: EPSG code of the input SRS
484
+ :param epsg_out: EPSG code of the output SRS
485
+ """
486
+ xyz_in = cloud.loc[:, [cst.X, cst.Y, cst.Z]].values
487
+
488
+ if xyz_in.shape[0] != 0:
489
+ xyz_in = point_cloud_conversion(xyz_in, epsg_in, epsg_out)
490
+ cloud[cst.X] = xyz_in[:, 0]
491
+ cloud[cst.Y] = xyz_in[:, 1]
492
+ cloud[cst.Z] = xyz_in[:, 2]
493
+
494
+
495
+ def ground_polygon_from_envelopes(
496
+ poly_envelope1: Polygon,
497
+ poly_envelope2: Polygon,
498
+ epsg1: int,
499
+ epsg2: int,
500
+ tgt_epsg: int = 4326,
501
+ ) -> Tuple[Polygon, Tuple[int, int, int, int]]:
502
+ """
503
+ compute the ground polygon of the intersection of two envelopes
504
+
505
+ :raise: Exception when the envelopes don't intersect one to each other
506
+
507
+ :param poly_envelope1: path to the first envelope
508
+ :param poly_envelope2: path to the second envelope
509
+ :param epsg1: EPSG code of poly_envelope1
510
+ :param epsg2: EPSG code of poly_envelope2
511
+ :param tgt_epsg: EPSG code of the new projection
512
+ (default value is set to 4326)
513
+ :return: a tuple with the shapely polygon of the intersection
514
+ and the intersection's bounding box
515
+ (described by a tuple (minx, miny, maxx, maxy))
516
+ """
517
+ # project to the correct epsg if necessary
518
+ if epsg1 != tgt_epsg:
519
+ poly_envelope1 = polygon_projection(poly_envelope1, epsg1, tgt_epsg)
520
+
521
+ if epsg2 != tgt_epsg:
522
+ poly_envelope2 = polygon_projection(poly_envelope2, epsg2, tgt_epsg)
523
+
524
+ # intersect both envelopes
525
+ if poly_envelope1.intersects(poly_envelope2):
526
+ inter = poly_envelope1.intersection(poly_envelope2)
527
+ else:
528
+ raise RuntimeError("The two envelopes do not intersect one another")
529
+
530
+ return inter, inter.bounds
531
+
532
+
533
+ # pylint: disable=too-many-positional-arguments
534
+ @cars_profile(name="Ground intersection envelopes", interval=0.5)
535
+ def ground_intersection_envelopes(
536
+ sensor1,
537
+ sensor2,
538
+ geomodel1,
539
+ geomodel2,
540
+ geometry_plugin: str,
541
+ envelope_1_path: str,
542
+ envelope_2_path: str,
543
+ out_intersect_path: str,
544
+ envelope_file_driver: str = "GeoJSON",
545
+ intersect_file_driver: str = "GPKG",
546
+ ) -> Tuple[Polygon, Tuple[int, int, int, int]]:
547
+ """
548
+ Compute ground intersection of two images with envelopes:
549
+ 1/ Create envelopes for left, right images
550
+ 2/ Read vectors and polygons with adequate EPSG codes.
551
+ 3/ compute the ground polygon of the intersection of two envelopes
552
+ 4/ Write the GPKG vector
553
+
554
+ Returns a shapely polygon and intersection bounding box
555
+
556
+ :raise: Exception when the envelopes don't intersect one to each other
557
+
558
+ :param sensor1: path to left sensor image
559
+ :param sensor2: path to right sensor image
560
+ :param geomodel1: path and attributes for left geomodel
561
+ :param geomodel2: path and attributes for right geomodel
562
+ :param geometry_plugin: name of geometry plugin to use
563
+ :param envelope_1_path: Path to the output shapefile left
564
+ :param envelope_2_path: Path to the output shapefile right
565
+ :param dem_dir: Directory containing DEM tiles
566
+ :param default_alt: Default altitude above ellipsoid
567
+ :param out_intersect_path: out vector file path to create
568
+ :param envelope_file_driver: driver used to write envelope files
569
+ :param intersect_file_driver: driver used to write the intersection file
570
+ :return: a tuple with the shapely polygon of the intersection
571
+ and the intersection's bounding box
572
+ (described by a tuple (minx, miny, maxx, maxy))
573
+ """
574
+ geometry_plugin.image_envelope(
575
+ sensor1, geomodel1, envelope_1_path, envelope_file_driver
576
+ )
577
+ geometry_plugin.image_envelope(
578
+ sensor2, geomodel2, envelope_2_path, envelope_file_driver
579
+ )
580
+
581
+ # Read vectors shapefiles
582
+ poly1, epsg1 = inputs.read_vector(envelope_1_path)
583
+ poly2, epsg2 = inputs.read_vector(envelope_2_path)
584
+
585
+ # Find polygon intersection from left, right polygons
586
+ inter_poly, (
587
+ inter_xmin,
588
+ inter_ymin,
589
+ inter_xmax,
590
+ inter_ymax,
591
+ ) = ground_polygon_from_envelopes(poly1, poly2, epsg1, epsg2, epsg1)
592
+
593
+ # Write intersection file vector from inter_poly
594
+ outputs.write_vector(
595
+ [inter_poly], out_intersect_path, epsg1, intersect_file_driver
596
+ )
597
+
598
+ return inter_poly, (inter_xmin, inter_ymin, inter_xmax, inter_ymax)
599
+
600
+
601
+ def get_ground_direction( # pylint: disable=too-many-positional-arguments
602
+ sensor,
603
+ geomodel,
604
+ geometry_plugin: str,
605
+ x_coord: float = None,
606
+ y_coord: float = None,
607
+ z0_coord: float = None,
608
+ z_coord: float = None,
609
+ ) -> np.ndarray:
610
+ """
611
+ For a given image (x,y) point, compute the direction vector to ground
612
+ The function uses the direct localization operation and makes a z
613
+ variation to get a ground direction vector.
614
+ By default, (x,y) is put at image center and z0, z at RPC geometric
615
+ model limits.
616
+
617
+ :param TODO
618
+ :param geometry_plugin: name of geometry plugin to use
619
+ :param x_coord: X Coordinate in input image sensor
620
+ :param y_coord: Y Coordinate in input image sensor
621
+ :param z0_coord: Z altitude reference coordinate
622
+ :param z_coord: Z Altitude coordinate to take the image
623
+ :param dem: path to the dem directory
624
+ :param geoid: path to the geoid file
625
+ :return: (lat0,lon0,alt0, lat,lon,alt) origin and end vector coordinates
626
+ """
627
+ # Define x, y in image center if not defined
628
+ img_size_x, img_size_y = inputs.rasterio_get_size(sensor)
629
+
630
+ if x_coord is None:
631
+ x_coord = img_size_x / 2
632
+ if y_coord is None:
633
+ y_coord = img_size_y / 2
634
+
635
+ # Check x, y to be in image
636
+ assert x_coord >= 0
637
+ assert x_coord <= img_size_x
638
+ assert y_coord >= 0
639
+ assert y_coord <= img_size_y
640
+
641
+ # Define z and z0 from img RPC constraints if not defined
642
+ (min_alt, max_alt) = utils.get_elevation_range_from_metadata(sensor)
643
+ if z0_coord is None:
644
+ z0_coord = min_alt
645
+ if z_coord is None:
646
+ z_coord = max_alt
647
+
648
+ # Check z0 and z to be in RPC constraints
649
+ assert z0_coord >= min_alt
650
+ assert z0_coord <= max_alt
651
+ assert z_coord >= min_alt
652
+ assert z_coord <= max_alt
653
+
654
+ # Get origin vector coordinate with z0 altitude
655
+ lat0, lon0, alt0 = geometry_plugin.direct_loc(
656
+ sensor,
657
+ geomodel,
658
+ np.array([x_coord]),
659
+ np.array([y_coord]),
660
+ np.array([z0_coord]),
661
+ )
662
+ # Get end vector coordinate with z altitude
663
+ lat, lon, alt = geometry_plugin.direct_loc(
664
+ sensor,
665
+ geomodel,
666
+ np.array([x_coord]),
667
+ np.array([y_coord]),
668
+ z_coord=np.array([z_coord]),
669
+ )
670
+
671
+ return np.array([lat0, lon0, alt0, lat, lon, alt])
672
+
673
+
674
+ def get_ground_angles( # pylint: disable=too-many-positional-arguments
675
+ sensor1,
676
+ sensor2,
677
+ geomodel1,
678
+ geomodel2,
679
+ geometry_plugin,
680
+ x1_coord: float = None,
681
+ y1_coord: float = None,
682
+ z1_0_coord: float = None,
683
+ z1_coord: float = None,
684
+ x2_coord: float = None,
685
+ y2_coord: float = None,
686
+ z2_0_coord: float = None,
687
+ z2_coord: float = None,
688
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
689
+ """
690
+ For a given image (x,y) point, compute the Azimuth angle,
691
+ Elevation angle (not the altitude !) and Range from Ground z0 perspective
692
+ for both stereo image (img1: left and img2: right)
693
+
694
+ Calculate also the convergence angle between the two satellites from ground.
695
+
696
+ The function use get_ground_direction function to have coordinates of
697
+ ground direction vector and compute angles and range.
698
+
699
+ Ref: Jeong, Jaehoon. (2017).
700
+ IMAGING GEOMETRY AND POSITIONING ACCURACY OF DUAL SATELLITE STEREO IMAGES:
701
+ A REVIEW. ISPRS Annals of Photogrammetry, Remote Sensing and Spatial
702
+ Information Sciences.
703
+ IV-2/W4. 235-242. 10.5194/isprs-annals-IV-2-W4-235-2017.
704
+
705
+ Perspectives: get bisector elevation (BIE), and asymmetry angle
706
+
707
+ :param TODO
708
+ :param geometry_plugin: name of geometry plugin to use
709
+ :param x1_coord: X Coordinate in input left image1 sensor
710
+ :param y1_coord: Y Coordinate in input left image1 sensor
711
+ :param z1_0_coord: Left image1 Z altitude origin coordinate for ground
712
+ direction vector
713
+ :param z1_coord: Left image1 Z altitude end coordinate for ground
714
+ direction vector
715
+ :param x2_coord: X Coordinate in input right image2 sensor
716
+ :param y2_coord: Y Coordinate in input right image2 sensor
717
+ :param z2_0_coord: Right image2 Z altitude origin coordinate for ground
718
+ direction vector
719
+ :param z2_coord: Right image2 Z altitude end coordinate for ground
720
+ direction vector
721
+ :return: Left Azimuth, Left Elevation Angle,
722
+ Right Azimuth, Right Elevation Angle, Convergence Angle
723
+ """
724
+ # TODO remove ? unused
725
+
726
+ # Get image1 <-> satellite vector from image1 metadata geometric model
727
+ lat1_0, lon1_0, alt1_0, lat1, lon1, alt1 = get_ground_direction(
728
+ sensor1,
729
+ geomodel1,
730
+ geometry_plugin,
731
+ x1_coord,
732
+ y1_coord,
733
+ z1_0_coord,
734
+ z1_coord,
735
+ )
736
+ # Get East North Up vector for left image1
737
+ x1_e, y1_n, y1_u = enu1 = geo_to_enu(
738
+ lat1, lon1, alt1, lat1_0, lon1_0, alt1_0
739
+ )
740
+ # Convert vector to Azimuth, Elevation, Range (unused)
741
+ az1, elev_angle1, __ = enu_to_aer(x1_e, y1_n, y1_u)
742
+
743
+ # Get image2 <-> satellite vector from image2 metadata geometric model
744
+ lat2_0, lon2_0, alt2_0, lat2, lon2, alt2 = get_ground_direction(
745
+ sensor2,
746
+ geomodel2,
747
+ geometry_plugin,
748
+ x2_coord,
749
+ y2_coord,
750
+ z2_0_coord,
751
+ z2_coord,
752
+ )
753
+ # Get East North Up vector for right image2
754
+ x2_e, y2_n, y2_u = enu2 = geo_to_enu(
755
+ lat2, lon2, alt2, lat2_0, lon2_0, alt2_0
756
+ )
757
+ # Convert ENU to Azimuth, Elevation, Range (unused)
758
+ az2, elev_angle2, __ = enu_to_aer(x2_e, y2_n, y2_u)
759
+
760
+ # Get convergence angle from two enu vectors.
761
+ convergence_angle = np.degrees(utils.angle_vectors(enu1, enu2))
762
+
763
+ return az1, elev_angle1, az2, elev_angle2, convergence_angle
764
+
765
+
766
+ def get_output_crs(epsg, out_conf):
767
+ """
768
+ Détermine le CRS de sortie en fonction de la config.
769
+ """
770
+ geoid = out_conf.get("geoid")
771
+ crs_epsg = CRS(f"EPSG:{epsg}")
772
+
773
+ if len(crs_epsg.axis_info) != 2:
774
+ return crs_epsg # the user himself set a 3D CRS
775
+
776
+ geoid_is_path = isinstance(geoid, str)
777
+
778
+ if geoid_is_path: # user given geoid
779
+ vepsg = guess_vcrs_from_file_name(geoid)
780
+ if vepsg is None:
781
+ custom_wkt = (
782
+ 'VERTCRS["Custom geoid height",'
783
+ + f' VDATUM["Custom geoid model (file: {geoid})"],'
784
+ + " CS[vertical,1],"
785
+ + ' AXIS["gravity-related height (h)", up],'
786
+ + ' LENGTHUNIT["metre", 1, ID["EPSG", 9001]]'
787
+ "]"
788
+ )
789
+ logging.warning(
790
+ "Could not create a known VCRS from the geoid file."
791
+ )
792
+ return CRS.from_wkt(
793
+ f'COMPOUNDCRS["EPSG:{epsg} + Custom geoid height",'
794
+ f" {crs_epsg.to_wkt()},"
795
+ f" {custom_wkt}]"
796
+ )
797
+ # a vepsg was found using the geoid file
798
+ return CRS(f"EPSG:{epsg}+{vepsg}")
799
+
800
+ if geoid: # geoid == True
801
+ return CRS(f"EPSG:{epsg}+5773")
802
+
803
+ # geoid == False
804
+ wgs84_wkt = (
805
+ 'VERTCRS["WGS 84 ellipsoidal height",'
806
+ + ' VDATUM["WGS 84"],'
807
+ + " CS[vertical,1],"
808
+ + ' AXIS["ellipsoidal height (h)", up],'
809
+ + ' LENGTHUNIT["metre", 1, ID["EPSG", 9001]]'
810
+ "]"
811
+ )
812
+ logging.warning("The output VCRS is WGS84.")
813
+
814
+ return CRS.from_wkt(
815
+ f'COMPOUNDCRS["EPSG:{epsg} + WGS84 ellipsoidal height",'
816
+ f" {crs_epsg.to_wkt()},"
817
+ f" {wgs84_wkt}]"
818
+ )
819
+
820
+
821
+ def guess_vcrs_from_file_name(filepath):
822
+ """
823
+ Tries to detect the geoid's EPSG from the file name
824
+ """
825
+ filename = os.path.basename(filepath).lower()
826
+
827
+ known_models = {
828
+ "egm96": 5773, # EGM96 height
829
+ "egm_96": 5773, # alias
830
+ "egm 96": 5773, # alias
831
+ "egm1996": 5773, # alias
832
+ "egm08": 3855, # EGM2008 height
833
+ "egm_08": 3855, # alias
834
+ "egm 08": 3855, # alias
835
+ "egm2008": 3855, # alias
836
+ }
837
+
838
+ for key, vepsg in known_models.items():
839
+ if key in filename:
840
+ return vepsg
841
+
842
+ # aucun match connu
843
+ return None