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