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