samgis_core 3.0.4__py3-none-any.whl → 3.0.6__py3-none-any.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.
@@ -37,7 +37,7 @@ from samgis_core.utilities.utilities import convert_ndarray_to_pil, apply_coords
37
37
 
38
38
  class SegmentAnythingONNX2:
39
39
  """
40
- Segmentation model using SegmentAnything.
40
+ Segmentation model using Segment Anything.
41
41
  Compatible with onnxruntime 1.17.x and later
42
42
  """
43
43
 
@@ -68,8 +68,17 @@ class SegmentAnythingONNX2:
68
68
  )
69
69
 
70
70
  @staticmethod
71
- def get_input_points(prompt: ListDict):
72
- """Get input points"""
71
+ def get_input_points(prompt: ListDict) -> tuple[ndarray]:
72
+ """
73
+ Get input points from a prompt dict list.
74
+
75
+ Args:
76
+ prompt: dict list
77
+
78
+ Returns:
79
+ tuple of points, labels ndarray ready for Segment Anything inference
80
+
81
+ """
73
82
  points = []
74
83
  labels = []
75
84
  for mark in prompt:
@@ -95,11 +104,12 @@ class SegmentAnythingONNX2:
95
104
 
96
105
  Returns:
97
106
  embedding image dict useful to store and cache image embeddings
107
+
98
108
  """
99
109
  resized_image = self.preprocess_image(img)
100
110
  padded_input_tensor = self.padding_tensor(resized_image)
101
111
 
102
- # 2. GET IMAGE EMBEDDINGS USING IMAGE ENCODER
112
+ # 2. GET IMAGE EMBEDDINGS USING IMAGE ENCODER (`size` argument here is like ndarray `shape`)
103
113
  outputs = self.encoder_session.run(None, {"images": padded_input_tensor})
104
114
  image_embedding = outputs[0]
105
115
  img = convert_ndarray_to_pil(img)
@@ -109,9 +119,17 @@ class SegmentAnythingONNX2:
109
119
  "resized_size": resized_image.size
110
120
  }
111
121
 
112
- def predict_masks(self, embedding: EmbeddingPILImage, prompt: ListDict):
122
+ def predict_masks(self, embedding: EmbeddingPILImage, prompt: ListDict) -> ndarray:
113
123
  """
114
124
  Predict masks for a single image.
125
+
126
+ Args:
127
+ embedding: input image embedding dict
128
+ prompt: Segment Anything input prompt
129
+
130
+ Returns:
131
+ prediction masks ndarray; this should have (1, 1, **image.shape) shape
132
+
115
133
  """
116
134
  input_points, input_labels = self.get_input_points(prompt)
117
135
 
@@ -136,8 +154,17 @@ class SegmentAnythingONNX2:
136
154
  })
137
155
  return output_masks
138
156
 
139
- def preprocess_image(self, img: PIL_Image | ndarray):
140
- """Resize image preserving aspect ratio using 'output_size_target' as a long side"""
157
+ def preprocess_image(self, img: PIL_Image | ndarray) -> ndarray:
158
+ """
159
+ Resize image preserving aspect ratio using `output_size_target` as a long side.
160
+
161
+ Args:
162
+ img: input ndarray/PIL image
163
+
164
+ Returns:
165
+ image ndarray
166
+
167
+ """
141
168
  from PIL import Image
142
169
 
143
170
  app_logger.info(f"image type:{type(img)}, shape/size:{img.size}.")
@@ -157,7 +184,17 @@ class SegmentAnythingONNX2:
157
184
  img = img.resize((resized_width, resized_height), Image.Resampling.BILINEAR)
158
185
  return img
159
186
 
160
- def padding_tensor(self, img: PIL_Image | ndarray):
187
+ def padding_tensor(self, img: PIL_Image | ndarray) -> ndarray:
188
+ """
189
+ Pad an image ndarray/tensor to given instance self.target_size
190
+
191
+ Args:
192
+ img: input ndarray/PIL image
193
+
194
+ Returns:
195
+ image ndarray
196
+
197
+ """
161
198
  # Prepare input tensor from image
162
199
  tensor_input = np_array(img)
163
200
  resized_width, resized_height = img.size
@@ -20,6 +20,7 @@ def get_raster_inference(
20
20
 
21
21
  Returns:
22
22
  raster prediction mask, prediction number
23
+
23
24
  """
24
25
  np_img = np_array(img)
25
26
  app_logger.info(f"img type {type(np_img)}, prompt:{prompt}.")
@@ -49,6 +50,7 @@ def get_inference_embedding(
49
50
 
50
51
  Returns:
51
52
  raster dict
53
+
52
54
  """
53
55
  if embedding_key in embedding_dict:
54
56
  app_logger.info("found embedding in dict...")
@@ -68,7 +70,8 @@ def get_inference_embedding(
68
70
 
69
71
 
70
72
  def get_raster_inference_using_existing_embedding(
71
- embedding: dict, prompt: ListDict, models_instance: SegmentAnythingONNX2) -> TupleNdarrayInt:
73
+ embedding: dict, prompt: ListDict, models_instance: SegmentAnythingONNX2, folder_write_tmp_on_disk: str = None,
74
+ key: str = None) -> TupleNdarrayInt:
72
75
  """
73
76
  Get inference output for a given image using a SegmentAnythingONNX model, using an existing embedding instead of a
74
77
  new ndarray or PIL image
@@ -77,9 +80,12 @@ def get_raster_inference_using_existing_embedding(
77
80
  embedding: dict
78
81
  prompt: list of prompt dict
79
82
  models_instance: SegmentAnythingONNX instance model
83
+ folder_write_tmp_on_disk: output folder where to write debug images
84
+ key: embedding key
80
85
 
81
86
  Returns:
82
87
  raster prediction mask, prediction number
88
+
83
89
  """
84
90
  app_logger.info(f"using existing embedding of type {type(embedding)}.")
85
91
  inference_out = models_instance.predict_masks(embedding, prompt)
@@ -87,16 +93,27 @@ def get_raster_inference_using_existing_embedding(
87
93
  app_logger.info(f"Created {len_inference_out} prediction_masks,"
88
94
  f"shape:{inference_out.shape}, dtype:{inference_out.dtype}.")
89
95
  mask = zeros((inference_out.shape[2], inference_out.shape[3]), dtype=uint8)
96
+ write_tmp_img = bool(folder_write_tmp_on_disk)
90
97
  for n, m in enumerate(inference_out[0, :, :, :]):
91
98
  app_logger.debug(f"{n}th of prediction_masks shape {inference_out.shape}"
92
99
  f" => mask shape:{mask.shape}, {mask.dtype}.")
93
100
  mask[m > 0.0] = 255
101
+ if write_tmp_img:
102
+ from pathlib import Path
103
+ from datetime import datetime
104
+ from samgis_core.utilities.utilities import convert_ndarray_to_pil, normalize_array
105
+ m_normalized = normalize_array(m, type_normalization="float")
106
+ m_out = convert_ndarray_to_pil(m_normalized)
107
+ now = datetime.now().isoformat()
108
+ if len(m.shape) == 2:
109
+ m_out = m_out.convert("L")
110
+ m_out.save(Path(folder_write_tmp_on_disk) / f"mask_{key}_{now}_n{n}.png")
94
111
  return mask, len_inference_out
95
112
 
96
113
 
97
114
  def get_raster_inference_with_embedding_from_dict(
98
115
  img: PIL_Image | ndarray, prompt: ListDict, models_instance: SegmentAnythingONNX2, model_name: str,
99
- embedding_key: str, embedding_dict: dict) -> TupleNdarrayInt:
116
+ embedding_key: str, embedding_dict: dict, folder_write_tmp_on_disk: str = None) -> TupleNdarrayInt:
100
117
  """
101
118
  Get inference output using a SegmentAnythingONNX model, but get the image embedding from the given embedding dict
102
119
  instead of creating a new embedding. This function needs the img argument to update the embedding dict if necessary
@@ -108,9 +125,11 @@ def get_raster_inference_with_embedding_from_dict(
108
125
  model_name: model name string
109
126
  embedding_key: embedding id
110
127
  embedding_dict: embedding images dict
128
+ folder_write_tmp_on_disk: output folder where to write debug images
111
129
 
112
130
  Returns:
113
131
  raster prediction mask, prediction number
132
+
114
133
  """
115
134
  app_logger.info(f"handling embedding using key {embedding_key}.")
116
135
  embedding_dict = get_inference_embedding(img, models_instance, model_name, embedding_key, embedding_dict)
@@ -118,4 +137,5 @@ def get_raster_inference_with_embedding_from_dict(
118
137
  embedding = embedding_dict[embedding_key]
119
138
  n_keys = len(embedding_dict)
120
139
  app_logger.info(f"embedding created ({n_keys} keys in embedding dict), running predict_masks with prompt {prompt}.")
121
- return get_raster_inference_using_existing_embedding(embedding, prompt, models_instance)
140
+ return get_raster_inference_using_existing_embedding(
141
+ embedding, prompt, models_instance, folder_write_tmp_on_disk=folder_write_tmp_on_disk, key=embedding_key)
@@ -10,6 +10,14 @@ def stats_pathname(pathname: Path | str):
10
10
 
11
11
 
12
12
  def create_folder_if_not_exists(pathname: Path | str):
13
+ """Create a folder given its path.
14
+
15
+ Args:
16
+ pathname: folder Path or string
17
+
18
+ Returns:
19
+
20
+ """
13
21
  current_pathname = Path(pathname)
14
22
  try:
15
23
  print(f"Pathname exists? {current_pathname.exists()}, That's a folder? {current_pathname.is_dir()}...")
@@ -32,6 +40,18 @@ def create_folder_if_not_exists(pathname: Path | str):
32
40
 
33
41
 
34
42
  def folders_creation(folders_map: dict | str = None, ignore_errors: bool = True):
43
+ """Create all folders listed within the folders_map argument (this argument can be a dict or a json string).
44
+ If folders_map is None the function will try to load the 'FOLDERS_MAP' env variable, then will load that json into
45
+ dict. Once loaded and parsed the folders_map variable, the function will loop over the dict to create the folders
46
+ using the `create_folder_if_not_exists()` function.
47
+
48
+ Args:
49
+ folders_map: dict or string map of folder string
50
+ ignore_errors: bool needed to eventually ignore errors on folder creation
51
+
52
+ Returns:
53
+
54
+ """
35
55
  enforce_validation_with_getenv = folders_map is None
36
56
  if enforce_validation_with_getenv:
37
57
  folders_map = os.getenv("FOLDERS_MAP")
@@ -1,11 +1,99 @@
1
- def helper_imshow_output_expected(img1, img2, title1, title2):
2
- from matplotlib import pyplot as plt
1
+ import structlog.stdlib
2
+ from numpy import ndarray
3
+ from matplotlib import pyplot as plt
3
4
 
5
+ from samgis_core.utilities.type_hints import ListStr, MatplotlibBackend
6
+
7
+ FigAxes = tuple[plt.Figure, plt.Axes]
8
+
9
+
10
+ logger = structlog.stdlib.get_logger(__file__)
11
+
12
+
13
+ def helper_imshow_output_expected(
14
+ img_list: list[ndarray], titles_list: ListStr, cmap: str = "gist_rainbow", plot_size: int = 5,
15
+ show=False, debug: bool = False, close_after: float = 0.0) -> FigAxes:
16
+ """
17
+ Simple way to display a list of images with their titles, color map.
18
+ Should work also in an automate environments, like tests (use a `close_after` argument > 0)
19
+
20
+ Args:
21
+ img_list: ndarray images to display
22
+ titles_list: title images
23
+ cmap: color map
24
+ plot_size: figure plot size
25
+ show: fire plt.show() action if needed
26
+ debug: workaround useful in an interactive context, like Pycharm debugger
27
+ close_after: close after give seconds (useful in tests, contrasted to 'debug' option)
28
+
29
+ Returns:
30
+ tuple of matplotlib Figure, Axes
31
+
32
+ """
33
+ n = len(img_list)
34
+ assert len(titles_list) == n
4
35
  fig, ax = plt.subplot_mosaic([
5
- [title1, title2]
6
- ], figsize=(15, 10))
36
+ titles_list
37
+ ], figsize=(n * plot_size, plot_size))
38
+
39
+ for title, img in zip(titles_list, img_list):
40
+ ax[title].imshow(img, cmap=cmap)
41
+ ax[title].legend()
42
+ if show:
43
+ if debug:
44
+ plt.pause(0.01)
45
+ plt.show()
46
+ if close_after > 0:
47
+ plt.pause(close_after)
48
+ plt.show(block=False)
49
+ plt.close("all")
50
+ return fig, ax
51
+
52
+
53
+ def imshow_raster(
54
+ raster, title, cmap: str = "gist_rainbow", interpolation: str = None, alpha=None, transform=None, plot_size=5,
55
+ show=False, debug: bool = False, close_after: float = 0.0, backend: MatplotlibBackend = None) -> FigAxes:
56
+ """
57
+ Displays raster images lists/arrays with titles, legend, alpha transparency, figure sizes
58
+ and geographic transformations, if not none (leveraging rasterio.plot)
59
+
60
+ Args:
61
+ raster: image to display
62
+ title: title image
63
+ cmap: color map
64
+ interpolation: interpolation type
65
+ alpha: alpha transparency
66
+ transform: geographic transform, eventually used for map representation by rasterio
67
+ plot_size: figure plot size
68
+ show: fire plt.show() action if needed
69
+ debug: workaround useful in an interactive context, like Pycharm debugger
70
+ close_after: close after give seconds (useful in tests, contrasted to 'debug' option)
71
+ backend: matplotlib backend string
72
+
73
+ Returns:
74
+ tuple of matplotlib Figure, Axes
75
+
76
+ """
77
+ from rasterio import plot
7
78
 
8
- ax[title1].imshow(img1)
9
- ax[title2].imshow(img2)
79
+ if not backend:
80
+ backend = plt.get_backend()
81
+ plt.rcParams["backend"] = backend
82
+ logger.info(f"use {backend} as matplotlib backend...")
10
83
 
11
- plt.show()
84
+ fig, ax = plt.subplots(figsize=(plot_size, plot_size))
85
+ raster_ax = raster[0] if transform is not None else raster
86
+ image_hidden = ax.imshow(raster_ax, cmap=cmap, interpolation=interpolation, alpha=alpha)
87
+ if transform is not None:
88
+ plot.show(raster, transform=transform, ax=ax, cmap=cmap, interpolation=interpolation, alpha=alpha)
89
+ fig.colorbar(image_hidden, ax=ax)
90
+ ax.set_title(title)
91
+ if show:
92
+ if debug:
93
+ plt.pause(0.01)
94
+ plt.show()
95
+ if close_after > 0:
96
+ plt.pause(close_after)
97
+ plt.show(block=False)
98
+ plt.close("all")
99
+ return fig, ax
@@ -27,6 +27,20 @@ def drop_color_message_key(_, __, event_dict: EventDict) -> EventDict:
27
27
 
28
28
 
29
29
  def setup_logging(json_logs: bool = False, log_level: str = "INFO"):
30
+ """Enhance the configuration of structlog.
31
+ Needed for correlation id injection with fastapi middleware in samgis-web.
32
+ After the use of logging_middleware() in samgis_web.web.middlewares, add also the CorrelationIdMiddleware from
33
+ 'asgi_correlation_id' package. (See 'tests/web/test_middlewares.py' in samgis_web).
34
+ To change an input parameter like the log level, re-run the function changing the parameter
35
+ (no need to re-instantiate the logger instance: it's a hot change)
36
+
37
+ Args:
38
+ json_logs: set logs in json format
39
+ log_level: log level string
40
+
41
+ Returns:
42
+
43
+ """
30
44
  timestamper = structlog.processors.TimeStamper(fmt="iso")
31
45
 
32
46
  shared_processors: list[Processor] = [
@@ -67,5 +67,32 @@ class EmbeddingPILImage(TypedDict):
67
67
  resized_size: TupleInt2
68
68
 
69
69
 
70
+ class MatplotlibBackend(StrEnum):
71
+ gtk3agg = "gtk3agg"
72
+ gtk3cairo = "gtk3cairo"
73
+ gtk4agg = "gtk4agg"
74
+ gtk4cairo = "gtk4cairo"
75
+ macosx = "macosx"
76
+ nbagg = "nbagg"
77
+ notebook = "notebook"
78
+ qtagg = "qtagg"
79
+ qtcairo = "qtcairo"
80
+ qt5agg = "qt5agg"
81
+ qt5cairo = "qt5cairo"
82
+ tkagg = "tkagg"
83
+ tkcairo = "tkcairo"
84
+ webagg = "webagg"
85
+ wx = "wx"
86
+ wxagg = "wxagg"
87
+ wxcairo = "wxcairo"
88
+ agg = "agg"
89
+ cairo = "cairo"
90
+ pdf = "pdf"
91
+ pgf = "pgf"
92
+ ps = "ps"
93
+ svg = "svg"
94
+ template = "template"
95
+
96
+
70
97
  EmbeddingDict = dict[str, EmbeddingImage]
71
98
  EmbeddingPILDict = dict[str, EmbeddingPILImage]
@@ -1,6 +1,7 @@
1
1
  """Various utilities (logger, time benchmark, args dump, numerical and stats info)"""
2
2
  from copy import deepcopy
3
3
 
4
+ import numpy as np
4
5
  from numpy import ndarray, float32
5
6
 
6
7
  from samgis_core import app_logger
@@ -98,6 +99,16 @@ def hash_calculate(arr) -> str | bytes:
98
99
 
99
100
 
100
101
  def convert_ndarray_to_pil(pil_image: PIL_Image | ndarray):
102
+ """
103
+ Check if an image is a ndarray and then convert to a PIL Image instance.
104
+
105
+ Args:
106
+ pil_image: PIL image or ndarray
107
+
108
+ Returns:
109
+ PIL Image
110
+
111
+ """
101
112
  from PIL import Image
102
113
 
103
114
  if isinstance(pil_image, ndarray):
@@ -109,6 +120,14 @@ def apply_coords(coords: ndarray, embedding: EmbeddingPILImage):
109
120
  """
110
121
  Expects a numpy np_array of length 2 in the final dimension. Requires the
111
122
  original image size in (H, W) format.
123
+
124
+ Args:
125
+ coords: coordinates ndarray
126
+ embedding: PIL image embedding dict
127
+
128
+ Returns:
129
+ coordinates ndarray
130
+
112
131
  """
113
132
  orig_width, orig_height = embedding["original_size"]
114
133
  resized_width, resized_height = embedding["resized_size"]
@@ -118,3 +137,12 @@ def apply_coords(coords: ndarray, embedding: EmbeddingPILImage):
118
137
  coords[..., 1] = coords[..., 1] * (resized_height / orig_height)
119
138
 
120
139
  return coords.astype(float32)
140
+
141
+
142
+ def normalize_array(arr: ndarray, new_h: int | float = 255., type_normalization: str = "int") -> ndarray:
143
+ arr = arr.astype(float)
144
+ arr_max = np.nanmax(arr)
145
+ arr_min = np.nanmin(arr)
146
+ scaled_arr = (arr - arr_min) / (arr_max - arr_min)
147
+ multiplied_arr = scaled_arr * new_h
148
+ return multiplied_arr.astype(int) if type_normalization == "int" else multiplied_arr
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: samgis_core
3
- Version: 3.0.4
3
+ Version: 3.0.6
4
4
  Summary: SamGIS CORE
5
5
  License: MIT
6
6
  Author: alessandro trinca tornidor
@@ -0,0 +1,17 @@
1
+ samgis_core/__init__.py,sha256=4e1lkiD-cLi_YLjJ4DUpu8LrUgjCHfhO_wnOGyEv_m0,673
2
+ samgis_core/__version__.py,sha256=x-8LeQkljky-ao1MyT98tSQE1xNvJp3LelqsAdCb9Og,94
3
+ samgis_core/prediction_api/__init__.py,sha256=_jUZhspS26ygiSzBJywfZ4fQf9X7oC8w6oxvhd9S2hQ,57
4
+ samgis_core/prediction_api/sam_onnx2.py,sha256=8mORGMl_pE6YjOKGz6L7Lsav_xGteOUma3GzQXIbTFU,8260
5
+ samgis_core/prediction_api/sam_onnx_inference.py,sha256=WFRc8TNMOix4g0IAteNbDAs040lWXC3YEZYHcnCHRcE,6493
6
+ samgis_core/utilities/__init__.py,sha256=nL9pzdB4SdEF8m5gCbtlVCtdGLg9JjPm-FNxKBsIBZA,32
7
+ samgis_core/utilities/constants.py,sha256=0xBdfGYwCg4O0OXFtTcMVNj-kryjbajcxOZhMVkVP7U,227
8
+ samgis_core/utilities/create_folders_if_not_exists.py,sha256=Ee_4l83suZvY5r24D26SNKGJVH3KAHregsiWs-0W-gQ,3202
9
+ samgis_core/utilities/plot_images.py,sha256=1pwTJC2_ju-QhJXDonvEw6FMK_9i1kQhLnC4tjn2EH0,3469
10
+ samgis_core/utilities/serialize.py,sha256=aIjhEoibBpV_gpgOg6LiVxZCWjOkYxlzcboDZLQctJE,2689
11
+ samgis_core/utilities/session_logger.py,sha256=mlzLaeSC2b_MF3wLywyXYQsSPIvEd3OnhlD7_4VhMzo,5704
12
+ samgis_core/utilities/type_hints.py,sha256=anbm8pHFWr_C1upCmncYOK-q9Iq82fO9_yftTqXeizc,1649
13
+ samgis_core/utilities/utilities.py,sha256=4EBnsTBontnmaZRVtI4VAbvISdmYyg2yEnn8Sb28dUc,4014
14
+ samgis_core-3.0.6.dist-info/LICENSE,sha256=cmg7mi2IynvK5xYN_TJBikA008n6IJNjQIig1c3ge9Q,1083
15
+ samgis_core-3.0.6.dist-info/METADATA,sha256=cF143cyh8z5hhFc3_ZXad95rLjfin9prTKMBBteZLfo,1109
16
+ samgis_core-3.0.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
17
+ samgis_core-3.0.6.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- samgis_core/__init__.py,sha256=4e1lkiD-cLi_YLjJ4DUpu8LrUgjCHfhO_wnOGyEv_m0,673
2
- samgis_core/__version__.py,sha256=x-8LeQkljky-ao1MyT98tSQE1xNvJp3LelqsAdCb9Og,94
3
- samgis_core/prediction_api/__init__.py,sha256=_jUZhspS26ygiSzBJywfZ4fQf9X7oC8w6oxvhd9S2hQ,57
4
- samgis_core/prediction_api/sam_onnx2.py,sha256=-nt6xfeLuUCB9GtJD74o53Ks3zPVgBWZb_-JogeFYwo,7439
5
- samgis_core/prediction_api/sam_onnx_inference.py,sha256=EQjJy1RLZn0-m66bdY_T1bR-t6PnNu47JJVphEkgPhE,5539
6
- samgis_core/utilities/__init__.py,sha256=nL9pzdB4SdEF8m5gCbtlVCtdGLg9JjPm-FNxKBsIBZA,32
7
- samgis_core/utilities/constants.py,sha256=0xBdfGYwCg4O0OXFtTcMVNj-kryjbajcxOZhMVkVP7U,227
8
- samgis_core/utilities/create_folders_if_not_exists.py,sha256=yCxuCUTAB9FG_6CBhN5vHScs7MLU_Myn6-tHbI_OpuI,2510
9
- samgis_core/utilities/plot_images.py,sha256=nY_1KW7x_h218MA9leulEKLkoNhLlyEscLSWs0Az2uE,263
10
- samgis_core/utilities/serialize.py,sha256=aIjhEoibBpV_gpgOg6LiVxZCWjOkYxlzcboDZLQctJE,2689
11
- samgis_core/utilities/session_logger.py,sha256=P_H6LcMmNoD-cuwl9mMLl73KYz1kfLQhYiKY37NujcI,5095
12
- samgis_core/utilities/type_hints.py,sha256=YhTSsD7ICVZtLRoq-P6O5AP9BEGg5hSHJaboS1qbpuw,1101
13
- samgis_core/utilities/utilities.py,sha256=tRGp-Iw0PoPf5YHDL6Hx2CEdZhqs03hh3YPe_rmoO-E,3286
14
- samgis_core-3.0.4.dist-info/LICENSE,sha256=cmg7mi2IynvK5xYN_TJBikA008n6IJNjQIig1c3ge9Q,1083
15
- samgis_core-3.0.4.dist-info/METADATA,sha256=xmhlFTAxcFoVrca7dhkRyMNosm2EdbV1WIJ1dSBtlaQ,1109
16
- samgis_core-3.0.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
17
- samgis_core-3.0.4.dist-info/RECORD,,