cars 1.0.0a1__cp313-cp313-win_amd64.whl → 1.0.0a3__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cars might be problematic. Click here for more details.

Files changed (81) hide show
  1. cars/__init__.py +4 -4
  2. cars/applications/application.py +14 -6
  3. cars/applications/application_template.py +22 -0
  4. cars/applications/auxiliary_filling/auxiliary_filling_from_sensors_app.py +15 -10
  5. cars/applications/auxiliary_filling/auxiliary_filling_wrappers.py +7 -6
  6. cars/applications/dem_generation/abstract_dem_generation_app.py +9 -5
  7. cars/applications/dem_generation/dem_generation_wrappers.py +48 -25
  8. cars/applications/dem_generation/dichotomic_generation_app.py +27 -9
  9. cars/applications/dem_generation/rasterization_app.py +85 -32
  10. cars/applications/dense_match_filling/abstract_dense_match_filling_app.py +4 -0
  11. cars/applications/dense_match_filling/cpp/dense_match_filling_cpp.cp313-win_amd64.dll.a +0 -0
  12. cars/applications/dense_match_filling/cpp/dense_match_filling_cpp.cp313-win_amd64.pyd +0 -0
  13. cars/applications/dense_match_filling/fill_disp_algo.py +41 -12
  14. cars/applications/dense_match_filling/plane_app.py +11 -0
  15. cars/applications/dense_match_filling/zero_padding_app.py +11 -1
  16. cars/applications/dense_matching/census_mccnn_sgm_app.py +254 -548
  17. cars/applications/dense_matching/cpp/dense_matching_cpp.cp313-win_amd64.dll.a +0 -0
  18. cars/applications/dense_matching/cpp/dense_matching_cpp.cp313-win_amd64.pyd +0 -0
  19. cars/applications/dense_matching/dense_matching_algo.py +59 -11
  20. cars/applications/dense_matching/dense_matching_wrappers.py +51 -31
  21. cars/applications/dense_matching/disparity_grid_algo.py +566 -0
  22. cars/applications/dense_matching/loaders/config_mapping.json +13 -0
  23. cars/applications/dense_matching/loaders/global_land_cover_map.tif +0 -0
  24. cars/applications/dense_matching/loaders/pandora_loader.py +78 -1
  25. cars/applications/dsm_filling/border_interpolation_app.py +10 -5
  26. cars/applications/dsm_filling/bulldozer_filling_app.py +14 -7
  27. cars/applications/dsm_filling/exogenous_filling_app.py +10 -5
  28. cars/applications/grid_generation/grid_correction_app.py +0 -53
  29. cars/applications/grid_generation/transform_grid.py +5 -5
  30. cars/applications/point_cloud_fusion/pc_fusion_algo.py +17 -11
  31. cars/applications/point_cloud_fusion/pc_fusion_wrappers.py +3 -4
  32. cars/applications/point_cloud_outlier_removal/abstract_outlier_removal_app.py +9 -5
  33. cars/applications/point_cloud_outlier_removal/small_components_app.py +5 -3
  34. cars/applications/point_cloud_outlier_removal/statistical_app.py +4 -2
  35. cars/applications/rasterization/abstract_pc_rasterization_app.py +1 -0
  36. cars/applications/rasterization/rasterization_algo.py +20 -27
  37. cars/applications/rasterization/rasterization_wrappers.py +6 -5
  38. cars/applications/rasterization/simple_gaussian_app.py +30 -17
  39. cars/applications/resampling/resampling_algo.py +44 -49
  40. cars/applications/sparse_matching/sift_app.py +2 -22
  41. cars/applications/sparse_matching/sparse_matching_wrappers.py +0 -49
  42. cars/applications/triangulation/line_of_sight_intersection_app.py +1 -1
  43. cars/applications/triangulation/triangulation_wrappers.py +2 -1
  44. cars/bundleadjustment.py +51 -11
  45. cars/cars.py +15 -5
  46. cars/core/constants.py +1 -1
  47. cars/core/geometry/abstract_geometry.py +166 -12
  48. cars/core/geometry/shareloc_geometry.py +61 -14
  49. cars/core/inputs.py +15 -0
  50. cars/core/projection.py +117 -0
  51. cars/data_structures/cars_dataset.py +7 -5
  52. cars/orchestrator/cluster/log_wrapper.py +1 -1
  53. cars/orchestrator/cluster/mp_cluster/multiprocessing_cluster.py +1 -1
  54. cars/orchestrator/orchestrator.py +1 -1
  55. cars/orchestrator/registry/saver_registry.py +0 -78
  56. cars/pipelines/default/default_pipeline.py +69 -52
  57. cars/pipelines/parameters/advanced_parameters.py +17 -0
  58. cars/pipelines/parameters/advanced_parameters_constants.py +4 -0
  59. cars/pipelines/parameters/depth_map_inputs.py +22 -67
  60. cars/pipelines/parameters/dsm_inputs.py +16 -29
  61. cars/pipelines/parameters/output_parameters.py +44 -8
  62. cars/pipelines/parameters/sensor_inputs.py +117 -24
  63. cars/pipelines/parameters/sensor_loaders/basic_sensor_loader.py +3 -3
  64. cars/pipelines/parameters/sensor_loaders/pivot_sensor_loader.py +2 -2
  65. cars/pipelines/parameters/sensor_loaders/sensor_loader.py +4 -6
  66. cars/pipelines/parameters/sensor_loaders/sensor_loader_template.py +2 -2
  67. cars/pipelines/pipeline.py +8 -8
  68. cars/pipelines/unit/unit_pipeline.py +276 -274
  69. cars/starter.py +20 -1
  70. cars-1.0.0a3.dist-info/DELVEWHEEL +2 -0
  71. {cars-1.0.0a1.dist-info → cars-1.0.0a3.dist-info}/METADATA +3 -2
  72. {cars-1.0.0a1.dist-info → cars-1.0.0a3.dist-info}/RECORD +77 -74
  73. cars.libs/libgcc_s_seh-1-ca70890bbc5723b6d0ea31e9c9cded2b.dll +0 -0
  74. cars.libs/libstdc++-6-00ee19f73d5122a1277c137b1c218401.dll +0 -0
  75. cars.libs/libwinpthread-1-f5042e8e3d21edce20c1bc99445f551b.dll +0 -0
  76. cars-1.0.0a1.dist-info/DELVEWHEEL +0 -2
  77. cars.libs/libgcc_s_seh-1-f2b6825d483bdf14050493af93b5997d.dll +0 -0
  78. cars.libs/libstdc++-6-6b0059df6bc601df5a0f18a5805eea05.dll +0 -0
  79. cars.libs/libwinpthread-1-e01b8e85fd67c2b861f64d4ccc7df607.dll +0 -0
  80. {cars-1.0.0a1.dist-info → cars-1.0.0a3.dist-info}/WHEEL +0 -0
  81. {cars-1.0.0a1.dist-info → cars-1.0.0a3.dist-info}/entry_points.txt +0 -0
@@ -18,6 +18,9 @@
18
18
  # See the License for the specific language governing permissions and
19
19
  # limitations under the License.
20
20
  #
21
+
22
+ # pylint: disable=C0302
23
+
21
24
  """
22
25
  this module contains the abstract geometry class to use in the
23
26
  geometry plugins
@@ -30,7 +33,7 @@ from typing import Dict, List, Tuple, Union
30
33
  import numpy as np
31
34
  import rasterio as rio
32
35
  import xarray as xr
33
- from json_checker import And, Checker
36
+ from json_checker import And, Checker, Or
34
37
  from scipy import interpolate
35
38
  from scipy.interpolate import LinearNDInterpolator
36
39
  from shapely.geometry import Polygon
@@ -50,7 +53,7 @@ class AbstractGeometry(metaclass=ABCMeta):
50
53
 
51
54
  available_plugins: Dict = {}
52
55
 
53
- def __new__(cls, geometry_plugin_conf=None, **kwargs):
56
+ def __new__(cls, geometry_plugin_conf=None, scaling_coeff=1, **kwargs):
54
57
  """
55
58
  Return the required plugin
56
59
  :raises:
@@ -59,13 +62,17 @@ class AbstractGeometry(metaclass=ABCMeta):
59
62
  :param geometry_plugin_conf: plugin name or plugin configuration
60
63
  to instantiate
61
64
  :type geometry_plugin_conf: str or dict
65
+ :param scaling_coeff: scaling factor for resolution
66
+ :type scaling_coeff: float
62
67
  :return: a geometry_plugin object
63
68
  """
64
69
  if geometry_plugin_conf is not None:
65
70
  if isinstance(geometry_plugin_conf, str):
66
71
  geometry_plugin = geometry_plugin_conf
67
72
  elif isinstance(geometry_plugin_conf, dict):
68
- geometry_plugin = geometry_plugin_conf.get("plugin_name", None)
73
+ geometry_plugin = geometry_plugin_conf.get(
74
+ "plugin_name", "SharelocGeometry"
75
+ )
69
76
  else:
70
77
  raise RuntimeError("Not a supported type")
71
78
 
@@ -98,13 +105,17 @@ class AbstractGeometry(metaclass=ABCMeta):
98
105
  dem=None,
99
106
  geoid=None,
100
107
  default_alt=None,
108
+ scaling_coeff=1,
101
109
  **kwargs,
102
110
  ):
103
111
 
112
+ self.scaling_coeff = scaling_coeff
113
+
104
114
  config = self.check_conf(geometry_plugin_conf)
105
115
 
106
116
  self.plugin_name = config["plugin_name"]
107
117
  self.interpolator = config["interpolator"]
118
+ self.dem_roi_margin = config["dem_roi_margin"]
108
119
 
109
120
  self.dem = dem
110
121
  self.dem_roi = None
@@ -153,12 +164,18 @@ class AbstractGeometry(metaclass=ABCMeta):
153
164
  conf = {"plugin_name": conf}
154
165
 
155
166
  # overload conf
156
- overloaded_conf["plugin_name"] = conf.get("plugin_name", None)
167
+ overloaded_conf["plugin_name"] = conf.get(
168
+ "plugin_name", "SharelocGeometry"
169
+ )
157
170
  overloaded_conf["interpolator"] = conf.get("interpolator", "cubic")
171
+ overloaded_conf["dem_roi_margin"] = conf.get(
172
+ "dem_roi_margin", float(self.scaling_coeff * 0.012)
173
+ )
158
174
 
159
175
  geometry_schema = {
160
176
  "plugin_name": str,
161
177
  "interpolator": And(str, lambda x: x in ["cubic", "linear"]),
178
+ "dem_roi_margin": Or(float, int),
162
179
  }
163
180
 
164
181
  # Check conf
@@ -372,7 +389,6 @@ class AbstractGeometry(metaclass=ABCMeta):
372
389
  array of size [number of points, 2]. The last index indicates
373
390
  the 'x' coordinate (last index set to 0) or the 'y' coordinate
374
391
  (last index set to 1).
375
- :param interpolator: interpolator to use
376
392
  :return: sensors positions as a numpy array of size
377
393
  [number of points, 2]. The last index indicates the 'x'
378
394
  coordinate (last index set to 0) or
@@ -387,6 +403,9 @@ class AbstractGeometry(metaclass=ABCMeta):
387
403
  f"Grid type {type(grid)} not a dict or RectificationGrid"
388
404
  )
389
405
 
406
+ # Ensure positions is a numpy array
407
+ positions = np.asarray(positions)
408
+
390
409
  # Get data
391
410
  with rio.open(grid["path"]) as grid_data:
392
411
  row_dep = grid_data.read(2)
@@ -403,17 +422,42 @@ class AbstractGeometry(metaclass=ABCMeta):
403
422
  cols = np.arange(ori_col, last_col, step_col)
404
423
  rows = np.arange(ori_row, last_row, step_row)
405
424
 
406
- # create regular grid points positions
407
- sensor_row_positions = row_dep
408
- sensor_col_positions = col_dep
425
+ # Determine margin based on interpolator type
426
+ margin = 6 if self.interpolator == "cubic" else 3
427
+
428
+ # Find the bounds of positions to determine crop region
429
+ min_col = np.nanmin(positions[:, 0])
430
+ max_col = np.nanmax(positions[:, 0])
431
+ min_row = np.nanmin(positions[:, 1])
432
+ max_row = np.nanmax(positions[:, 1])
433
+
434
+ # Convert position bounds to grid indices with margin
435
+ min_col_idx = max(0, int((min_col - ori_col) / step_col) - margin)
436
+ max_col_idx = min(
437
+ len(cols) - 1, int((max_col - ori_col) / step_col) + margin
438
+ )
439
+ min_row_idx = max(0, int((min_row - ori_row) / step_row) - margin)
440
+ max_row_idx = min(
441
+ len(rows) - 1, int((max_row - ori_row) / step_row) + margin
442
+ )
443
+
444
+ # Crop the grids and coordinate arrays
445
+ cols_cropped = cols[min_col_idx : max_col_idx + 1]
446
+ rows_cropped = rows[min_row_idx : max_row_idx + 1]
447
+ sensor_row_positions_cropped = row_dep[
448
+ min_row_idx : max_row_idx + 1, min_col_idx : max_col_idx + 1
449
+ ]
450
+ sensor_col_positions_cropped = col_dep[
451
+ min_row_idx : max_row_idx + 1, min_col_idx : max_col_idx + 1
452
+ ]
409
453
 
410
454
  # interpolate sensor positions
411
455
  interpolator = interpolate.RegularGridInterpolator(
412
- (cols, rows),
456
+ (cols_cropped, rows_cropped),
413
457
  np.stack(
414
458
  (
415
- sensor_row_positions.transpose(),
416
- sensor_col_positions.transpose(),
459
+ sensor_row_positions_cropped.transpose(),
460
+ sensor_col_positions_cropped.transpose(),
417
461
  ),
418
462
  axis=2,
419
463
  ),
@@ -583,6 +627,55 @@ class AbstractGeometry(metaclass=ABCMeta):
583
627
  :return: Latitude, Longitude, Altitude coordinates list as a numpy array
584
628
  """
585
629
 
630
+ def safe_direct_loc(
631
+ self,
632
+ sensor,
633
+ geomodel,
634
+ x_coord: np.array,
635
+ y_coord: np.array,
636
+ z_coord: np.array = None,
637
+ ) -> np.ndarray:
638
+ """
639
+ For a given image points list, compute the latitudes,
640
+ longitudes, altitudes
641
+
642
+ Advice: to be sure, use x,y,z list inputs only
643
+
644
+ :param sensor: path to sensor image
645
+ :param geomodel: path and attributes for geomodel
646
+ :param x_coord: X Coordinates list in input image sensor
647
+ :param y_coord: Y Coordinate list in input image sensor
648
+ :param z_coord: Z Altitude list coordinate to take the image
649
+ :return: Latitude, Longitude, Altitude coordinates list as a numpy array
650
+ """
651
+ if len(x_coord) > 0:
652
+ ground_points = self.direct_loc(
653
+ sensor,
654
+ geomodel,
655
+ x_coord,
656
+ y_coord,
657
+ z_coord,
658
+ )
659
+ else:
660
+ logging.warning("Direct loc function launched on empty list")
661
+ return []
662
+ if z_coord is None:
663
+ status = np.any(np.isnan(ground_points), axis=0)
664
+ if sum(status) > 0:
665
+ logging.warning(
666
+ "{} errors have been detected on direct "
667
+ "loc and will be re-launched".format(sum(status))
668
+ )
669
+ ground_points_retry = self.direct_loc(
670
+ sensor,
671
+ geomodel,
672
+ x_coord[status],
673
+ y_coord[status],
674
+ np.array([0]),
675
+ )
676
+ ground_points[:, status] = ground_points_retry
677
+ return ground_points
678
+
586
679
  @abstractmethod
587
680
  def inverse_loc(
588
681
  self,
@@ -606,8 +699,65 @@ class AbstractGeometry(metaclass=ABCMeta):
606
699
  :return: X / Y / Z Coordinates list in input image as a numpy array
607
700
  """
608
701
 
702
+ def safe_inverse_loc(
703
+ self,
704
+ sensor,
705
+ geomodel,
706
+ lat_coord: np.array,
707
+ lon_coord: np.array,
708
+ z_coord: np.array = None,
709
+ ) -> np.ndarray:
710
+ """
711
+ For a given image points list, compute the latitudes,
712
+ longitudes, altitudes
713
+
714
+ Advice: to be sure, use x,y,z list inputs only
715
+
716
+ :param sensor: path to sensor image
717
+ :param geomodel: path and attributes for geomodel
718
+ :param lat_coord: latitute Coordinate list
719
+ :param lon_coord: longitude Coordinates list
720
+ :param z_coord: Z Altitude list
721
+ :return: X / Y / Z Coordinates list in input image as a numpy array
722
+ """
723
+ if len(lat_coord) > 0:
724
+ image_points = self.inverse_loc(
725
+ sensor,
726
+ geomodel,
727
+ lat_coord,
728
+ lon_coord,
729
+ z_coord,
730
+ )
731
+ image_points = np.array(image_points)
732
+ else:
733
+ logging.warning("Inverse loc function launched on empty list")
734
+ return [], [], []
735
+ if z_coord is None:
736
+ image_points = np.array(image_points)
737
+ status = np.any(np.isnan(image_points), axis=0)
738
+ if sum(status) > 0:
739
+ logging.warning(
740
+ "{} errors have been detected on inverse "
741
+ "loc and will be re-launched".format(sum(status))
742
+ )
743
+ image_points_retry = self.inverse_loc(
744
+ sensor,
745
+ geomodel,
746
+ lat_coord[status],
747
+ lon_coord[status],
748
+ np.array([self.default_alt]),
749
+ )
750
+
751
+ image_points[:, status] = image_points_retry
752
+ return image_points[0], image_points[1], image_points[2]
753
+
609
754
  def image_envelope(
610
- self, sensor, geomodel, out_path=None, out_driver="ESRI Shapefile"
755
+ self,
756
+ sensor,
757
+ geomodel,
758
+ out_path=None,
759
+ out_driver="ESRI Shapefile",
760
+ elevation=None,
611
761
  ):
612
762
  """
613
763
  Export the image footprint to a vector file
@@ -629,24 +779,28 @@ class AbstractGeometry(metaclass=ABCMeta):
629
779
  geomodel,
630
780
  np.array(shift_x),
631
781
  np.array(shift_y),
782
+ elevation,
632
783
  )
633
784
  lat_upper_right, lon_upper_right, _ = self.direct_loc(
634
785
  sensor,
635
786
  geomodel,
636
787
  np.array(img_size_x + shift_x),
637
788
  np.array(shift_y),
789
+ elevation,
638
790
  )
639
791
  lat_bottom_left, lon_bottom_left, _ = self.direct_loc(
640
792
  sensor,
641
793
  geomodel,
642
794
  np.array(shift_x),
643
795
  np.array(img_size_y + shift_y),
796
+ elevation,
644
797
  )
645
798
  lat_bottom_right, lon_bottom_right, _ = self.direct_loc(
646
799
  sensor,
647
800
  geomodel,
648
801
  np.array(img_size_x + shift_x),
649
802
  np.array(img_size_y + shift_y),
803
+ elevation,
650
804
  )
651
805
 
652
806
  u_l = (lon_upper_left, lat_upper_left)
@@ -63,6 +63,7 @@ class SharelocGeometry(AbstractGeometry):
63
63
  geoid=None,
64
64
  default_alt=None,
65
65
  pairs_for_roi=None,
66
+ scaling_coeff=1,
66
67
  ):
67
68
 
68
69
  super().__init__(
@@ -71,6 +72,7 @@ class SharelocGeometry(AbstractGeometry):
71
72
  geoid=geoid,
72
73
  default_alt=default_alt,
73
74
  pairs_for_roi=pairs_for_roi,
75
+ scaling_coeff=scaling_coeff,
74
76
  )
75
77
 
76
78
  self.dem_roi = None
@@ -91,7 +93,11 @@ class SharelocGeometry(AbstractGeometry):
91
93
  self.dem_roi_epsg = inputs.rasterio_get_epsg(dem)
92
94
 
93
95
  self.roi_shareloc = self.get_roi(
94
- pairs_for_roi, self.dem_roi_epsg, margin=0.012
96
+ pairs_for_roi,
97
+ self.dem_roi_epsg,
98
+ z_min=0,
99
+ z_max=0,
100
+ margin=self.dem_roi_margin,
95
101
  )
96
102
  # change convention
97
103
  self.dem_roi = [
@@ -105,14 +111,26 @@ class SharelocGeometry(AbstractGeometry):
105
111
 
106
112
  # fill_nodata option should be set when dealing with void in DTM
107
113
  # see shareloc DTM limitations in sphinx doc for further details
108
- dtm_image = dtm_reader(
109
- dem,
110
- geoid,
111
- roi=self.roi_shareloc,
112
- roi_is_in_physical_space=True,
113
- fill_nodata="mean",
114
- fill_value=0.0,
115
- )
114
+
115
+ try:
116
+ dtm_image = dtm_reader(
117
+ dem,
118
+ geoid,
119
+ roi=self.roi_shareloc,
120
+ roi_is_in_physical_space=True,
121
+ fill_nodata="mean",
122
+ fill_value=0.0,
123
+ )
124
+ except RuntimeError as err:
125
+ mss = "the roi bounds are"
126
+ if mss in str(err):
127
+ new_except_mss = (
128
+ f"The extent of the roi lies outside "
129
+ f"the extent of the initial elevation : {err}"
130
+ )
131
+ raise RuntimeError(new_except_mss) from err
132
+ raise
133
+
116
134
  self.elevation = (
117
135
  bindings_cpp.DTMIntersection( # pylint: disable=I1101
118
136
  dtm_image.epsg,
@@ -125,7 +143,7 @@ class SharelocGeometry(AbstractGeometry):
125
143
  else:
126
144
  self.elevation = default_alt
127
145
 
128
- def get_roi(self, pairs_for_roi, epsg, margin=0.006):
146
+ def get_roi(self, pairs_for_roi, epsg, z_min=0, z_max=0, margin=0.012):
129
147
  """
130
148
  Compute region of interest for intersection of DEM
131
149
 
@@ -138,27 +156,56 @@ class SharelocGeometry(AbstractGeometry):
138
156
  """
139
157
  coords_list = []
140
158
  for image1, geomodel1, image2, geomodel2 in pairs_for_roi:
141
- # Footprint of left image
159
+ # Footprint of left image with altitude z_min
142
160
  coords_list.extend(
143
- self.image_envelope(image1["main_file"], geomodel1)
161
+ self.image_envelope(
162
+ image1["main_file"], geomodel1, elevation=z_min
163
+ )
144
164
  )
145
- # Footprint of right image
165
+ # Footprint of left image with altitude z_max
146
166
  coords_list.extend(
147
- self.image_envelope(image2["main_file"], geomodel2)
167
+ self.image_envelope(
168
+ image1["main_file"], geomodel1, elevation=z_max
169
+ )
170
+ )
171
+ # Footprint of right image with altitude z_min
172
+ coords_list.extend(
173
+ self.image_envelope(
174
+ image2["main_file"], geomodel2, elevation=z_min
175
+ )
176
+ )
177
+ # Footprint of right image with altitude z_max
178
+ coords_list.extend(
179
+ self.image_envelope(
180
+ image2["main_file"], geomodel2, elevation=z_max
181
+ )
148
182
  )
149
183
  # Footprint of rectification grid (with margins)
150
184
  image1 = SharelocGeometry.load_image(image1["main_file"])
151
185
  geomodel1 = self.load_geom_model(geomodel1)
152
186
  geomodel2 = self.load_geom_model(geomodel2)
187
+
188
+ # With altitude z_min
153
189
  epipolar_extent = rectif.get_epipolar_extent(
154
190
  image1,
155
191
  geomodel1,
156
192
  geomodel2,
193
+ elevation=z_min,
157
194
  grid_margin=self.rectification_grid_margin,
158
195
  )
159
196
  lat_min, lon_min, lat_max, lon_max = list(epipolar_extent)
160
197
  coords_list.extend([(lon_min, lat_min), (lon_max, lat_max)])
161
198
 
199
+ # With altitude z_max
200
+ epipolar_extent = rectif.get_epipolar_extent(
201
+ image1,
202
+ geomodel1,
203
+ geomodel2,
204
+ elevation=z_max,
205
+ grid_margin=self.rectification_grid_margin,
206
+ )
207
+ lat_min, lon_min, lat_max, lon_max = list(epipolar_extent)
208
+ coords_list.extend([(lon_min, lat_min), (lon_max, lat_max)])
162
209
  lon_list, lat_list = list(zip(*coords_list)) # noqa: B905
163
210
  roi = [
164
211
  min(lat_list) - margin,
cars/core/inputs.py CHANGED
@@ -36,6 +36,7 @@ import numpy as np
36
36
  import rasterio as rio
37
37
  import xarray as xr
38
38
  from json_checker import Checker
39
+ from pyproj import CRS
39
40
  from rasterio.warp import Resampling, calculate_default_transform, reproject
40
41
  from rasterio.windows import Window
41
42
  from shapely.geometry import shape
@@ -378,6 +379,20 @@ def rasterio_get_epsg(raster_file: str) -> int:
378
379
  return epsg
379
380
 
380
381
 
382
+ def rasterio_get_crs(raster_file: str) -> CRS:
383
+ """
384
+ Get the crs of an image file
385
+
386
+ :param raster_file: Image file
387
+ :return: The crs of the given image
388
+ """
389
+ crs = None
390
+ with rio.open(raster_file, "r") as descriptor:
391
+ crs = descriptor.crs
392
+
393
+ return crs
394
+
395
+
381
396
  def rasterio_transform_epsg(file_name, new_epsg):
382
397
  """
383
398
  Modify epsg of raster file
cars/core/projection.py CHANGED
@@ -182,6 +182,22 @@ def polygon_projection(poly: Polygon, epsg_in: int, epsg_out: int) -> Polygon:
182
182
  return poly
183
183
 
184
184
 
185
+ def polygon_projection_crs(poly: Polygon, crs_in: CRS, crs_out: CRS) -> Polygon:
186
+ """
187
+ Projects a polygon from an initial crs to another
188
+
189
+ :param poly: poly to project
190
+ :param crs_in: initial crs
191
+ :param crs_out: final crs
192
+ :return: The polygon in the final projection
193
+ """
194
+ # Project polygon between CRS (keep always_xy for compatibility)
195
+ project = pyproj.Transformer.from_crs(crs_in, crs_out, always_xy=True)
196
+ poly = transform(project.transform, poly)
197
+
198
+ return poly
199
+
200
+
185
201
  def geo_to_ecef(
186
202
  lat: np.ndarray, lon: np.ndarray, alt: np.ndarray
187
203
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
@@ -351,6 +367,27 @@ def point_cloud_conversion(
351
367
  return cloud_in
352
368
 
353
369
 
370
+ def point_cloud_conversion_crs(
371
+ cloud_in: np.ndarray, crs_in: int, crs_out: int
372
+ ) -> np.ndarray:
373
+ """
374
+ Convert a point cloud from a SRS to another one.
375
+
376
+ :param cloud_in: cloud to project
377
+ :param crs_in: crs of the input SRS
378
+ :param crs_out: crs of the output SRS
379
+ :return: Projected point cloud
380
+ """
381
+ # Project point cloud between CRS (keep always_xy for compatibility)
382
+ cloud_in = np.array(cloud_in).T
383
+ transformer = pyproj.Transformer.from_crs(crs_in, crs_out, always_xy=True)
384
+
385
+ cloud_in = transformer.transform(*cloud_in)
386
+ cloud_in = np.array(cloud_in).T
387
+
388
+ return cloud_in
389
+
390
+
354
391
  def get_xyz_np_array_from_dataset(
355
392
  cloud_in: xr.Dataset,
356
393
  ) -> Tuple[np.array, List[int]]:
@@ -721,3 +758,83 @@ def get_ground_angles(
721
758
  convergence_angle = np.degrees(utils.angle_vectors(enu1, enu2))
722
759
 
723
760
  return az1, elev_angle1, az2, elev_angle2, convergence_angle
761
+
762
+
763
+ def get_output_crs(epsg, out_conf):
764
+ """
765
+ Détermine le CRS de sortie en fonction de la config.
766
+ """
767
+ geoid = out_conf.get("geoid")
768
+ crs_epsg = CRS(f"EPSG:{epsg}")
769
+
770
+ if len(crs_epsg.axis_info) != 2:
771
+ return crs_epsg # the user himself set a 3D CRS
772
+
773
+ geoid_is_path = isinstance(geoid, str)
774
+
775
+ if geoid_is_path: # user given geoid
776
+ vepsg = guess_vcrs_from_file_name(geoid)
777
+ if vepsg is None:
778
+ custom_wkt = (
779
+ 'VERTCRS["Custom geoid height",'
780
+ + f' VDATUM["Custom geoid model (file: {geoid})"],'
781
+ + " CS[vertical,1],"
782
+ + ' AXIS["gravity-related height (h)", up],'
783
+ + ' LENGTHUNIT["metre", 1, ID["EPSG", 9001]]'
784
+ "]"
785
+ )
786
+ logging.warning(
787
+ "Could not create a known VCRS from the geoid file."
788
+ )
789
+ return CRS.from_wkt(
790
+ f'COMPOUNDCRS["EPSG:{epsg} + Custom geoid height",'
791
+ f" {crs_epsg.to_wkt()},"
792
+ f" {custom_wkt}]"
793
+ )
794
+ # a vepsg was found using the geoid file
795
+ return CRS(f"EPSG:{epsg}+{vepsg}")
796
+
797
+ if geoid: # geoid == True
798
+ return CRS(f"EPSG:{epsg}+5773")
799
+
800
+ # geoid == False
801
+ wgs84_wkt = (
802
+ 'VERTCRS["WGS 84 ellipsoidal height",'
803
+ + ' VDATUM["WGS 84"],'
804
+ + " CS[vertical,1],"
805
+ + ' AXIS["ellipsoidal height (h)", up],'
806
+ + ' LENGTHUNIT["metre", 1, ID["EPSG", 9001]]'
807
+ "]"
808
+ )
809
+ logging.warning("The output VCRS is WGS84.")
810
+
811
+ return CRS.from_wkt(
812
+ f'COMPOUNDCRS["EPSG:{epsg} + WGS84 ellipsoidal height",'
813
+ f" {crs_epsg.to_wkt()},"
814
+ f" {wgs84_wkt}]"
815
+ )
816
+
817
+
818
+ def guess_vcrs_from_file_name(filepath):
819
+ """
820
+ Tries to detect the geoid's EPSG from the file name
821
+ """
822
+ filename = os.path.basename(filepath).lower()
823
+
824
+ known_models = {
825
+ "egm96": 5773, # EGM96 height
826
+ "egm_96": 5773, # alias
827
+ "egm 96": 5773, # alias
828
+ "egm1996": 5773, # alias
829
+ "egm08": 3855, # EGM2008 height
830
+ "egm_08": 3855, # alias
831
+ "egm 08": 3855, # alias
832
+ "egm2008": 3855, # alias
833
+ }
834
+
835
+ for key, vepsg in known_models.items():
836
+ if key in filename:
837
+ return vepsg
838
+
839
+ # aucun match connu
840
+ return None
@@ -38,6 +38,7 @@ from typing import Dict
38
38
  # Third party imports
39
39
  import numpy as np
40
40
  import pandas
41
+ import pyproj
41
42
  import rasterio as rio
42
43
  import xarray as xr
43
44
  from rasterio.profiles import DefaultGTiffProfile
@@ -1327,12 +1328,13 @@ def dict_profile_to_rio_profile(dict_profile: Dict) -> Dict:
1327
1328
  crs = None
1328
1329
  if "crs" in dict_profile:
1329
1330
  if dict_profile["crs"] is not None:
1330
- if isinstance(dict_profile["crs"], str):
1331
- crs = rio.crs.CRS.from_epsg(
1332
- dict_profile["crs"].replace("EPSG:", "")
1333
- )
1331
+ if (
1332
+ isinstance(dict_profile["crs"], str)
1333
+ and "EPSG:" in dict_profile["crs"]
1334
+ ):
1335
+ crs = pyproj.CRS(dict_profile["crs"].replace("EPSG:", ""))
1334
1336
  else:
1335
- crs = rio.crs.CRS.from_epsg(dict_profile["crs"])
1337
+ crs = pyproj.CRS(dict_profile["crs"])
1336
1338
 
1337
1339
  rio_profile["crs"] = crs
1338
1340
  rio_profile["transform"] = transform
@@ -21,7 +21,7 @@
21
21
  """
22
22
  Contains functions for wrapper logs
23
23
  """
24
- # pylint: disable=too-many-lines
24
+ # pylint: disable=C0302
25
25
 
26
26
  import copy
27
27
  import cProfile
@@ -21,7 +21,7 @@
21
21
  """
22
22
  Contains abstract function for multiprocessing Cluster
23
23
  """
24
- # pylint: disable=too-many-lines
24
+ # pylint: disable=C0302
25
25
 
26
26
  import copy
27
27
  import itertools
@@ -22,7 +22,7 @@
22
22
  this module contains the orchestrator class
23
23
  """
24
24
 
25
- # pylint: disable=too-many-lines
25
+ # pylint: disable=C0302
26
26
 
27
27
  import collections
28
28
  import logging