cars 1.0.0rc1__cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.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.

Potentially problematic release.


This version of cars might be problematic. Click here for more details.

Files changed (200) hide show
  1. cars/__init__.py +74 -0
  2. cars/applications/__init__.py +37 -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 +104 -0
  8. cars/applications/auxiliary_filling/auxiliary_filling_algo.py +475 -0
  9. cars/applications/auxiliary_filling/auxiliary_filling_from_sensors_app.py +630 -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 +42 -0
  14. cars/applications/dem_generation/bulldozer_dem_app.py +655 -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 +1460 -0
  28. cars/applications/dense_matching/cpp/__init__.py +0 -0
  29. cars/applications/dense_matching/cpp/dense_matching_cpp.cpython-312-i386-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 +588 -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 +270 -0
  53. cars/applications/dsm_filling/bulldozer_config/base_config.yaml +44 -0
  54. cars/applications/dsm_filling/bulldozer_filling_app.py +279 -0
  55. cars/applications/dsm_filling/exogenous_filling_app.py +333 -0
  56. cars/applications/grid_generation/__init__.py +30 -0
  57. cars/applications/grid_generation/abstract_grid_generation_app.py +142 -0
  58. cars/applications/grid_generation/epipolar_grid_generation_app.py +327 -0
  59. cars/applications/grid_generation/grid_correction_app.py +496 -0
  60. cars/applications/grid_generation/grid_generation_algo.py +388 -0
  61. cars/applications/grid_generation/grid_generation_constants.py +46 -0
  62. cars/applications/grid_generation/transform_grid.py +88 -0
  63. cars/applications/ground_truth_reprojection/__init__.py +30 -0
  64. cars/applications/ground_truth_reprojection/abstract_ground_truth_reprojection_app.py +137 -0
  65. cars/applications/ground_truth_reprojection/direct_localization_app.py +629 -0
  66. cars/applications/ground_truth_reprojection/ground_truth_reprojection_algo.py +275 -0
  67. cars/applications/point_cloud_outlier_removal/__init__.py +30 -0
  68. cars/applications/point_cloud_outlier_removal/abstract_outlier_removal_app.py +385 -0
  69. cars/applications/point_cloud_outlier_removal/outlier_removal_algo.py +392 -0
  70. cars/applications/point_cloud_outlier_removal/outlier_removal_constants.py +43 -0
  71. cars/applications/point_cloud_outlier_removal/small_components_app.py +527 -0
  72. cars/applications/point_cloud_outlier_removal/statistical_app.py +531 -0
  73. cars/applications/rasterization/__init__.py +30 -0
  74. cars/applications/rasterization/abstract_pc_rasterization_app.py +183 -0
  75. cars/applications/rasterization/rasterization_algo.py +534 -0
  76. cars/applications/rasterization/rasterization_constants.py +38 -0
  77. cars/applications/rasterization/rasterization_wrappers.py +634 -0
  78. cars/applications/rasterization/simple_gaussian_app.py +1152 -0
  79. cars/applications/resampling/__init__.py +28 -0
  80. cars/applications/resampling/abstract_resampling_app.py +187 -0
  81. cars/applications/resampling/bicubic_resampling_app.py +762 -0
  82. cars/applications/resampling/resampling_algo.py +614 -0
  83. cars/applications/resampling/resampling_constants.py +36 -0
  84. cars/applications/resampling/resampling_wrappers.py +309 -0
  85. cars/applications/sparse_matching/__init__.py +30 -0
  86. cars/applications/sparse_matching/abstract_sparse_matching_app.py +498 -0
  87. cars/applications/sparse_matching/sift_app.py +735 -0
  88. cars/applications/sparse_matching/sparse_matching_algo.py +360 -0
  89. cars/applications/sparse_matching/sparse_matching_constants.py +68 -0
  90. cars/applications/sparse_matching/sparse_matching_wrappers.py +238 -0
  91. cars/applications/triangulation/__init__.py +32 -0
  92. cars/applications/triangulation/abstract_triangulation_app.py +227 -0
  93. cars/applications/triangulation/line_of_sight_intersection_app.py +1243 -0
  94. cars/applications/triangulation/pc_transform.py +552 -0
  95. cars/applications/triangulation/triangulation_algo.py +371 -0
  96. cars/applications/triangulation/triangulation_constants.py +38 -0
  97. cars/applications/triangulation/triangulation_wrappers.py +259 -0
  98. cars/bundleadjustment.py +757 -0
  99. cars/cars.py +177 -0
  100. cars/conf/__init__.py +23 -0
  101. cars/conf/geoid/egm96.grd +0 -0
  102. cars/conf/geoid/egm96.grd.hdr +15 -0
  103. cars/conf/input_parameters.py +156 -0
  104. cars/conf/mask_cst.py +35 -0
  105. cars/core/__init__.py +23 -0
  106. cars/core/cars_logging.py +402 -0
  107. cars/core/constants.py +191 -0
  108. cars/core/constants_disparity.py +50 -0
  109. cars/core/datasets.py +140 -0
  110. cars/core/geometry/__init__.py +27 -0
  111. cars/core/geometry/abstract_geometry.py +1119 -0
  112. cars/core/geometry/shareloc_geometry.py +598 -0
  113. cars/core/inputs.py +568 -0
  114. cars/core/outputs.py +176 -0
  115. cars/core/preprocessing.py +722 -0
  116. cars/core/projection.py +843 -0
  117. cars/core/roi_tools.py +215 -0
  118. cars/core/tiling.py +774 -0
  119. cars/core/utils.py +164 -0
  120. cars/data_structures/__init__.py +23 -0
  121. cars/data_structures/cars_dataset.py +1541 -0
  122. cars/data_structures/cars_dict.py +74 -0
  123. cars/data_structures/corresponding_tiles_tools.py +186 -0
  124. cars/data_structures/dataframe_converter.py +185 -0
  125. cars/data_structures/format_transformation.py +297 -0
  126. cars/devibrate.py +689 -0
  127. cars/extractroi.py +264 -0
  128. cars/orchestrator/__init__.py +23 -0
  129. cars/orchestrator/achievement_tracker.py +125 -0
  130. cars/orchestrator/cluster/__init__.py +37 -0
  131. cars/orchestrator/cluster/abstract_cluster.py +244 -0
  132. cars/orchestrator/cluster/abstract_dask_cluster.py +375 -0
  133. cars/orchestrator/cluster/dask_cluster_tools.py +103 -0
  134. cars/orchestrator/cluster/dask_config/README.md +94 -0
  135. cars/orchestrator/cluster/dask_config/dask.yaml +21 -0
  136. cars/orchestrator/cluster/dask_config/distributed.yaml +70 -0
  137. cars/orchestrator/cluster/dask_config/jobqueue.yaml +26 -0
  138. cars/orchestrator/cluster/dask_config/reference_confs/dask-schema.yaml +137 -0
  139. cars/orchestrator/cluster/dask_config/reference_confs/dask.yaml +26 -0
  140. cars/orchestrator/cluster/dask_config/reference_confs/distributed-schema.yaml +1009 -0
  141. cars/orchestrator/cluster/dask_config/reference_confs/distributed.yaml +273 -0
  142. cars/orchestrator/cluster/dask_config/reference_confs/jobqueue.yaml +212 -0
  143. cars/orchestrator/cluster/dask_jobqueue_utils.py +204 -0
  144. cars/orchestrator/cluster/local_dask_cluster.py +116 -0
  145. cars/orchestrator/cluster/log_wrapper.py +1075 -0
  146. cars/orchestrator/cluster/mp_cluster/__init__.py +27 -0
  147. cars/orchestrator/cluster/mp_cluster/mp_factorizer.py +212 -0
  148. cars/orchestrator/cluster/mp_cluster/mp_objects.py +535 -0
  149. cars/orchestrator/cluster/mp_cluster/mp_tools.py +93 -0
  150. cars/orchestrator/cluster/mp_cluster/mp_wrapper.py +505 -0
  151. cars/orchestrator/cluster/mp_cluster/multiprocessing_cluster.py +873 -0
  152. cars/orchestrator/cluster/mp_cluster/multiprocessing_profiler.py +399 -0
  153. cars/orchestrator/cluster/pbs_dask_cluster.py +207 -0
  154. cars/orchestrator/cluster/sequential_cluster.py +139 -0
  155. cars/orchestrator/cluster/slurm_dask_cluster.py +234 -0
  156. cars/orchestrator/orchestrator.py +905 -0
  157. cars/orchestrator/orchestrator_constants.py +29 -0
  158. cars/orchestrator/registry/__init__.py +23 -0
  159. cars/orchestrator/registry/abstract_registry.py +143 -0
  160. cars/orchestrator/registry/compute_registry.py +106 -0
  161. cars/orchestrator/registry/id_generator.py +116 -0
  162. cars/orchestrator/registry/replacer_registry.py +213 -0
  163. cars/orchestrator/registry/saver_registry.py +363 -0
  164. cars/orchestrator/registry/unseen_registry.py +118 -0
  165. cars/orchestrator/tiles_profiler.py +279 -0
  166. cars/pipelines/__init__.py +26 -0
  167. cars/pipelines/conf_resolution/conf_final_resolution.yaml +5 -0
  168. cars/pipelines/conf_resolution/conf_first_resolution.yaml +2 -0
  169. cars/pipelines/conf_resolution/conf_intermediate_resolution.yaml +2 -0
  170. cars/pipelines/default/__init__.py +26 -0
  171. cars/pipelines/default/default_pipeline.py +786 -0
  172. cars/pipelines/parameters/__init__.py +0 -0
  173. cars/pipelines/parameters/advanced_parameters.py +417 -0
  174. cars/pipelines/parameters/advanced_parameters_constants.py +69 -0
  175. cars/pipelines/parameters/application_parameters.py +71 -0
  176. cars/pipelines/parameters/depth_map_inputs.py +0 -0
  177. cars/pipelines/parameters/dsm_inputs.py +918 -0
  178. cars/pipelines/parameters/dsm_inputs_constants.py +25 -0
  179. cars/pipelines/parameters/output_constants.py +52 -0
  180. cars/pipelines/parameters/output_parameters.py +454 -0
  181. cars/pipelines/parameters/sensor_inputs.py +842 -0
  182. cars/pipelines/parameters/sensor_inputs_constants.py +49 -0
  183. cars/pipelines/parameters/sensor_loaders/__init__.py +29 -0
  184. cars/pipelines/parameters/sensor_loaders/basic_classif_loader.py +86 -0
  185. cars/pipelines/parameters/sensor_loaders/basic_image_loader.py +98 -0
  186. cars/pipelines/parameters/sensor_loaders/pivot_classif_loader.py +90 -0
  187. cars/pipelines/parameters/sensor_loaders/pivot_image_loader.py +105 -0
  188. cars/pipelines/parameters/sensor_loaders/sensor_loader.py +93 -0
  189. cars/pipelines/parameters/sensor_loaders/sensor_loader_template.py +71 -0
  190. cars/pipelines/parameters/sensor_loaders/slurp_classif_loader.py +86 -0
  191. cars/pipelines/pipeline.py +119 -0
  192. cars/pipelines/pipeline_constants.py +31 -0
  193. cars/pipelines/pipeline_template.py +139 -0
  194. cars/pipelines/unit/__init__.py +26 -0
  195. cars/pipelines/unit/unit_pipeline.py +2850 -0
  196. cars/starter.py +167 -0
  197. cars-1.0.0rc1.dist-info/METADATA +292 -0
  198. cars-1.0.0rc1.dist-info/RECORD +200 -0
  199. cars-1.0.0rc1.dist-info/WHEEL +6 -0
  200. cars-1.0.0rc1.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,1119 @@
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
+ # pylint: disable=C0302
23
+
24
+ """
25
+ this module contains the abstract geometry class to use in the
26
+ geometry plugins
27
+ """
28
+ import logging
29
+ import os
30
+ from abc import ABCMeta, abstractmethod
31
+ from typing import Dict, List, Tuple, Union
32
+
33
+ import numpy as np
34
+ import rasterio as rio
35
+ import xarray as xr
36
+ from affine import Affine
37
+ from json_checker import And, Checker
38
+ from rasterio.enums import Resampling
39
+ from rasterio.warp import reproject
40
+ from scipy import interpolate
41
+ from scipy.interpolate import LinearNDInterpolator
42
+ from shapely.geometry import Polygon
43
+ from shareloc import proj_utils
44
+ from shareloc.geofunctions.rectification_grid import RectificationGrid
45
+
46
+ from cars.core import constants as cst
47
+ from cars.core import inputs, outputs, projection
48
+ from cars.core.utils import safe_makedirs
49
+ from cars.data_structures import cars_dataset
50
+ from cars.orchestrator.cluster.log_wrapper import cars_profile
51
+
52
+
53
+ class AbstractGeometry(metaclass=ABCMeta): # pylint: disable=R0902
54
+ """
55
+ AbstractGeometry
56
+ """
57
+
58
+ available_plugins: Dict = {}
59
+
60
+ def __new__(
61
+ cls,
62
+ geometry_plugin_conf=None,
63
+ pairs_for_roi=None,
64
+ scaling_coeff=1,
65
+ **kwargs,
66
+ ):
67
+ """
68
+ Return the required plugin
69
+ :raises:
70
+ - KeyError when the required plugin is not registered
71
+
72
+ :param geometry_plugin_conf: plugin name or plugin configuration
73
+ to instantiate
74
+ :type geometry_plugin_conf: str or dict
75
+ :param scaling_coeff: scaling factor for resolution
76
+ :type scaling_coeff: float
77
+ :return: a geometry_plugin object
78
+ """
79
+ if geometry_plugin_conf is not None:
80
+ if isinstance(geometry_plugin_conf, str):
81
+ geometry_plugin = geometry_plugin_conf
82
+ elif isinstance(geometry_plugin_conf, dict):
83
+ geometry_plugin = geometry_plugin_conf.get(
84
+ "plugin_name", "SharelocGeometry"
85
+ )
86
+ else:
87
+ raise RuntimeError("Not a supported type")
88
+
89
+ if geometry_plugin not in cls.available_plugins:
90
+ logging.error(
91
+ "No geometry plugin named {} registered".format(
92
+ geometry_plugin
93
+ )
94
+ )
95
+ raise KeyError(
96
+ "No geometry plugin named {} registered".format(
97
+ geometry_plugin
98
+ )
99
+ )
100
+
101
+ logging.info(
102
+ "The AbstractGeometry {} plugin will be used".format(
103
+ geometry_plugin
104
+ )
105
+ )
106
+
107
+ return super(AbstractGeometry, cls).__new__(
108
+ cls.available_plugins[geometry_plugin]
109
+ )
110
+ return super().__new__(cls)
111
+
112
+ def __init__( # pylint: disable=too-many-positional-arguments
113
+ self,
114
+ geometry_plugin_conf,
115
+ dem=None,
116
+ geoid=None,
117
+ default_alt=None,
118
+ pairs_for_roi=None,
119
+ scaling_coeff=1,
120
+ output_dem_dir=None,
121
+ **kwargs,
122
+ ):
123
+ self.scaling_coeff = scaling_coeff
124
+
125
+ self.used_config = self.check_conf(geometry_plugin_conf)
126
+
127
+ self.plugin_name = self.used_config["plugin_name"]
128
+ self.interpolator = self.used_config["interpolator"]
129
+ self.dem_roi_margin = self.used_config["dem_roi_margin"]
130
+ self.dem = None
131
+ self.dem_roi = None
132
+ self.dem_roi_epsg = None
133
+ self.geoid = geoid
134
+ self.default_alt = default_alt
135
+ self.elevation = default_alt
136
+ # a margin is needed for cubic interpolation
137
+ self.rectification_grid_margin = 0
138
+ if self.interpolator == "cubic":
139
+ self.rectification_grid_margin = 5
140
+ self.kwargs = kwargs
141
+
142
+ # compute roi only when generating geometry object with dem
143
+ if dem is not None:
144
+ self.dem = dem
145
+ self.default_alt = self.get_dem_median_value()
146
+ self.elevation = self.default_alt
147
+ logging.info(
148
+ "Median value of DEM ({}) will be used as default_alt".format(
149
+ self.default_alt
150
+ )
151
+ )
152
+ if pairs_for_roi is not None:
153
+ self.dem_roi_epsg = inputs.rasterio_get_epsg(dem)
154
+ self.dem_roi = self.get_roi(
155
+ pairs_for_roi,
156
+ self.dem_roi_epsg,
157
+ z_min=-1000,
158
+ z_max=9000,
159
+ linear_margin=self.dem_roi_margin[0],
160
+ constant_margin=self.dem_roi_margin[1],
161
+ )
162
+ if output_dem_dir is not None:
163
+ self.dem = self.extend_dem_to_roi(dem, output_dem_dir)
164
+
165
+ def get_dem_median_value(self):
166
+ """
167
+ Compute dem median value
168
+ :param dem: path of DEM
169
+ """
170
+ with rio.open(self.dem) as dem_file:
171
+ dem_data = dem_file.read(1)
172
+ median_value = np.nanmedian(dem_data)
173
+ median_value = float(median_value)
174
+ return median_value
175
+
176
+ def get_roi( # pylint: disable=too-many-positional-arguments
177
+ self,
178
+ pairs_for_roi,
179
+ epsg,
180
+ z_min=0,
181
+ z_max=0,
182
+ linear_margin=0,
183
+ constant_margin=0.012,
184
+ ):
185
+ """
186
+ Compute region of interest for intersection of DEM
187
+
188
+ :param pairs_for_roi: list of pairs of images and geomodels
189
+ :type pairs_for_roi: List[(str, dict, str, dict)]
190
+ :param dem_epsg: output EPSG code for ROI
191
+ :type dem_epsg: int
192
+ :param linear_margin: margin for ROI (factor of initial ROI size)
193
+ :type linear_margin: float
194
+ :param constant_margin: margin for ROI in degrees
195
+ :type constant_margin: float
196
+ """
197
+ coords_list = []
198
+ z_min = np.array(z_min)
199
+ z_max = np.array(z_max)
200
+ for image1, geomodel1, image2, geomodel2 in pairs_for_roi:
201
+ # Footprint of left image with altitude z_min
202
+ coords_list.extend(
203
+ self.image_envelope(
204
+ image1["bands"]["b0"]["path"], geomodel1, elevation=z_min
205
+ )
206
+ )
207
+ # Footprint of left image with altitude z_max
208
+ coords_list.extend(
209
+ self.image_envelope(
210
+ image1["bands"]["b0"]["path"], geomodel1, elevation=z_max
211
+ )
212
+ )
213
+ # Footprint of right image with altitude z_min
214
+ coords_list.extend(
215
+ self.image_envelope(
216
+ image2["bands"]["b0"]["path"], geomodel2, elevation=z_min
217
+ )
218
+ )
219
+ # Footprint of right image with altitude z_max
220
+ coords_list.extend(
221
+ self.image_envelope(
222
+ image2["bands"]["b0"]["path"], geomodel2, elevation=z_max
223
+ )
224
+ )
225
+ lon_list, lat_list = list(zip(*coords_list)) # noqa: B905
226
+ roi = [
227
+ min(lon_list) - constant_margin,
228
+ min(lat_list) - constant_margin,
229
+ max(lon_list) + constant_margin,
230
+ max(lat_list) + constant_margin,
231
+ ]
232
+ points = np.array(
233
+ [
234
+ (roi[0], roi[1], 0),
235
+ (roi[2], roi[3], 0),
236
+ (roi[0], roi[1], 0),
237
+ (roi[2], roi[3], 0),
238
+ ]
239
+ )
240
+ new_points = projection.point_cloud_conversion(points, 4326, epsg)
241
+ roi = [
242
+ min(new_points[:, 0]),
243
+ min(new_points[:, 1]),
244
+ max(new_points[:, 0]),
245
+ max(new_points[:, 1]),
246
+ ]
247
+
248
+ lon_size = roi[2] - roi[0]
249
+ lat_size = roi[3] - roi[1]
250
+
251
+ roi[0] -= linear_margin * lon_size
252
+ roi[1] -= linear_margin * lat_size
253
+ roi[2] += linear_margin * lon_size
254
+ roi[3] += linear_margin * lat_size
255
+
256
+ return roi
257
+
258
+ def extend_dem_to_roi(self, dem, output_dem_dir):
259
+ """
260
+ Extend the size of the dem to the required ROI and fill
261
+ :param dem: path to the input DEM
262
+ :param output_dem_dir: path to write the output extended DEM
263
+ """
264
+ with rio.open(dem) as in_dem:
265
+ src_dem = in_dem.read(1)
266
+ metadata = in_dem.meta
267
+ src_transform = in_dem.transform
268
+ crs = in_dem.crs
269
+ bounds = in_dem.bounds
270
+
271
+ logging.info(
272
+ "DEM bounds : {}, {}, {}, {}".format(
273
+ bounds.left, bounds.top, bounds.right, bounds.bottom
274
+ )
275
+ )
276
+ logging.info(
277
+ "ROI bounds : {}, {}, {}, {}".format(
278
+ self.dem_roi[0],
279
+ self.dem_roi[1],
280
+ self.dem_roi[2],
281
+ self.dem_roi[3],
282
+ )
283
+ )
284
+
285
+ # Longitude
286
+ lon_res = src_transform[0]
287
+ lon_shift = (self.dem_roi[0] - bounds.left) / lon_res
288
+ dst_width = int((self.dem_roi[2] - self.dem_roi[0]) / abs(lon_res)) + 1
289
+ # Latitude
290
+ lat_res = src_transform[4]
291
+ lat_shift = (self.dem_roi[3] - bounds.top) / lat_res
292
+ dst_height = int((self.dem_roi[3] - self.dem_roi[1]) / abs(lat_res)) + 1
293
+
294
+ shift = Affine.translation(lon_shift, lat_shift)
295
+ dst_transform = src_transform * shift
296
+ dst_dem = np.zeros((dst_height, dst_width))
297
+
298
+ reproject(
299
+ source=src_dem,
300
+ destination=dst_dem,
301
+ src_transform=src_transform,
302
+ src_crs=crs,
303
+ dst_transform=dst_transform,
304
+ dst_crs=crs,
305
+ resampling=Resampling.bilinear,
306
+ )
307
+ # Fill nodata
308
+ dst_dem = rio.fill.fillnodata(
309
+ dst_dem,
310
+ mask=~(dst_dem == 0),
311
+ )
312
+ metadata["transform"] = dst_transform
313
+ metadata["height"] = dst_height
314
+ metadata["width"] = dst_width
315
+ metadata["driver"] = "GTiff"
316
+
317
+ out_dem_path = os.path.join(output_dem_dir, "initial_elevation.tif")
318
+
319
+ with rio.open(out_dem_path, "w", **metadata) as dst:
320
+ dst.write(dst_dem, 1)
321
+
322
+ return out_dem_path
323
+
324
+ @classmethod
325
+ def register_subclass(cls, short_name: str):
326
+ """
327
+ Allows to register the subclass with its short name
328
+ :param short_name: the subclass to be registered
329
+ :type short_name: string
330
+ """
331
+
332
+ def decorator(subclass):
333
+ """
334
+ Registers the subclass in the available methods
335
+ :param subclass: the subclass to be registered
336
+ :type subclass: object
337
+ """
338
+ cls.available_plugins[short_name] = subclass
339
+ return subclass
340
+
341
+ return decorator
342
+
343
+ def check_conf(self, conf):
344
+ """
345
+ Check configuration
346
+
347
+ :param conf: configuration to check
348
+ :type conf: str or dict
349
+
350
+ :return: full dict
351
+ :rtype: dict
352
+
353
+ """
354
+
355
+ if conf is None:
356
+ raise RuntimeError("Geometry plugin configuration is None")
357
+
358
+ overloaded_conf = {}
359
+
360
+ if isinstance(conf, str):
361
+ conf = {"plugin_name": conf}
362
+
363
+ # overload conf
364
+ overloaded_conf["plugin_name"] = conf.get(
365
+ "plugin_name", "SharelocGeometry"
366
+ )
367
+ overloaded_conf["interpolator"] = conf.get("interpolator", "cubic")
368
+ overloaded_conf["dem_roi_margin"] = conf.get(
369
+ "dem_roi_margin", [0.75, 0.02]
370
+ )
371
+
372
+ geometry_schema = {
373
+ "plugin_name": str,
374
+ "interpolator": And(str, lambda x: x in ["cubic", "linear"]),
375
+ "dem_roi_margin": [float],
376
+ }
377
+
378
+ # Check conf
379
+ checker = Checker(geometry_schema)
380
+ checker.validate(overloaded_conf)
381
+
382
+ return overloaded_conf
383
+
384
+ @abstractmethod
385
+ def triangulate( # pylint: disable=too-many-positional-arguments
386
+ self,
387
+ sensor1,
388
+ sensor2,
389
+ geomodel1,
390
+ geomodel2,
391
+ mode: str,
392
+ matches: Union[xr.Dataset, np.ndarray],
393
+ grid1: str,
394
+ grid2: str,
395
+ roi_key: Union[None, str] = None,
396
+ interpolation_method=None,
397
+ ) -> np.ndarray:
398
+ """
399
+ Performs triangulation from cars disparity or matches dataset
400
+
401
+ :param sensor1: path to left sensor image
402
+ :param sensor2: path to right sensor image
403
+ :param geomodel1: path and attributes for left geomodel
404
+ :param geomodel2: path and attributes for right geomodel
405
+ :param mode: triangulation mode
406
+ (constants.DISP_MODE or constants.MATCHES)
407
+ :param matches: cars disparity dataset or matches as numpy array
408
+ :param grid1: path to epipolar grid of img1
409
+ :param grid2: path to epipolar grid of image 2
410
+ :param roi_key: dataset roi to use
411
+ (can be cst.ROI or cst.ROI_WITH_MARGINS)
412
+ :return: the long/lat/height numpy array in output of the triangulation
413
+ """
414
+
415
+ @staticmethod
416
+ @abstractmethod
417
+ def check_product_consistency(sensor: str, geomodel: str, **kwargs) -> bool:
418
+ """
419
+ Test if the product is readable by the geometry plugin
420
+
421
+ :param sensor: path to sensor image
422
+ :param geomodel: path to geomodel
423
+ :return: True if the products are readable, False otherwise
424
+ """
425
+
426
+ # pylint: disable=too-many-positional-arguments
427
+ @abstractmethod
428
+ def generate_epipolar_grids(
429
+ self, sensor1, sensor2, geomodel1, geomodel2, epipolar_step: int = 30
430
+ ) -> Tuple[
431
+ np.ndarray, np.ndarray, List[float], List[float], List[int], float
432
+ ]:
433
+ """
434
+ Computes the left and right epipolar grids
435
+
436
+ :param sensor1: path to left sensor image
437
+ :param sensor2: path to right sensor image
438
+ :param geomodel1: path and attributes for left geomodel
439
+ :param geomodel2: path and attributes for right geomodel
440
+ :param epipolar_step: step to use to construct the epipolar grids
441
+ :return: Tuple composed of :
442
+
443
+ - the left epipolar grid as a numpy array
444
+ - the right epipolar grid as a numpy array
445
+ - the left grid origin as a list of float
446
+ - the left grid spacing as a list of float
447
+ - the epipolar image size as a list of int \
448
+ (x-axis size is given with the index 0, y-axis size with index 1)
449
+ - the disparity to altitude ratio as a float
450
+ """
451
+
452
+ def load_geomodel(self, geomodel: dict) -> dict:
453
+ """
454
+ By default return the geomodel
455
+ This method can be overloaded by plugins to load geomodel in memory
456
+
457
+ :param geomodel
458
+ """
459
+ return geomodel
460
+
461
+ # pylint: disable=too-many-positional-arguments
462
+ def matches_to_sensor_coords(
463
+ self,
464
+ grid1: Union[str, cars_dataset.CarsDataset, RectificationGrid],
465
+ grid2: Union[str, cars_dataset.CarsDataset, RectificationGrid],
466
+ matches: np.ndarray,
467
+ matches_type: str,
468
+ matches_msk: np.ndarray = None,
469
+ ul_matches_shift: Tuple[int, int] = None,
470
+ interpolation_method=None,
471
+ ) -> Tuple[np.ndarray, np.ndarray]:
472
+ """
473
+ Convert matches (sparse or dense matches) given in epipolar
474
+ coordinates to sensor coordinates. This function is available for
475
+ plugins if it requires matches in sensor coordinates to perform
476
+ the triangulation.
477
+
478
+ This function returns a tuple composed of the matches left and right
479
+ sensor coordinates as numpy arrays. For each original image, the sensor
480
+ coordinates are arranged as follows :
481
+
482
+ - if the matches are a vector of matching points: a numpy array of\
483
+ size [number of matches, 2].\
484
+ The last index indicates the 'x' coordinate(last index set to 0) or\
485
+ the 'y' coordinate (last index set to 1).
486
+ - if matches is a cars disparity dataset: a numpy array of size \
487
+ [nb_epipolar_line, nb_epipolar_col, 2]. Where\
488
+ [nb_epipolar_line, nb_epipolar_col] is the size of the disparity \
489
+ map. The last index indicates the 'x' coordinate (last index set \
490
+ to 0) or the 'y' coordinate (last index set to 1).
491
+
492
+ :param grid1: path to epipolar grid of image 1
493
+ :param grid2: path to epipolar grid of image 2
494
+ :param matches: cars disparity dataset or matches as numpy array
495
+ :param matches_type: matches type (cst.DISP_MODE or cst.MATCHES)
496
+ :param matches_msk: matches mask to provide for cst.DISP_MODE
497
+ :param ul_matches_shift: coordinates (x, y) of the upper left corner of
498
+ the matches map (for cst.DISP_MODE) in the original epipolar
499
+ geometry (use this if the map have been cropped)
500
+ :return: a tuple of numpy array. The first array corresponds to the
501
+ left matches in sensor coordinates, the second one is the right
502
+ matches in sensor coordinates.
503
+ """
504
+ vec_epi_pos_left = None
505
+ vec_epi_pos_right = None
506
+
507
+ if matches_type == cst.MATCHES_MODE:
508
+ # retrieve left and right matches
509
+ vec_epi_pos_left = matches[:, 0:2]
510
+ vec_epi_pos_right = matches[:, 2:4]
511
+
512
+ elif matches_type == cst.DISP_MODE:
513
+ if matches_msk is None:
514
+ logging.error("No disparity mask given in input")
515
+ raise RuntimeError("No disparity mask given in input")
516
+
517
+ if ul_matches_shift is None:
518
+ ul_matches_shift = (0, 0)
519
+
520
+ # convert disparity to matches
521
+ epi_pos_left_y, epi_pos_left_x = np.mgrid[
522
+ ul_matches_shift[1] : ul_matches_shift[1] + matches.shape[0],
523
+ ul_matches_shift[0] : ul_matches_shift[0] + matches.shape[1],
524
+ ]
525
+
526
+ epi_pos_left_x = epi_pos_left_x.astype(np.float64)
527
+ epi_pos_left_y = epi_pos_left_y.astype(np.float64)
528
+ epi_pos_right_y = np.copy(epi_pos_left_y)
529
+ epi_pos_right_x = np.copy(epi_pos_left_x)
530
+ epi_pos_right_x[np.where(matches_msk == 255)] += matches[
531
+ np.where(matches_msk == 255)
532
+ ]
533
+
534
+ # vectorize matches
535
+ vec_epi_pos_left = np.transpose(
536
+ np.vstack([epi_pos_left_x.ravel(), epi_pos_left_y.ravel()])
537
+ )
538
+ vec_epi_pos_right = np.transpose(
539
+ np.vstack([epi_pos_right_x.ravel(), epi_pos_right_y.ravel()])
540
+ )
541
+
542
+ # convert epipolar matches to sensor coordinates
543
+ sensor_pos_left = self.sensor_position_from_grid(
544
+ grid1, vec_epi_pos_left, interpolation_method=interpolation_method
545
+ )
546
+ sensor_pos_right = self.sensor_position_from_grid(
547
+ grid2, vec_epi_pos_right, interpolation_method=interpolation_method
548
+ )
549
+
550
+ if matches_type == cst.DISP_MODE:
551
+ # rearrange matches in the original epipolar geometry
552
+ sensor_pos_left_x = sensor_pos_left[:, 0].reshape(matches_msk.shape)
553
+ sensor_pos_left_x[np.where(matches_msk != 255)] = np.nan
554
+ sensor_pos_left_y = sensor_pos_left[:, 1].reshape(matches_msk.shape)
555
+ sensor_pos_left_y[np.where(matches_msk != 255)] = np.nan
556
+
557
+ sensor_pos_right_x = sensor_pos_right[:, 0].reshape(
558
+ matches_msk.shape
559
+ )
560
+ sensor_pos_right_x[np.where(matches_msk != 255)] = np.nan
561
+ sensor_pos_right_y = sensor_pos_right[:, 1].reshape(
562
+ matches_msk.shape
563
+ )
564
+ sensor_pos_right_y[np.where(matches_msk != 255)] = np.nan
565
+
566
+ sensor_pos_left = np.zeros(
567
+ (matches_msk.shape[0], matches_msk.shape[1], 2)
568
+ )
569
+ sensor_pos_left[:, :, 0] = sensor_pos_left_x
570
+ sensor_pos_left[:, :, 1] = sensor_pos_left_y
571
+ sensor_pos_right = np.zeros(
572
+ (matches_msk.shape[0], matches_msk.shape[1], 2)
573
+ )
574
+ sensor_pos_right[:, :, 0] = sensor_pos_right_x
575
+ sensor_pos_right[:, :, 1] = sensor_pos_right_y
576
+
577
+ return sensor_pos_left, sensor_pos_right
578
+
579
+ def sensor_position_from_grid(
580
+ self,
581
+ grid: Union[dict, RectificationGrid],
582
+ positions: np.ndarray,
583
+ interpolation_method=None,
584
+ ) -> np.ndarray:
585
+ """
586
+ Interpolate the positions given as inputs using the grid
587
+
588
+ :param grid: rectification grid dict, or RectificationGrid object
589
+ :type grid: Union[dict, RectificationGrid]
590
+ :param positions: epipolar positions to interpolate given as a numpy
591
+ array of size [number of points, 2]. The last index indicates
592
+ the 'x' coordinate (last index set to 0) or the 'y' coordinate
593
+ (last index set to 1).
594
+ :return: sensors positions as a numpy array of size
595
+ [number of points, 2]. The last index indicates the 'x'
596
+ coordinate (last index set to 0) or
597
+ the 'y' coordinate (last index set to 1).
598
+ """
599
+
600
+ if isinstance(grid, RectificationGrid):
601
+ return grid.interpolate(positions)
602
+
603
+ if not isinstance(grid, dict):
604
+ raise RuntimeError(
605
+ f"Grid type {type(grid)} not a dict or RectificationGrid"
606
+ )
607
+
608
+ # Ensure positions is a numpy array
609
+ positions = np.asarray(positions)
610
+
611
+ # Get data
612
+ with rio.open(grid["path"]) as grid_data:
613
+ row_dep = grid_data.read(2)
614
+ col_dep = grid_data.read(1)
615
+
616
+ # Get step
617
+ step_col = grid["grid_spacing"][1]
618
+ step_row = grid["grid_spacing"][0]
619
+ ori_col = grid["grid_origin"][1]
620
+ ori_row = grid["grid_origin"][0]
621
+ last_col = ori_col + step_col * row_dep.shape[1]
622
+ last_row = ori_row + step_row * row_dep.shape[0]
623
+
624
+ cols = np.arange(ori_col, last_col, step_col)
625
+ rows = np.arange(ori_row, last_row, step_row)
626
+
627
+ # Determine margin based on interpolator type
628
+ margin = 6 if self.interpolator == "cubic" else 3
629
+
630
+ # Find the bounds of positions to determine crop region
631
+ min_col = np.nanmin(positions[:, 0])
632
+ max_col = np.nanmax(positions[:, 0])
633
+ min_row = np.nanmin(positions[:, 1])
634
+ max_row = np.nanmax(positions[:, 1])
635
+
636
+ # Convert position bounds to grid indices with margin
637
+ min_col_idx = max(0, int((min_col - ori_col) / step_col) - margin)
638
+ max_col_idx = min(
639
+ len(cols) - 1, int((max_col - ori_col) / step_col) + margin
640
+ )
641
+ min_row_idx = max(0, int((min_row - ori_row) / step_row) - margin)
642
+ max_row_idx = min(
643
+ len(rows) - 1, int((max_row - ori_row) / step_row) + margin
644
+ )
645
+
646
+ # Crop the grids and coordinate arrays
647
+ cols_cropped = cols[min_col_idx : max_col_idx + 1]
648
+ rows_cropped = rows[min_row_idx : max_row_idx + 1]
649
+ sensor_row_positions_cropped = row_dep[
650
+ min_row_idx : max_row_idx + 1, min_col_idx : max_col_idx + 1
651
+ ]
652
+ sensor_col_positions_cropped = col_dep[
653
+ min_row_idx : max_row_idx + 1, min_col_idx : max_col_idx + 1
654
+ ]
655
+
656
+ if interpolation_method is not None:
657
+ method = interpolation_method
658
+ else:
659
+ method = self.interpolator
660
+
661
+ # interpolate sensor positions
662
+ interpolator = interpolate.RegularGridInterpolator(
663
+ (cols_cropped, rows_cropped),
664
+ np.stack(
665
+ (
666
+ sensor_row_positions_cropped.transpose(),
667
+ sensor_col_positions_cropped.transpose(),
668
+ ),
669
+ axis=2,
670
+ ),
671
+ method=method,
672
+ bounds_error=False,
673
+ fill_value=None,
674
+ )
675
+
676
+ sensor_positions = interpolator(positions)
677
+
678
+ min_row = np.min(sensor_row_positions_cropped)
679
+ max_row = np.max(sensor_row_positions_cropped)
680
+ min_col = np.min(sensor_col_positions_cropped)
681
+ max_col = np.max(sensor_col_positions_cropped)
682
+
683
+ valid_rows = np.logical_and(
684
+ sensor_positions[:, 0] > min_row,
685
+ sensor_positions[:, 0] < max_row,
686
+ )
687
+ valid_cols = np.logical_and(
688
+ sensor_positions[:, 1] > min_col,
689
+ sensor_positions[:, 1] < max_col,
690
+ )
691
+ valid = np.logical_and(valid_rows, valid_cols)
692
+
693
+ if np.sum(~valid) > 0:
694
+ logging.warning(
695
+ "{}/{} points are outside of epipolar grid".format(
696
+ np.sum(~valid), valid.size
697
+ )
698
+ )
699
+
700
+ # swap
701
+ sensor_positions[:, [0, 1]] = sensor_positions[:, [1, 0]]
702
+
703
+ return sensor_positions
704
+
705
+ def epipolar_position_from_grid(self, grid, sensor_positions, step=30):
706
+ """
707
+ Compute epipolar position from grid
708
+
709
+ :param grid: epipolar grid
710
+ :param sensor_positions: sensor positions
711
+ :param step: step of grid interpolator
712
+
713
+ :return epipolar positions
714
+ """
715
+ # Generate interpolations grid to compute reverse
716
+
717
+ epi_size_x = grid["epipolar_size_x"]
718
+ epi_size_y = grid["epipolar_size_y"]
719
+
720
+ epi_grid_row, epi_grid_col = np.mgrid[
721
+ 0:epi_size_x:step, 0:epi_size_y:step
722
+ ]
723
+
724
+ full_epi_pos = np.stack(
725
+ [epi_grid_row.flatten(), epi_grid_col.flatten()], axis=1
726
+ )
727
+
728
+ sensor_interp_pos = self.sensor_position_from_grid(grid, full_epi_pos)
729
+ interp_row = LinearNDInterpolator(
730
+ list(
731
+ zip( # noqa: B905
732
+ sensor_interp_pos[:, 0], sensor_interp_pos[:, 1]
733
+ )
734
+ ),
735
+ epi_grid_row.flatten(),
736
+ )
737
+ epi_interp_row = interp_row(
738
+ sensor_positions[:, 0], sensor_positions[:, 1]
739
+ )
740
+
741
+ interp_col = LinearNDInterpolator(
742
+ list(
743
+ zip( # noqa: B905
744
+ sensor_interp_pos[:, 0], sensor_interp_pos[:, 1]
745
+ )
746
+ ),
747
+ epi_grid_col.flatten(),
748
+ )
749
+ epi_interp_col = interp_col(
750
+ sensor_positions[:, 0], sensor_positions[:, 1]
751
+ )
752
+
753
+ epipolar_positions = np.stack(
754
+ (epi_interp_row, epi_interp_col)
755
+ ).transpose()
756
+
757
+ return epipolar_positions
758
+
759
+ @cars_profile(name="Transform matches", interval=0.5)
760
+ def transform_matches_from_grids(
761
+ self,
762
+ sensor_matches_left,
763
+ sensor_matches_right,
764
+ new_grid_left,
765
+ new_grid_right,
766
+ ):
767
+ """
768
+ Transform epipolar matches with grid transformation
769
+
770
+ :param new_grid_left: path to epipolar grid of image 1
771
+ :param new_grid_right: path to epipolar grid of image 2
772
+ :param matches: cars disparity dataset or matches as numpy array
773
+
774
+ """
775
+
776
+ # Transform to new grids
777
+ new_grid_matches_left = self.epipolar_position_from_grid(
778
+ new_grid_left, sensor_matches_left
779
+ )
780
+ new_grid_matches_right = self.epipolar_position_from_grid(
781
+ new_grid_right, sensor_matches_right
782
+ )
783
+
784
+ # Concatenate matches
785
+ new_matches_array = np.concatenate(
786
+ [new_grid_matches_left, new_grid_matches_right], axis=1
787
+ )
788
+
789
+ # Linear interpolation might generate nan on the borders
790
+ new_matches_array = new_matches_array[
791
+ ~np.isnan(new_matches_array).any(axis=1)
792
+ ]
793
+
794
+ return new_matches_array
795
+
796
+ @cars_profile(name="Get sensor matches")
797
+ def get_sensor_matches( # pylint: disable=too-many-positional-arguments
798
+ self,
799
+ matches_array,
800
+ grid_left,
801
+ grid_right,
802
+ pair_folder,
803
+ save_matches,
804
+ ):
805
+ """
806
+ Get sensor matches
807
+
808
+ :param grid_left: path to epipolar grid of image 1
809
+ :param grid_left: path to epipolar grid of image 2
810
+ """
811
+ # Transform to sensors
812
+ sensor_matches_left = self.sensor_position_from_grid(
813
+ grid_left, matches_array[:, 0:2]
814
+ )
815
+ sensor_matches_right = self.sensor_position_from_grid(
816
+ grid_right, matches_array[:, 2:4]
817
+ )
818
+
819
+ current_out_dir = None
820
+ if save_matches:
821
+ logging.info("Writing matches file")
822
+ if pair_folder is None:
823
+ logging.error("Pair folder not provided")
824
+ else:
825
+ safe_makedirs(pair_folder)
826
+ current_out_dir = pair_folder
827
+ matches_sensor_left_path = os.path.join(
828
+ current_out_dir, "sensor_matches_left.npy"
829
+ )
830
+ matches_sensor_right_path = os.path.join(
831
+ current_out_dir, "sensor_matches_right.npy"
832
+ )
833
+ np.save(matches_sensor_left_path, sensor_matches_left)
834
+ np.save(matches_sensor_right_path, sensor_matches_right)
835
+
836
+ return sensor_matches_left, sensor_matches_right
837
+
838
+ @abstractmethod
839
+ def direct_loc( # pylint: disable=too-many-positional-arguments
840
+ self,
841
+ sensor,
842
+ geomodel,
843
+ x_coord: np.array,
844
+ y_coord: np.array,
845
+ z_coord: np.array = None,
846
+ ) -> np.ndarray:
847
+ """
848
+ For a given image points list, compute the latitudes,
849
+ longitudes, altitudes
850
+
851
+ Advice: to be sure, use x,y,z list inputs only
852
+
853
+ :param sensor: path to sensor image
854
+ :param geomodel: path and attributes for geomodel
855
+ :param x_coord: X Coordinates list in input image sensor
856
+ :param y_coord: Y Coordinate list in input image sensor
857
+ :param z_coord: Z Altitude list coordinate to take the image
858
+ :return: Latitude, Longitude, Altitude coordinates list as a numpy array
859
+ """
860
+
861
+ def safe_direct_loc( # pylint: disable=too-many-positional-arguments
862
+ self,
863
+ sensor,
864
+ geomodel,
865
+ x_coord: np.array,
866
+ y_coord: np.array,
867
+ z_coord: np.array = None,
868
+ ) -> np.ndarray:
869
+ """
870
+ For a given image points list, compute the latitudes,
871
+ longitudes, altitudes
872
+
873
+ Advice: to be sure, use x,y,z list inputs only
874
+
875
+ :param sensor: path to sensor image
876
+ :param geomodel: path and attributes for geomodel
877
+ :param x_coord: X Coordinates list in input image sensor
878
+ :param y_coord: Y Coordinate list in input image sensor
879
+ :param z_coord: Z Altitude list coordinate to take the image
880
+ :return: Latitude, Longitude, Altitude coordinates list as a numpy array
881
+ """
882
+ if len(x_coord) > 0:
883
+ ground_points = self.direct_loc(
884
+ sensor,
885
+ geomodel,
886
+ x_coord,
887
+ y_coord,
888
+ z_coord,
889
+ )
890
+ else:
891
+ logging.warning("Direct loc function launched on empty list")
892
+ return []
893
+ if z_coord is None:
894
+ status = np.any(np.isnan(ground_points), axis=0)
895
+ if sum(status) > 0:
896
+ logging.warning(
897
+ "{} errors have been detected on direct "
898
+ "loc and will be re-launched".format(sum(status))
899
+ )
900
+ ground_points_retry = self.direct_loc(
901
+ sensor,
902
+ geomodel,
903
+ x_coord[status],
904
+ y_coord[status],
905
+ np.array([0]),
906
+ )
907
+ ground_points[:, status] = ground_points_retry
908
+ return ground_points
909
+
910
+ @abstractmethod
911
+ def inverse_loc( # pylint: disable=too-many-positional-arguments
912
+ self,
913
+ sensor,
914
+ geomodel,
915
+ lat_coord: np.array,
916
+ lon_coord: np.array,
917
+ z_coord: np.array = None,
918
+ ) -> np.ndarray:
919
+ """
920
+ For a given image points list, compute the latitudes,
921
+ longitudes, altitudes
922
+
923
+ Advice: to be sure, use x,y,z list inputs only
924
+
925
+ :param sensor: path to sensor image
926
+ :param geomodel: path and attributes for geomodel
927
+ :param lat_coord: latitute Coordinate list
928
+ :param lon_coord: longitude Coordinates list
929
+ :param z_coord: Z Altitude list
930
+ :return: X / Y / Z Coordinates list in input image as a numpy array
931
+ """
932
+
933
+ def safe_inverse_loc( # pylint: disable=too-many-positional-arguments
934
+ self,
935
+ sensor,
936
+ geomodel,
937
+ lat_coord: np.array,
938
+ lon_coord: np.array,
939
+ z_coord: np.array = None,
940
+ ) -> np.ndarray:
941
+ """
942
+ For a given image points list, compute the latitudes,
943
+ longitudes, altitudes
944
+
945
+ Advice: to be sure, use x,y,z list inputs only
946
+
947
+ :param sensor: path to sensor image
948
+ :param geomodel: path and attributes for geomodel
949
+ :param lat_coord: latitute Coordinate list
950
+ :param lon_coord: longitude Coordinates list
951
+ :param z_coord: Z Altitude list
952
+ :return: X / Y / Z Coordinates list in input image as a numpy array
953
+ """
954
+ if len(lat_coord) > 0:
955
+ image_points = self.inverse_loc(
956
+ sensor,
957
+ geomodel,
958
+ lat_coord,
959
+ lon_coord,
960
+ z_coord,
961
+ )
962
+ image_points = np.array(image_points)
963
+ else:
964
+ logging.warning("Inverse loc function launched on empty list")
965
+ return [], [], []
966
+ if z_coord is None:
967
+ image_points = np.array(image_points)
968
+ status = np.any(np.isnan(image_points), axis=0)
969
+ if sum(status) > 0:
970
+ logging.warning(
971
+ "{} errors have been detected on inverse "
972
+ "loc and will be re-launched".format(sum(status))
973
+ )
974
+ image_points_retry = self.inverse_loc(
975
+ sensor,
976
+ geomodel,
977
+ lat_coord[status],
978
+ lon_coord[status],
979
+ np.array([self.default_alt]),
980
+ )
981
+
982
+ image_points[:, status] = image_points_retry
983
+ return image_points[0], image_points[1], image_points[2]
984
+
985
+ def image_envelope( # pylint: disable=too-many-positional-arguments
986
+ self,
987
+ sensor,
988
+ geomodel,
989
+ out_path=None,
990
+ out_driver="ESRI Shapefile",
991
+ elevation=None,
992
+ ):
993
+ """
994
+ Export the image footprint to a vector file
995
+
996
+ :param sensor: path to sensor image
997
+ :param geomodel: path and attributes for geometrical model
998
+ :param out_path: Path to the output vector file
999
+ :param out_driver: OGR driver to use to write output file
1000
+ """
1001
+ # retrieve image size
1002
+ img_size_x, img_size_y = inputs.rasterio_get_size(sensor)
1003
+
1004
+ # compute corners ground coordinates
1005
+ shift_x = -0.5
1006
+ shift_y = -0.5
1007
+ # TODO call 1 time with multipoint
1008
+ lat_upper_left, lon_upper_left, _ = self.direct_loc(
1009
+ sensor,
1010
+ geomodel,
1011
+ np.array(shift_x),
1012
+ np.array(shift_y),
1013
+ elevation,
1014
+ )
1015
+ lat_upper_right, lon_upper_right, _ = self.direct_loc(
1016
+ sensor,
1017
+ geomodel,
1018
+ np.array(img_size_x + shift_x),
1019
+ np.array(shift_y),
1020
+ elevation,
1021
+ )
1022
+ lat_bottom_left, lon_bottom_left, _ = self.direct_loc(
1023
+ sensor,
1024
+ geomodel,
1025
+ np.array(shift_x),
1026
+ np.array(img_size_y + shift_y),
1027
+ elevation,
1028
+ )
1029
+ lat_bottom_right, lon_bottom_right, _ = self.direct_loc(
1030
+ sensor,
1031
+ geomodel,
1032
+ np.array(img_size_x + shift_x),
1033
+ np.array(img_size_y + shift_y),
1034
+ elevation,
1035
+ )
1036
+
1037
+ u_l = (lon_upper_left, lat_upper_left)
1038
+ u_r = (lon_upper_right, lat_upper_right)
1039
+ l_l = (lon_bottom_left, lat_bottom_left)
1040
+ l_r = (lon_bottom_right, lat_bottom_right)
1041
+
1042
+ if out_path is not None:
1043
+ # create envelope polygon and save it as a shapefile
1044
+ poly_bb = Polygon([u_l, u_r, l_r, l_l, u_l])
1045
+ outputs.write_vector([poly_bb], out_path, 4326, driver=out_driver)
1046
+
1047
+ return u_l, u_r, l_l, l_r
1048
+
1049
+
1050
+ def min_max_to_physical_min_max(xmin, xmax, ymin, ymax, transform):
1051
+ """
1052
+ Transform min max index to position min max
1053
+
1054
+ :param xmin: xmin
1055
+ :type xmin: int
1056
+ :param xmax: xmax
1057
+ :type xmax: int
1058
+ :param ymin: ymin
1059
+ :type ymin: int
1060
+ :param ymax: ymax
1061
+ :type ymax: int
1062
+ :param transform: transform
1063
+ :type transform: Affine
1064
+
1065
+ :return: xmin, xmax, ymin, ymax
1066
+ :rtype: list(int)
1067
+ """
1068
+
1069
+ cols_ind = np.array([xmin, xmin, xmax, xmax])
1070
+ rows_ind = np.array([ymin, ymax, ymin, ymax])
1071
+
1072
+ rows_pos, cols_pos = proj_utils.transform_index_to_physical_point(
1073
+ transform,
1074
+ rows_ind,
1075
+ cols_ind,
1076
+ )
1077
+
1078
+ return (
1079
+ np.min(cols_pos),
1080
+ np.max(cols_pos),
1081
+ np.min(rows_pos),
1082
+ np.max(rows_pos),
1083
+ )
1084
+
1085
+
1086
+ def min_max_to_index_min_max(xmin, xmax, ymin, ymax, transform):
1087
+ """
1088
+ Transform min max position to index min max
1089
+
1090
+ :param xmin: xmin
1091
+ :type xmin: int
1092
+ :param xmax: xmax
1093
+ :type xmax: int
1094
+ :param ymin: ymin
1095
+ :type ymin: int
1096
+ :param ymax: ymax
1097
+ :type ymax: int
1098
+ :param transform: transform
1099
+ :type transform: Affine
1100
+
1101
+ :return: xmin, xmax, ymin, ymax
1102
+ :rtype: list(int)
1103
+ """
1104
+
1105
+ cols_ind = np.array([xmin, xmin, xmax, xmax])
1106
+ rows_ind = np.array([ymin, ymax, ymin, ymax])
1107
+
1108
+ rows_pos, cols_pos = proj_utils.transform_physical_point_to_index(
1109
+ ~transform,
1110
+ rows_ind,
1111
+ cols_ind,
1112
+ )
1113
+
1114
+ return (
1115
+ np.min(cols_pos),
1116
+ np.max(cols_pos),
1117
+ np.min(rows_pos),
1118
+ np.max(rows_pos),
1119
+ )