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