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,757 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ cars-bundleadjustment
4
+ """
5
+
6
+ import argparse
7
+ import copy
8
+ import json
9
+ import logging
10
+ import os
11
+ import textwrap
12
+ import warnings
13
+
14
+ import geopandas as gpd
15
+ import numpy as np
16
+ import pandas as pd
17
+ import rasterio as rio
18
+ import yaml
19
+
20
+ try:
21
+ from rpcfit import rpc_fit
22
+ except ModuleNotFoundError:
23
+ logging.warning(
24
+ "Module rpcfit is not installed. "
25
+ "RPC models will not be adjusted. "
26
+ "Run `pip install cars[bundleadjustment]` to install "
27
+ "missing module."
28
+ )
29
+
30
+ from affine import Affine
31
+ from scipy import interpolate, stats
32
+ from scipy.spatial import cKDTree
33
+ from shareloc.geofunctions.triangulation import n_view_triangulation
34
+ from shareloc.geomodels.geomodel import GeoModel
35
+ from shareloc.geomodels.los import LOS
36
+ from shareloc.proj_utils import coordinates_conversion
37
+
38
+ from cars.pipelines.parameters import sensor_inputs
39
+ from cars.pipelines.pipeline import Pipeline
40
+
41
+
42
+ def matches_concatenation(matches_list, pairing, nb_decimals):
43
+ """
44
+ Concatenate matches computed by pair: for the second and
45
+ subsequent pairs, the first image must be included in the list
46
+ of previous images.
47
+ """
48
+
49
+ matches_dataframe_list = []
50
+ merge_on_list = [pair[0] for pair in pairing[1:]]
51
+
52
+ # store matches as dataframe
53
+ for matches, pair in zip(matches_list, pairing, strict=True):
54
+ columns = [
55
+ "col_" + pair[0],
56
+ "row_" + pair[0],
57
+ "col_" + pair[1],
58
+ "row_" + pair[1],
59
+ ]
60
+ matches_dataframe = pd.DataFrame(matches[:, 4:8], columns=columns)
61
+ rounded_dataframe = matches_dataframe.round(nb_decimals).add_prefix("r")
62
+ matches_dataframe = pd.concat(
63
+ [matches_dataframe, rounded_dataframe], axis=1
64
+ )
65
+ matches_dataframe_list.append(matches_dataframe)
66
+
67
+ # aggregate dataframe with image pivot (merge_on)
68
+ matches_dataframe = matches_dataframe_list[0]
69
+
70
+ for matches_to_merge, merge_on in zip(
71
+ matches_dataframe_list[1:], merge_on_list, strict=True
72
+ ):
73
+ # print(matches_dataframe)
74
+ matches_dataframe = matches_dataframe.merge(
75
+ matches_to_merge, on=["rcol_" + merge_on, "rrow_" + merge_on]
76
+ )
77
+ matches_dataframe["col_" + merge_on] = (
78
+ matches_dataframe["col_" + merge_on + "_x"]
79
+ + matches_dataframe["col_" + merge_on + "_y"]
80
+ ) / 2
81
+ matches_dataframe["row_" + merge_on] = (
82
+ matches_dataframe["row_" + merge_on + "_x"]
83
+ + matches_dataframe["row_" + merge_on + "_y"]
84
+ ) / 2
85
+
86
+ matches_dataframe = matches_dataframe.drop(
87
+ [
88
+ "col_" + merge_on + "_x",
89
+ "col_" + merge_on + "_y",
90
+ "row_" + merge_on + "_x",
91
+ "row_" + merge_on + "_y",
92
+ ],
93
+ axis=1,
94
+ )
95
+
96
+ matches_dataframe = matches_dataframe.loc[
97
+ :, ~matches_dataframe.columns.str.startswith("rcol")
98
+ ]
99
+ matches_dataframe = matches_dataframe.loc[
100
+ :, ~matches_dataframe.columns.str.startswith("rrow")
101
+ ]
102
+ return matches_dataframe
103
+
104
+
105
+ def estimate_intersection_residues_from_matches(
106
+ geomodels, matches_dataframe, ignored=None
107
+ ):
108
+ """
109
+ Compute intersections from multiple matches and estimate the
110
+ residues as the difference between the previous sensor position and
111
+ the inverse location of the multiple views intersection.
112
+ """
113
+
114
+ # compute multi line intersection
115
+ vis_list, sis_list = [], []
116
+ for key in geomodels.keys():
117
+ if ignored is None or key not in ignored:
118
+ los = LOS(
119
+ matches_dataframe[["col_" + key, "row_" + key]].values,
120
+ geomodels[key],
121
+ )
122
+ vis_list.append(los.viewing_vectors)
123
+ sis_list.append(los.starting_points)
124
+
125
+ vis = np.dstack(vis_list)
126
+ vis = np.swapaxes(vis, 1, 2)
127
+
128
+ sis = np.dstack(sis_list)
129
+ sis = np.swapaxes(sis, 1, 2)
130
+
131
+ intersections_ecef = n_view_triangulation(sis, vis)
132
+
133
+ in_crs = 4978
134
+ out_crs = 4326
135
+ intersections_wgs84 = coordinates_conversion(
136
+ intersections_ecef, in_crs, out_crs
137
+ )
138
+
139
+ lon, lat, alt = (
140
+ intersections_wgs84[:, 0],
141
+ intersections_wgs84[:, 1],
142
+ intersections_wgs84[:, 2],
143
+ )
144
+
145
+ matches_dataframe["lon"] = lon
146
+ matches_dataframe["lat"] = lat
147
+ matches_dataframe["alt"] = alt
148
+
149
+ # get inverse localisation of intersection (delta)
150
+ for key in geomodels.keys():
151
+ row, col, _ = geomodels[key].inverse_loc(
152
+ matches_dataframe["lon"].values.astype(float),
153
+ matches_dataframe["lat"].values.astype(float),
154
+ matches_dataframe["alt"].values.astype(float),
155
+ )
156
+ matches_dataframe["col_" + key + "_new"] = col
157
+ matches_dataframe["row_" + key + "_new"] = row
158
+ matches_dataframe["delta_col_" + key] = (
159
+ matches_dataframe["col_" + key + "_new"]
160
+ - matches_dataframe["col_" + key]
161
+ )
162
+ matches_dataframe["delta_row_" + key] = (
163
+ matches_dataframe["row_" + key + "_new"]
164
+ - matches_dataframe["row_" + key]
165
+ )
166
+
167
+ return matches_dataframe
168
+
169
+
170
+ def aggregate_matches_by_cell(matches_dataframe, step, min_matches):
171
+ """
172
+ Aggregate matches with a step computing from matches density
173
+ matches density: footprint divided by number of matches
174
+ to deduce a "resolution
175
+ """
176
+
177
+ lon_min, lon_max = list(matches_dataframe["lon"].agg(["min", "max"]))
178
+ lat_min, lat_max = list(matches_dataframe["lat"].agg(["min", "max"]))
179
+ res = np.sqrt(
180
+ ((lon_max - lon_min) * (lat_max - lat_min))
181
+ / len(matches_dataframe.index)
182
+ )
183
+
184
+ cell_size = float("{:0.0e}".format(step * res))
185
+ lon_min = np.floor((lon_min / cell_size)) * cell_size
186
+ lat_min = np.floor((lat_min / cell_size)) * cell_size
187
+ lon_max = np.ceil((lon_max / cell_size)) * cell_size
188
+ lat_max = np.ceil((lat_max / cell_size)) * cell_size
189
+
190
+ matches_dataframe["lon_cell"] = (
191
+ (matches_dataframe["lon"] / cell_size).astype(int) + 0.5
192
+ ) * cell_size
193
+ matches_dataframe["lat_cell"] = (
194
+ (matches_dataframe["lat"] / cell_size).astype(int) + 0.5
195
+ ) * cell_size
196
+
197
+ grouped_matches = matches_dataframe.groupby(["lon_cell", "lat_cell"])
198
+ count = grouped_matches.count()
199
+ regular_matches = grouped_matches.median()
200
+ regular_matches = regular_matches[count.alt > min_matches]
201
+
202
+ return regular_matches
203
+
204
+
205
+ def plane_regression(points, values):
206
+ """
207
+ Deduce a plane fitting points / values
208
+ """
209
+ x_coords = points[:, 0].flatten()
210
+ y_coords = points[:, 1].flatten()
211
+
212
+ coefficient_matrix = np.array([x_coords * 0 + 1, x_coords, y_coords]).T
213
+ ordinate = values.flatten()
214
+
215
+ coefs, _, _, _ = np.linalg.lstsq(coefficient_matrix, ordinate, rcond=None)
216
+
217
+ coefs_2d = np.ndarray((2, 2))
218
+ coefs_2d[0, 0] = coefs[0]
219
+ coefs_2d[1, 0] = coefs[1]
220
+ coefs_2d[0, 1] = coefs[2]
221
+ coefs_2d[1, 1] = 0.0
222
+
223
+ return coefs_2d
224
+
225
+
226
+ def create_deformation_grid(
227
+ images, regular_matches, interp_mode, step, aggregate_step
228
+ ):
229
+ """
230
+ Compute the deformation grid with a defined step
231
+ if interp_mode is True, nan is replaced by the row mean
232
+ else the deformation is a plane deformation
233
+ """
234
+ old_coordinates, new_coordinates = {}, {}
235
+ for key in images.keys():
236
+ old_coordinates[key] = {}
237
+ new_coordinates[key] = {}
238
+ with rio.open(images[key]) as reader:
239
+ height = reader.height
240
+ width = reader.width
241
+ transform = reader.transform
242
+
243
+ cols_ext, rows_ext = np.meshgrid(
244
+ np.arange(width, step=step), np.arange(height, step=step)
245
+ )
246
+
247
+ cols, rows = rio.transform.xy(transform, rows_ext, cols_ext)
248
+ shapes = {"col": cols_ext.shape, "row": rows_ext.shape}
249
+ old_coordinates[key]["col"] = cols = np.array(cols)
250
+ old_coordinates[key]["row"] = rows = np.array(rows)
251
+ for dimension in ["row", "col"]:
252
+ old_coordinates[key][dimension] = old_coordinates[key][
253
+ dimension
254
+ ].reshape(shapes[dimension])
255
+
256
+ points = np.array(
257
+ (
258
+ regular_matches["col_" + key + "_new"].to_numpy(),
259
+ regular_matches["row_" + key + "_new"].to_numpy(),
260
+ )
261
+ ).T
262
+
263
+ extrap_delta, interp_delta = {}, {}
264
+ for dimension in ["row", "col"]:
265
+ values = regular_matches[
266
+ "delta_" + dimension + "_" + key
267
+ ].to_numpy()
268
+ coefs_2d = plane_regression(points, values)
269
+ extrap_delta[dimension] = np.polynomial.polynomial.polyval2d(
270
+ cols, rows, coefs_2d
271
+ )
272
+ extrap_delta[dimension] = extrap_delta[dimension].reshape(
273
+ shapes[dimension]
274
+ )
275
+ if interp_mode:
276
+ interp_delta[dimension] = interpolate.griddata(
277
+ points=points,
278
+ values=values,
279
+ xi=(cols, rows),
280
+ method="linear",
281
+ )
282
+ tree = cKDTree(points)
283
+ coords = np.asanyarray((cols, rows)).T
284
+ dists, __ = tree.query(coords)
285
+ interp_delta[dimension][dists > step / aggregate_step] = np.nan
286
+ interp_delta[dimension] = interp_delta[dimension].reshape(
287
+ shapes[dimension]
288
+ )
289
+ isnan = np.isnan(interp_delta[dimension])
290
+ interp_delta[dimension] = rio.fill.fillnodata(
291
+ interp_delta[dimension], mask=~isnan, max_search_distance=5
292
+ )
293
+ isnan = np.isnan(interp_delta[dimension])
294
+
295
+ with warnings.catch_warnings():
296
+ warnings.filterwarnings(
297
+ "ignore", r"All-NaN (slice|axis) encountered"
298
+ )
299
+ meanrows = np.nanmedian(interp_delta[dimension], axis=1)[
300
+ np.newaxis
301
+ ].T
302
+
303
+ interp_delta[dimension][isnan] = np.tile(
304
+ meanrows, (1, shapes[dimension][1])
305
+ )[isnan]
306
+ isnan = np.isnan(interp_delta[dimension])
307
+ interp_delta[dimension] = rio.fill.fillnodata(
308
+ interp_delta[dimension], mask=~isnan
309
+ )
310
+
311
+ for dimension in ["row", "col"]:
312
+ if interp_mode:
313
+ new_coordinates[key][dimension] = (
314
+ old_coordinates[key][dimension] - interp_delta[dimension]
315
+ )
316
+ else:
317
+ new_coordinates[key][dimension] = (
318
+ old_coordinates[key][dimension] - extrap_delta[dimension]
319
+ )
320
+
321
+ return old_coordinates, new_coordinates
322
+
323
+
324
+ def refine_rpc(geomodels, old_coordinates, new_coordinates):
325
+ """
326
+ Compute new rpc matching old and new coordinates
327
+ """
328
+ refined_rpcs = {}
329
+ for key in geomodels.keys():
330
+ cols, rows = (
331
+ old_coordinates[key]["col"],
332
+ old_coordinates[key]["row"],
333
+ )
334
+ new_cols, new_rows = (
335
+ new_coordinates[key]["col"],
336
+ new_coordinates[key]["row"],
337
+ )
338
+ locs_train, target_train = [], []
339
+ for alt in [-50, 0, 500, 1000]:
340
+ locs_train += (
341
+ geomodels[key]
342
+ .direct_loc_h(
343
+ np.ravel(rows),
344
+ np.ravel(cols),
345
+ np.full(np.prod(cols.shape), alt),
346
+ )
347
+ .tolist()
348
+ )
349
+
350
+ target_train += np.stack(
351
+ (np.ravel(new_cols), np.ravel(new_rows)), axis=-1
352
+ ).tolist()
353
+
354
+ locs_train = np.array(locs_train)
355
+ target_train = np.array(target_train)
356
+
357
+ nanrows = np.isnan(target_train).any(axis=1)
358
+ target_train = target_train[~nanrows]
359
+ locs_train = locs_train[~nanrows]
360
+
361
+ # fit on training set
362
+ rpc_calib, __ = rpc_fit.calibrate_rpc(
363
+ target_train,
364
+ locs_train,
365
+ separate=False,
366
+ tol=1e-10,
367
+ max_iter=20,
368
+ method="initLcurve",
369
+ plot=False,
370
+ orientation="projloc",
371
+ get_log=True,
372
+ )
373
+ # evaluate on training set
374
+ rmse_err, __, __ = rpc_fit.evaluate(rpc_calib, locs_train, target_train)
375
+ print(
376
+ "Training set : Mean X-RMSE {:e} Mean Y-RMSE {:e}".format(
377
+ *rmse_err
378
+ )
379
+ )
380
+
381
+ refined_rpcs[key] = rpc_calib.to_geotiff_dict()
382
+ return refined_rpcs
383
+
384
+
385
+ def write_rpcs_as_geom(refined_rpcs, out_dir):
386
+ """
387
+ Write RPCs as geomfiles
388
+ """
389
+ geoms_filenames = {}
390
+ for key in refined_rpcs.keys():
391
+ geom = os.path.join(out_dir, key + ".geom")
392
+ with open(geom, "w", encoding="utf-8") as writer:
393
+ for rpc_key in refined_rpcs[key]:
394
+ try:
395
+ values = refined_rpcs[key][rpc_key].split()
396
+ for idx, value in enumerate(values):
397
+ line = rpc_key.lower() + "_%02d: " % idx + str(value)
398
+ writer.write(line + "\n")
399
+ except AttributeError:
400
+ line = (
401
+ rpc_key.lower() + ": " + str(refined_rpcs[key][rpc_key])
402
+ )
403
+ writer.write(line + "\n")
404
+ writer.write("type: ossimRpcModel\n")
405
+ writer.write("polynomial_format: B\n")
406
+ geoms_filenames[key] = geom
407
+ return geoms_filenames
408
+
409
+
410
+ def new_rpcs_from_matches( # pylint: disable=too-many-positional-arguments
411
+ sensors,
412
+ config_directory,
413
+ sparse_matching_directory,
414
+ pairing=None,
415
+ nb_decimals=0,
416
+ min_matches=50,
417
+ step=5,
418
+ aggregate_matches=True,
419
+ interp_mode=False,
420
+ ):
421
+ """
422
+ Main function of cars-bundleadjustement for new RPCs estimation:
423
+ - Retrieve matches from pairs and concatenate
424
+ - Estimate residues by inverse location
425
+ - Compute new RPCs
426
+ """
427
+ matches_list = []
428
+ for pair in pairing:
429
+ matches_filename = os.path.join(
430
+ sparse_matching_directory,
431
+ "dump_dir",
432
+ "sparse_matching",
433
+ "_".join(pair),
434
+ "filtered_matches.npy",
435
+ )
436
+ matches = np.load(matches_filename)
437
+ matches_list.append(matches)
438
+ print("pair: " + str(pair) + ": " + str(matches.shape[0]) + " matches")
439
+
440
+ matches_df = matches_concatenation(matches_list, pairing, nb_decimals)
441
+
442
+ # retrieve sensors keys
443
+ sensors_keys = sensors.keys()
444
+
445
+ # store geomodels
446
+ geomodels = {}
447
+ for key in sensors_keys:
448
+ geomodel_filename = sensors[key]["geomodel"] = os.path.abspath(
449
+ os.path.join(config_directory, sensors[key]["geomodel"])
450
+ )
451
+ geomodels[key] = GeoModel(geomodel_filename)
452
+
453
+ matches_df = estimate_intersection_residues_from_matches(
454
+ geomodels, matches_df
455
+ )
456
+ matches_df.drop_duplicates(inplace=True)
457
+
458
+ matches_gdf = gpd.GeoDataFrame(
459
+ matches_df,
460
+ geometry=gpd.points_from_xy(matches_df.lon, matches_df.lat),
461
+ crs="EPSG:4326",
462
+ )
463
+ matches_gdf.to_file(
464
+ os.path.join(sparse_matching_directory, "matches.gpkg"), driver="GPKG"
465
+ )
466
+ matches_gdf.to_csv(os.path.join(sparse_matching_directory, "matches.csv"))
467
+
468
+ if aggregate_matches is True:
469
+ matches = aggregate_matches_by_cell(
470
+ matches_df, step=step, min_matches=min_matches
471
+ )
472
+ else:
473
+ matches = matches_df
474
+ matches = matches[(np.abs(stats.zscore(matches)) < 3).all(axis=1)]
475
+
476
+ matches_gdf = gpd.GeoDataFrame(
477
+ matches,
478
+ geometry=gpd.points_from_xy(matches.lon, matches.lat),
479
+ crs="EPSG:4326",
480
+ )
481
+ matches_gdf.to_file(
482
+ os.path.join(sparse_matching_directory, "aggregate_matches.gpkg"),
483
+ driver="GPKG",
484
+ )
485
+ matches_gdf.to_csv(
486
+ os.path.join(sparse_matching_directory, "aggregate_matches.csv")
487
+ )
488
+
489
+ images = {}
490
+ for key in sensors_keys:
491
+ images[key] = sensors[key]["image"]["bands"]["b0"]["path"] = (
492
+ os.path.abspath(
493
+ os.path.join(
494
+ config_directory,
495
+ sensors[key]["image"]["bands"]["b0"]["path"],
496
+ )
497
+ )
498
+ )
499
+
500
+ grid_step = step * 25
501
+ old_coords, new_coords = create_deformation_grid(
502
+ images, matches, interp_mode, step=grid_step, aggregate_step=step
503
+ )
504
+
505
+ deformation_dir = os.path.join(
506
+ sparse_matching_directory, "deformation_grids"
507
+ )
508
+ os.makedirs(deformation_dir, exist_ok=True)
509
+
510
+ for key in geomodels:
511
+ cols, rows = new_coords[key]["col"], new_coords[key]["row"]
512
+ transform = Affine(
513
+ grid_step, 0.0, -grid_step / 2, 0.0, grid_step, -grid_step / 2
514
+ )
515
+
516
+ with rio.open(
517
+ os.path.join(deformation_dir, "positions_" + key + ".tif"),
518
+ "w",
519
+ driver="GTiff",
520
+ height=cols.shape[0],
521
+ width=cols.shape[1],
522
+ count=2,
523
+ dtype=cols.dtype,
524
+ transform=transform,
525
+ ) as writer:
526
+
527
+ writer.write(cols, 1)
528
+ writer.write(rows, 2)
529
+
530
+ with rio.open(
531
+ os.path.join(deformation_dir, "delta_" + key + ".tif"),
532
+ "w",
533
+ driver="GTiff",
534
+ height=cols.shape[0],
535
+ width=cols.shape[1],
536
+ count=2,
537
+ dtype=cols.dtype,
538
+ transform=transform,
539
+ ) as writer:
540
+
541
+ writer.write(cols - old_coords[key]["col"], 1)
542
+ writer.write(rows - old_coords[key]["row"], 2)
543
+
544
+ if interp_mode is False:
545
+ try:
546
+ refined_rpcs = refine_rpc(geomodels, old_coords, new_coords)
547
+ except NameError:
548
+ logging.warning(
549
+ "Module rpcfit is not installed. "
550
+ "RPC models will not be adjusted. "
551
+ "Run `pip install cars[bundleadjustment]` to install "
552
+ "missing module."
553
+ )
554
+ refined_rpcs = None
555
+ return refined_rpcs
556
+
557
+ return None
558
+
559
+
560
+ def cars_bundle_adjustment(conf, no_run_sparse, output_format="yaml"):
561
+ """
562
+ cars-bundleadjustement main:
563
+ - Launch CARS to compute homologous points (run sparse matching)
564
+ - Compute new RPCs
565
+ """
566
+ _, ext = os.path.splitext(conf)
567
+ ext = ext.lower()
568
+
569
+ if ext == ".json":
570
+ with open(conf, encoding="utf-8") as reader:
571
+ conf_as_dict = json.load(reader)
572
+ elif ext in [".yaml", ".yml"]:
573
+ with open(conf, encoding="utf-8") as reader:
574
+ conf_as_dict = yaml.safe_load(reader)
575
+ else:
576
+ raise ValueError(
577
+ f"Unsupported configuration file format: {ext}. "
578
+ "Please use .json, .yaml, or .yml"
579
+ )
580
+
581
+ conf_dirname = os.path.dirname(conf)
582
+ out_dir = os.path.abspath(
583
+ os.path.join(conf_dirname, conf_as_dict["output"]["directory"])
584
+ )
585
+
586
+ bundle_adjustment_config = conf_as_dict["applications"].pop(
587
+ "bundle_adjustment"
588
+ )
589
+
590
+ # create configuration file + launch cars sparse matching
591
+ sparse_matching = os.path.join(out_dir, "sparse_matching")
592
+ sparse_matching_config = copy.deepcopy(conf_as_dict)
593
+ sparse_matching_config["input"]["pairing"] = bundle_adjustment_config[
594
+ "pairing"
595
+ ]
596
+ sparse_matching_config["output"]["directory"] = sparse_matching
597
+ sparse_matching_config["output"]["product_level"] = []
598
+ sparse_matching_config["advanced"] = {}
599
+ sparse_matching_config["advanced"]["epipolar_resolutions"] = [1]
600
+ if "sparse_matching" not in sparse_matching_config["applications"]:
601
+ sparse_matching_config["applications"]["all"] = {"sparse_matching": {}}
602
+ sparse_matching_config["applications"]["all"]["sparse_matching"][
603
+ "save_intermediate_data"
604
+ ] = True
605
+
606
+ sparse_matching_config["applications"]["all"]["sparse_matching"][
607
+ "decimation_factor"
608
+ ] = 100
609
+
610
+ sparse_matching_pipeline = Pipeline(
611
+ "default", sparse_matching_config, conf_dirname
612
+ )
613
+
614
+ if no_run_sparse is False:
615
+ sparse_matching_pipeline.run()
616
+
617
+ # create new refined rpcs
618
+ conf_as_dict["input"] = sensor_inputs.sensors_check_inputs(
619
+ conf_as_dict["input"], config_dir=conf_dirname
620
+ )
621
+ separate = bundle_adjustment_config.pop("separate")
622
+ refined_rpcs = new_rpcs_from_matches(
623
+ conf_as_dict["input"]["sensors"],
624
+ conf_dirname,
625
+ sparse_matching,
626
+ **bundle_adjustment_config,
627
+ )
628
+
629
+ if refined_rpcs is not None:
630
+ write_rpcs_as_geom(refined_rpcs, out_dir)
631
+
632
+ pairing_list = conf_as_dict["input"]["pairing"]
633
+ if separate is False:
634
+ pairing_list = [pairing_list]
635
+
636
+ for pairing in pairing_list:
637
+ # create configuration file + launch cars dense matching
638
+ raw = os.path.join(out_dir, "raw")
639
+ raw_config = copy.deepcopy(conf_as_dict)
640
+ sensors_keys = conf_as_dict["input"]["sensors"].keys()
641
+
642
+ if separate:
643
+ raw_config["input"]["pairing"] = [pairing]
644
+ raw_config["output"]["directory"] = "_".join([raw] + pairing)
645
+ else:
646
+ raw_config["input"]["pairing"] = pairing
647
+ raw_config["output"]["directory"] = raw
648
+
649
+ # output config file
650
+ raw_cfg_file = raw_config["output"]["directory"] + (
651
+ ".yaml" if output_format == "yaml" else ".json"
652
+ )
653
+ with open(raw_cfg_file, "w", encoding="utf8") as writer:
654
+ if output_format == "yaml":
655
+ yaml.safe_dump(raw_config, writer, sort_keys=False)
656
+ else:
657
+ json.dump(raw_config, writer, indent=2)
658
+
659
+ if refined_rpcs is not None:
660
+ # create configuration file + launch cars dense matching
661
+ refined = os.path.join(out_dir, "refined")
662
+ refined_config = copy.deepcopy(conf_as_dict)
663
+ sensors_keys = conf_as_dict["input"]["sensors"].keys()
664
+ for key in sensors_keys:
665
+ refined_config["input"]["sensors"][key]["geomodel"] = (
666
+ os.path.join(out_dir, key + ".geom")
667
+ )
668
+ if separate:
669
+ refined_config["input"]["pairing"] = [pairing]
670
+ refined_config["output"]["directory"] = "_".join(
671
+ [refined] + pairing
672
+ )
673
+ else:
674
+ refined_config["input"]["pairing"] = pairing
675
+ refined_config["output"]["directory"] = refined
676
+
677
+ refined_cfg_file = refined_config["output"]["directory"] + (
678
+ ".yaml" if output_format == "yaml" else ".json"
679
+ )
680
+ with open(refined_cfg_file, "w", encoding="utf8") as writer:
681
+ if output_format == "yaml":
682
+ yaml.safe_dump(refined_config, writer, sort_keys=False)
683
+ else:
684
+ json.dump(refined_config, writer, indent=2)
685
+
686
+
687
+ def cli():
688
+ """
689
+ Command Line Interface
690
+ """
691
+
692
+ parser = argparse.ArgumentParser(
693
+ "cars-bundleadjustment",
694
+ description="Refine multiple stereo pairs",
695
+ formatter_class=argparse.RawDescriptionHelpFormatter,
696
+ epilog=textwrap.dedent(
697
+ """\
698
+ This script takes a configuration file as input, similar to \
699
+ a classic configuration file for cars, by adding a \
700
+ "bundle_adjustment" \
701
+ key and its associated value:
702
+
703
+ ```
704
+ "applications": {
705
+ "bundle_adjustment": {
706
+ "pairing": [["key1", "key2"], ["key1", "key3"], \
707
+ ["key3", "key4"]],
708
+ "separate": true,
709
+ "nb_decimals": 0,
710
+ "min_matches": 50
711
+ }
712
+ }
713
+ ```
714
+
715
+ - Parameters "pairing" and "separate" are mandatory.
716
+ - Parameters "nb_decimals" (default value: 0), "min_matches" \
717
+ (default value: 100) and "output_format" (default value: yaml) are optional.
718
+
719
+ ### Generation of homologous points calculated by pair
720
+
721
+ The pairs used to calculate homologous points are those declared \
722
+ by the "pairing" value in the "bundle_adjustment" application. Please \
723
+ note: for the second and subsequent pairs, the first image must be \
724
+ included in the list of previous images. In the example above, key1 of \
725
+ the second pair is contained in the first pair, key3 of the third pair \
726
+ is contained in the second pair.
727
+
728
+ ### Estimation of adjustment required
729
+
730
+ Matching points are used to adjust pairs. To find homologous points common \
731
+ to all images, the "nb_decimals" parameter is used to round off the position \
732
+ of the points to be matched. For example, if "nb_decimals" = 0, two points in \
733
+ an image are considered to be the same if they belong to the same pixel. In \
734
+ addition, measurements related to homologous points are robustified by \
735
+ calculating statistics. The "min_matches" parameter is used to set the minimum \
736
+ number of matches per zone required to calculate these statistics."""
737
+ ),
738
+ )
739
+ parser.add_argument("conf", type=str, help="Configuration File")
740
+ parser.add_argument("--no-run-sparse", action="store_true")
741
+ parser.add_argument(
742
+ "--output-format",
743
+ type=str,
744
+ default="json",
745
+ choices=["json", "yaml", "JSON", "YAML"],
746
+ help="Output format for generated configuration files "
747
+ "(json or yaml, case-insensitive). Default: json",
748
+ )
749
+
750
+ args = parser.parse_args()
751
+ # normalize format to lowercase
752
+ args.output_format = args.output_format.lower()
753
+ cars_bundle_adjustment(**vars(args))
754
+
755
+
756
+ if __name__ == "__main__":
757
+ cli()