cars 1.0.0rc1__cp313-cp313-musllinux_1_2_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.
- cars/__init__.py +74 -0
- cars/applications/__init__.py +37 -0
- cars/applications/application.py +117 -0
- cars/applications/application_constants.py +29 -0
- cars/applications/application_template.py +146 -0
- cars/applications/auxiliary_filling/__init__.py +29 -0
- cars/applications/auxiliary_filling/abstract_auxiliary_filling_app.py +104 -0
- cars/applications/auxiliary_filling/auxiliary_filling_algo.py +475 -0
- cars/applications/auxiliary_filling/auxiliary_filling_from_sensors_app.py +630 -0
- cars/applications/auxiliary_filling/auxiliary_filling_wrappers.py +90 -0
- cars/applications/dem_generation/__init__.py +30 -0
- cars/applications/dem_generation/abstract_dem_generation_app.py +116 -0
- cars/applications/dem_generation/bulldozer_config/base_config.yaml +42 -0
- cars/applications/dem_generation/bulldozer_dem_app.py +655 -0
- cars/applications/dem_generation/bulldozer_memory.py +55 -0
- cars/applications/dem_generation/dem_generation_algo.py +107 -0
- cars/applications/dem_generation/dem_generation_constants.py +32 -0
- cars/applications/dem_generation/dem_generation_wrappers.py +323 -0
- cars/applications/dense_match_filling/__init__.py +30 -0
- cars/applications/dense_match_filling/abstract_dense_match_filling_app.py +242 -0
- cars/applications/dense_match_filling/fill_disp_algo.py +113 -0
- cars/applications/dense_match_filling/fill_disp_constants.py +39 -0
- cars/applications/dense_match_filling/fill_disp_wrappers.py +83 -0
- cars/applications/dense_match_filling/zero_padding_app.py +302 -0
- cars/applications/dense_matching/__init__.py +30 -0
- cars/applications/dense_matching/abstract_dense_matching_app.py +261 -0
- cars/applications/dense_matching/census_mccnn_sgm_app.py +1460 -0
- cars/applications/dense_matching/cpp/__init__.py +0 -0
- cars/applications/dense_matching/cpp/dense_matching_cpp.cpython-313-i386-linux-musl.so +0 -0
- cars/applications/dense_matching/cpp/dense_matching_cpp.py +94 -0
- cars/applications/dense_matching/cpp/includes/dense_matching.hpp +58 -0
- cars/applications/dense_matching/cpp/meson.build +9 -0
- cars/applications/dense_matching/cpp/src/bindings.cpp +13 -0
- cars/applications/dense_matching/cpp/src/dense_matching.cpp +207 -0
- cars/applications/dense_matching/dense_matching_algo.py +401 -0
- cars/applications/dense_matching/dense_matching_constants.py +89 -0
- cars/applications/dense_matching/dense_matching_wrappers.py +951 -0
- cars/applications/dense_matching/disparity_grid_algo.py +588 -0
- cars/applications/dense_matching/loaders/__init__.py +23 -0
- cars/applications/dense_matching/loaders/config_census_sgm_default.json +31 -0
- cars/applications/dense_matching/loaders/config_census_sgm_homogeneous.json +30 -0
- cars/applications/dense_matching/loaders/config_census_sgm_mountain_and_vegetation.json +30 -0
- cars/applications/dense_matching/loaders/config_census_sgm_shadow.json +30 -0
- cars/applications/dense_matching/loaders/config_census_sgm_sparse.json +36 -0
- cars/applications/dense_matching/loaders/config_census_sgm_urban.json +30 -0
- cars/applications/dense_matching/loaders/config_mapping.json +13 -0
- cars/applications/dense_matching/loaders/config_mccnn.json +28 -0
- cars/applications/dense_matching/loaders/global_land_cover_map.tif +0 -0
- cars/applications/dense_matching/loaders/pandora_loader.py +593 -0
- cars/applications/dsm_filling/__init__.py +32 -0
- cars/applications/dsm_filling/abstract_dsm_filling_app.py +101 -0
- cars/applications/dsm_filling/border_interpolation_app.py +270 -0
- cars/applications/dsm_filling/bulldozer_config/base_config.yaml +44 -0
- cars/applications/dsm_filling/bulldozer_filling_app.py +279 -0
- cars/applications/dsm_filling/exogenous_filling_app.py +333 -0
- cars/applications/grid_generation/__init__.py +30 -0
- cars/applications/grid_generation/abstract_grid_generation_app.py +142 -0
- cars/applications/grid_generation/epipolar_grid_generation_app.py +327 -0
- cars/applications/grid_generation/grid_correction_app.py +496 -0
- cars/applications/grid_generation/grid_generation_algo.py +388 -0
- cars/applications/grid_generation/grid_generation_constants.py +46 -0
- cars/applications/grid_generation/transform_grid.py +88 -0
- cars/applications/ground_truth_reprojection/__init__.py +30 -0
- cars/applications/ground_truth_reprojection/abstract_ground_truth_reprojection_app.py +137 -0
- cars/applications/ground_truth_reprojection/direct_localization_app.py +629 -0
- cars/applications/ground_truth_reprojection/ground_truth_reprojection_algo.py +275 -0
- cars/applications/point_cloud_outlier_removal/__init__.py +30 -0
- cars/applications/point_cloud_outlier_removal/abstract_outlier_removal_app.py +385 -0
- cars/applications/point_cloud_outlier_removal/outlier_removal_algo.py +392 -0
- cars/applications/point_cloud_outlier_removal/outlier_removal_constants.py +43 -0
- cars/applications/point_cloud_outlier_removal/small_components_app.py +527 -0
- cars/applications/point_cloud_outlier_removal/statistical_app.py +531 -0
- cars/applications/rasterization/__init__.py +30 -0
- cars/applications/rasterization/abstract_pc_rasterization_app.py +183 -0
- cars/applications/rasterization/rasterization_algo.py +534 -0
- cars/applications/rasterization/rasterization_constants.py +38 -0
- cars/applications/rasterization/rasterization_wrappers.py +634 -0
- cars/applications/rasterization/simple_gaussian_app.py +1152 -0
- cars/applications/resampling/__init__.py +28 -0
- cars/applications/resampling/abstract_resampling_app.py +187 -0
- cars/applications/resampling/bicubic_resampling_app.py +762 -0
- cars/applications/resampling/resampling_algo.py +614 -0
- cars/applications/resampling/resampling_constants.py +36 -0
- cars/applications/resampling/resampling_wrappers.py +309 -0
- cars/applications/sparse_matching/__init__.py +30 -0
- cars/applications/sparse_matching/abstract_sparse_matching_app.py +498 -0
- cars/applications/sparse_matching/sift_app.py +735 -0
- cars/applications/sparse_matching/sparse_matching_algo.py +360 -0
- cars/applications/sparse_matching/sparse_matching_constants.py +68 -0
- cars/applications/sparse_matching/sparse_matching_wrappers.py +238 -0
- cars/applications/triangulation/__init__.py +32 -0
- cars/applications/triangulation/abstract_triangulation_app.py +227 -0
- cars/applications/triangulation/line_of_sight_intersection_app.py +1243 -0
- cars/applications/triangulation/pc_transform.py +552 -0
- cars/applications/triangulation/triangulation_algo.py +371 -0
- cars/applications/triangulation/triangulation_constants.py +38 -0
- cars/applications/triangulation/triangulation_wrappers.py +259 -0
- cars/bundleadjustment.py +757 -0
- cars/cars.py +177 -0
- cars/conf/__init__.py +23 -0
- cars/conf/geoid/egm96.grd +0 -0
- cars/conf/geoid/egm96.grd.hdr +15 -0
- cars/conf/input_parameters.py +156 -0
- cars/conf/mask_cst.py +35 -0
- cars/core/__init__.py +23 -0
- cars/core/cars_logging.py +402 -0
- cars/core/constants.py +191 -0
- cars/core/constants_disparity.py +50 -0
- cars/core/datasets.py +140 -0
- cars/core/geometry/__init__.py +27 -0
- cars/core/geometry/abstract_geometry.py +1119 -0
- cars/core/geometry/shareloc_geometry.py +598 -0
- cars/core/inputs.py +568 -0
- cars/core/outputs.py +176 -0
- cars/core/preprocessing.py +722 -0
- cars/core/projection.py +843 -0
- cars/core/roi_tools.py +215 -0
- cars/core/tiling.py +774 -0
- cars/core/utils.py +164 -0
- cars/data_structures/__init__.py +23 -0
- cars/data_structures/cars_dataset.py +1541 -0
- cars/data_structures/cars_dict.py +74 -0
- cars/data_structures/corresponding_tiles_tools.py +186 -0
- cars/data_structures/dataframe_converter.py +185 -0
- cars/data_structures/format_transformation.py +297 -0
- cars/devibrate.py +689 -0
- cars/extractroi.py +264 -0
- cars/orchestrator/__init__.py +23 -0
- cars/orchestrator/achievement_tracker.py +125 -0
- cars/orchestrator/cluster/__init__.py +37 -0
- cars/orchestrator/cluster/abstract_cluster.py +244 -0
- cars/orchestrator/cluster/abstract_dask_cluster.py +375 -0
- cars/orchestrator/cluster/dask_cluster_tools.py +103 -0
- cars/orchestrator/cluster/dask_config/README.md +94 -0
- cars/orchestrator/cluster/dask_config/dask.yaml +21 -0
- cars/orchestrator/cluster/dask_config/distributed.yaml +70 -0
- cars/orchestrator/cluster/dask_config/jobqueue.yaml +26 -0
- cars/orchestrator/cluster/dask_config/reference_confs/dask-schema.yaml +137 -0
- cars/orchestrator/cluster/dask_config/reference_confs/dask.yaml +26 -0
- cars/orchestrator/cluster/dask_config/reference_confs/distributed-schema.yaml +1009 -0
- cars/orchestrator/cluster/dask_config/reference_confs/distributed.yaml +273 -0
- cars/orchestrator/cluster/dask_config/reference_confs/jobqueue.yaml +212 -0
- cars/orchestrator/cluster/dask_jobqueue_utils.py +204 -0
- cars/orchestrator/cluster/local_dask_cluster.py +116 -0
- cars/orchestrator/cluster/log_wrapper.py +1075 -0
- cars/orchestrator/cluster/mp_cluster/__init__.py +27 -0
- cars/orchestrator/cluster/mp_cluster/mp_factorizer.py +212 -0
- cars/orchestrator/cluster/mp_cluster/mp_objects.py +535 -0
- cars/orchestrator/cluster/mp_cluster/mp_tools.py +93 -0
- cars/orchestrator/cluster/mp_cluster/mp_wrapper.py +505 -0
- cars/orchestrator/cluster/mp_cluster/multiprocessing_cluster.py +873 -0
- cars/orchestrator/cluster/mp_cluster/multiprocessing_profiler.py +399 -0
- cars/orchestrator/cluster/pbs_dask_cluster.py +207 -0
- cars/orchestrator/cluster/sequential_cluster.py +139 -0
- cars/orchestrator/cluster/slurm_dask_cluster.py +234 -0
- cars/orchestrator/orchestrator.py +905 -0
- cars/orchestrator/orchestrator_constants.py +29 -0
- cars/orchestrator/registry/__init__.py +23 -0
- cars/orchestrator/registry/abstract_registry.py +143 -0
- cars/orchestrator/registry/compute_registry.py +106 -0
- cars/orchestrator/registry/id_generator.py +116 -0
- cars/orchestrator/registry/replacer_registry.py +213 -0
- cars/orchestrator/registry/saver_registry.py +363 -0
- cars/orchestrator/registry/unseen_registry.py +118 -0
- cars/orchestrator/tiles_profiler.py +279 -0
- cars/pipelines/__init__.py +26 -0
- cars/pipelines/conf_resolution/conf_final_resolution.yaml +5 -0
- cars/pipelines/conf_resolution/conf_first_resolution.yaml +2 -0
- cars/pipelines/conf_resolution/conf_intermediate_resolution.yaml +2 -0
- cars/pipelines/default/__init__.py +26 -0
- cars/pipelines/default/default_pipeline.py +786 -0
- cars/pipelines/parameters/__init__.py +0 -0
- cars/pipelines/parameters/advanced_parameters.py +417 -0
- cars/pipelines/parameters/advanced_parameters_constants.py +69 -0
- cars/pipelines/parameters/application_parameters.py +71 -0
- cars/pipelines/parameters/depth_map_inputs.py +0 -0
- cars/pipelines/parameters/dsm_inputs.py +918 -0
- cars/pipelines/parameters/dsm_inputs_constants.py +25 -0
- cars/pipelines/parameters/output_constants.py +52 -0
- cars/pipelines/parameters/output_parameters.py +454 -0
- cars/pipelines/parameters/sensor_inputs.py +842 -0
- cars/pipelines/parameters/sensor_inputs_constants.py +49 -0
- cars/pipelines/parameters/sensor_loaders/__init__.py +29 -0
- cars/pipelines/parameters/sensor_loaders/basic_classif_loader.py +86 -0
- cars/pipelines/parameters/sensor_loaders/basic_image_loader.py +98 -0
- cars/pipelines/parameters/sensor_loaders/pivot_classif_loader.py +90 -0
- cars/pipelines/parameters/sensor_loaders/pivot_image_loader.py +105 -0
- cars/pipelines/parameters/sensor_loaders/sensor_loader.py +93 -0
- cars/pipelines/parameters/sensor_loaders/sensor_loader_template.py +71 -0
- cars/pipelines/parameters/sensor_loaders/slurp_classif_loader.py +86 -0
- cars/pipelines/pipeline.py +119 -0
- cars/pipelines/pipeline_constants.py +31 -0
- cars/pipelines/pipeline_template.py +139 -0
- cars/pipelines/unit/__init__.py +26 -0
- cars/pipelines/unit/unit_pipeline.py +2850 -0
- cars/starter.py +167 -0
- cars-1.0.0rc1.dist-info/METADATA +292 -0
- cars-1.0.0rc1.dist-info/RECORD +202 -0
- cars-1.0.0rc1.dist-info/WHEEL +5 -0
- cars-1.0.0rc1.dist-info/entry_points.txt +8 -0
- cars.libs/libgcc_s-1257a076.so.1 +0 -0
- cars.libs/libstdc++-0530927c.so.6.0.32 +0 -0
cars/bundleadjustment.py
ADDED
|
@@ -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()
|