mapreader 1.3.5__tar.gz → 1.3.6__tar.gz

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.
Files changed (52) hide show
  1. {mapreader-1.3.5 → mapreader-1.3.6}/PKG-INFO +2 -1
  2. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/__init__.py +25 -10
  3. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/_version.py +3 -3
  4. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/annotate/utils.py +4 -1
  5. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/classifier.py +33 -33
  6. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/load_annotations.py +13 -4
  7. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/data_structures.py +14 -6
  8. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/downloader.py +20 -6
  9. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/downloader_utils.py +8 -2
  10. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/sheet_downloader.py +13 -1
  11. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/tile_merging.py +4 -3
  12. mapreader-1.3.5/mapreader/process/post_process.py → mapreader-1.3.6/mapreader/process/context_post_process.py +1 -1
  13. mapreader-1.3.6/mapreader/process/occlusion_analysis.py +307 -0
  14. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/spot_text/deepsolo_runner.py +32 -8
  15. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/spot_text/dptext_detr_runner.py +25 -5
  16. mapreader-1.3.6/mapreader/spot_text/maptext_runner.py +511 -0
  17. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/spot_text/runner_base.py +63 -1
  18. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/PKG-INFO +2 -1
  19. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/SOURCES.txt +4 -5
  20. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/requires.txt +1 -0
  21. {mapreader-1.3.5 → mapreader-1.3.6}/setup.py +1 -0
  22. {mapreader-1.3.5 → mapreader-1.3.6}/tests/test_geo_pipeline.py +1 -1
  23. mapreader-1.3.5/tests/test_annotator.py +0 -276
  24. mapreader-1.3.5/tests/test_post_processing.py +0 -171
  25. mapreader-1.3.5/tests/test_sheet_downloader.py +0 -767
  26. {mapreader-1.3.5 → mapreader-1.3.6}/LICENSE +0 -0
  27. {mapreader-1.3.5 → mapreader-1.3.6}/README.md +0 -0
  28. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/__main__.py +0 -0
  29. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/annotate/__init__.py +0 -0
  30. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/annotate/annotator.py +0 -0
  31. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/__init__.py +0 -0
  32. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/custom_models.py +0 -0
  33. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/datasets.py +0 -0
  34. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/__init__.py +0 -0
  35. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/tile_loading.py +0 -0
  36. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/load/__init__.py +0 -0
  37. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/load/geo_utils.py +0 -0
  38. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/load/images.py +0 -0
  39. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/load/loader.py +0 -0
  40. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/process/__init__.py +0 -0
  41. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/process/process.py +0 -0
  42. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/spot_text/__init__.py +0 -0
  43. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/utils/__init__.py +0 -0
  44. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/utils/compute_and_save_stats.py +0 -0
  45. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/utils/slice_parallel.py +0 -0
  46. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/dependency_links.txt +0 -0
  47. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/entry_points.txt +0 -0
  48. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/not-zip-safe +0 -0
  49. {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/top_level.txt +0 -0
  50. {mapreader-1.3.5 → mapreader-1.3.6}/setup.cfg +0 -0
  51. {mapreader-1.3.5 → mapreader-1.3.6}/tests/test_import.py +0 -0
  52. {mapreader-1.3.5 → mapreader-1.3.6}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mapreader
3
- Version: 1.3.5
3
+ Version: 1.3.6
4
4
  Summary: A computer vision pipeline for the semantic exploration of maps/images at scale
5
5
  Home-page: https://github.com/Living-with-machines/MapReader
6
6
  Download-URL: https://github.com/Living-with-machines/MapReader/archive/refs/heads/main.zip
@@ -58,6 +58,7 @@ Requires-Dist: geopandas<1.0.0
58
58
  Requires-Dist: pyogrio>=0.7.2
59
59
  Requires-Dist: cartopy>=0.22.0
60
60
  Requires-Dist: joblib>=1.4.0
61
+ Requires-Dist: opencv-python<5.0.0.0
61
62
  Provides-Extra: dev
62
63
  Requires-Dist: pytest<9.0.0; extra == "dev"
63
64
  Requires-Dist: pytest-cov<6.0.0,>=4.1.0; extra == "dev"
@@ -1,17 +1,25 @@
1
+ # download
2
+ from mapreader.download.sheet_downloader import SheetDownloader
3
+ from mapreader.download.downloader import Downloader
4
+ from mapreader.download.downloader_utils import create_polygon_from_latlons, create_line_from_latlons
5
+
6
+
7
+ # load
1
8
  from mapreader.load.images import MapImages
2
9
  from mapreader.load.loader import loader
3
10
  from mapreader.load.loader import load_patches
4
11
 
5
- from mapreader.download.sheet_downloader import SheetDownloader
6
- from mapreader.download.downloader import Downloader
7
- from mapreader.download.downloader_utils import create_polygon_from_latlons, create_line_from_latlons
12
+ # annotate
13
+ from mapreader.annotate.annotator import Annotator
8
14
 
15
+ # classify
9
16
  from mapreader.classify.load_annotations import AnnotationsLoader
10
17
  from mapreader.classify.datasets import PatchDataset
11
18
  from mapreader.classify.datasets import PatchContextDataset
12
19
  from mapreader.classify.classifier import ClassifierContainer
13
20
  from mapreader.classify import custom_models
14
21
 
22
+ # spot_text
15
23
  try:
16
24
  from mapreader.spot_text.deepsolo_runner import DeepSoloRunner
17
25
  except ImportError:
@@ -22,19 +30,26 @@ try:
22
30
  except ImportError:
23
31
  pass
24
32
 
25
- from mapreader.process import process
26
-
27
- from mapreader.annotate.annotator import Annotator
33
+ try:
34
+ from mapreader.spot_text.maptext_runner import MapTextRunner
35
+ except ImportError:
36
+ pass
28
37
 
29
- from . import _version
30
38
 
31
- __version__ = _version.get_versions()["version"]
39
+ # post process
40
+ from mapreader.process.context_post_process import ContextPostProcessor
41
+ from mapreader.process.occlusion_analysis import OcclusionAnalyzer
32
42
 
43
+ # utils
33
44
  from mapreader.load import geo_utils
34
45
 
35
- import mapreader
36
46
 
47
+ # version
48
+ from . import _version
49
+ __version__ = _version.get_versions()["version"]
50
+
51
+ import mapreader
37
52
 
38
53
  def print_version():
39
54
  """Print the current version of mapreader."""
40
- print(mapreader.__version__)
55
+ print(mapreader.__version__)
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-08-02T15:22:56+0100",
11
+ "date": "2024-08-09T14:10:51+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "49ac0f1daccf0da74b0570db6ade09578debb164",
15
- "version": "1.3.5"
14
+ "full-revisionid": "5ba6c479346a58dc6d937776c895b30d6c21f833",
15
+ "version": "1.3.6"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -310,7 +310,10 @@ def annotation_interface(
310
310
  # stream=True so we don't download the whole page, only check if
311
311
  # the page exists
312
312
  response = requests.get(url, stream=True)
313
- assert response.status_code < 400
313
+ if not response.status_code < 400:
314
+ raise RuntimeError(
315
+ f"URL could not get a response: {response.status_code}"
316
+ )
314
317
  print()
315
318
  print(f"URL: {url}")
316
319
  except:
@@ -86,8 +86,8 @@ class ClassifierContainer:
86
86
  The optimizer being used for training the model.
87
87
  scheduler : None or torch.optim.lr_scheduler._LRScheduler
88
88
  The learning rate scheduler being used for training the model.
89
- criterion : None or nn.modules.loss._Loss
90
- The criterion to use for training the model.
89
+ loss_fn : None or nn.modules.loss._Loss
90
+ The loss function to use for training the model.
91
91
  metrics : dict
92
92
  A dictionary to store the metrics computed during training.
93
93
  last_epoch : int
@@ -159,7 +159,7 @@ class ClassifierContainer:
159
159
 
160
160
  self.optimizer = None
161
161
  self.scheduler = None
162
- self.criterion = None
162
+ self.loss_fn = None
163
163
 
164
164
  self.metrics = {}
165
165
  self.last_epoch = 0
@@ -399,54 +399,54 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
399
399
 
400
400
  self.scheduler = scheduler
401
401
 
402
- def add_criterion(
403
- self, criterion: str | nn.modules.loss._Loss | None = "cross entropy"
402
+ def add_loss_fn(
403
+ self, loss_fn: str | nn.modules.loss._Loss | None = "cross entropy"
404
404
  ) -> None:
405
405
  """
406
- Add a loss criterion to the classifier object.
406
+ Add a loss function to the classifier object.
407
407
 
408
408
  Parameters
409
409
  ----------
410
- criterion : str or torch.nn.modules.loss._Loss
411
- The loss criterion to add to the classifier object.
410
+ loss_fn : str or torch.nn.modules.loss._Loss
411
+ The loss function to add to the classifier object.
412
412
  Accepted string values are "cross entropy" or "ce" (cross-entropy), "bce" (binary cross-entropy) and "mse" (mean squared error).
413
413
 
414
414
  Returns
415
415
  -------
416
416
  None
417
- The function only modifies the ``criterion`` attribute of the
417
+ The function only modifies the ``loss_fn`` attribute of the
418
418
  classifier and does not return anything.
419
419
  """
420
- if isinstance(criterion, str):
421
- if criterion in ["cross entropy", "ce", "cross_entropy", "cross-entropy"]:
422
- criterion = nn.CrossEntropyLoss()
423
- elif criterion in [
420
+ if isinstance(loss_fn, str):
421
+ if loss_fn in ["cross entropy", "ce", "cross_entropy", "cross-entropy"]:
422
+ loss_fn = nn.CrossEntropyLoss()
423
+ elif loss_fn in [
424
424
  "bce",
425
425
  "binary_cross_entropy",
426
426
  "binary cross entropy",
427
427
  "binary cross-entropy",
428
428
  ]:
429
- criterion = nn.BCELoss()
430
- elif criterion in [
429
+ loss_fn = nn.BCELoss()
430
+ elif loss_fn in [
431
431
  "mse",
432
432
  "mean_square_error",
433
433
  "mean_squared_error",
434
434
  "mean squared error",
435
435
  ]:
436
- criterion = nn.MSELoss()
436
+ loss_fn = nn.MSELoss()
437
437
  else:
438
438
  raise NotImplementedError(
439
- '[ERROR] At present, if passing ``criterion`` as a string, criterion can only be "cross entropy" or "ce" (cross-entropy), "bce" (binary cross-entropy) or "mse" (mean squared error).'
439
+ '[ERROR] At present, if passing ``loss_fn`` as a string, the loss function can only be "cross entropy" or "ce" (cross-entropy), "bce" (binary cross-entropy) or "mse" (mean squared error).'
440
440
  )
441
441
 
442
- print(f'[INFO] Using "{criterion}" as criterion.')
442
+ print(f'[INFO] Using "{loss_fn}" as loss function.')
443
443
 
444
- elif not isinstance(criterion, nn.modules.loss._Loss):
444
+ elif not isinstance(loss_fn, nn.modules.loss._Loss):
445
445
  raise ValueError(
446
- '[ERROR] Please pass ``criterion`` as a string ("cross entropy", "bce" or "mse") or torch.nn loss function (see https://pytorch.org/docs/stable/nn.html).'
446
+ '[ERROR] Please pass ``loss_fn`` as a string ("cross entropy", "bce" or "mse") or torch.nn loss function (see https://pytorch.org/docs/stable/nn.html).'
447
447
  )
448
448
 
449
- self.criterion = criterion
449
+ self.loss_fn = loss_fn
450
450
 
451
451
  def model_summary(
452
452
  self,
@@ -643,7 +643,7 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
643
643
 
644
644
  def train_component_summary(self) -> None:
645
645
  """
646
- Print a summary of the optimizer, criterion, and trainable model
646
+ Print a summary of the optimizer, loss function, and trainable model
647
647
  components.
648
648
 
649
649
  Returns:
@@ -655,8 +655,8 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
655
655
  print("* Optimizer:")
656
656
  print(str(self.optimizer))
657
657
  print(divider)
658
- print("* Criterion:")
659
- print(str(self.criterion))
658
+ print("* Loss function:")
659
+ print(str(self.loss_fn))
660
660
  print(divider)
661
661
  print("* Model:")
662
662
  self.model_summary(trainable_col=True)
@@ -783,9 +783,9 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
783
783
  Raises
784
784
  ------
785
785
  ValueError
786
- If the criterion is not set. Use the
787
- :meth:`~.classify.classifier.ClassifierContainer.add_criterion`
788
- method to set the criterion.
786
+ If the loss function is not set. Use the
787
+ :meth:`~.classify.classifier.ClassifierContainer.add_loss_fn`
788
+ method to set the loss function.
789
789
 
790
790
  If the optimizer is not set and the phase is "train". Use the
791
791
  :meth:`~.classify.classifier.ClassifierContainer.initialize_optimizer`
@@ -894,10 +894,10 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to add one." # noqa
894
894
  # summing the final output and the auxiliary
895
895
  # output but in testing we only consider the
896
896
  # final output.
897
- if self.criterion is None:
897
+ if self.loss_fn is None:
898
898
  raise ValueError(
899
- "[ERROR] Criterion is not yet defined.\n\n\
900
- Use ``add_criterion`` to define one."
899
+ "[ERROR] Loss function is not yet defined.\n\n\
900
+ Use ``add_loss_fn`` to define one."
901
901
  )
902
902
 
903
903
  if self.is_inception and (
@@ -910,8 +910,8 @@ Use ``add_criterion`` to define one."
910
910
  if not isinstance(aux_outputs, torch.Tensor):
911
911
  aux_outputs = self._get_logits(aux_outputs)
912
912
 
913
- loss1 = self.criterion(outputs, label_indices)
914
- loss2 = self.criterion(aux_outputs, label_indices)
913
+ loss1 = self.loss_fn(outputs, label_indices)
914
+ loss2 = self.loss_fn(aux_outputs, label_indices)
915
915
  # https://discuss.pytorch.org/t/how-to-optimize-inception-model-with-auxiliary-classifiers/7958
916
916
  loss = loss1 + 0.4 * loss2
917
917
 
@@ -921,7 +921,7 @@ Use ``add_criterion`` to define one."
921
921
  if not isinstance(outputs, torch.Tensor):
922
922
  outputs = self._get_logits(outputs)
923
923
 
924
- loss = self.criterion(outputs, label_indices)
924
+ loss = self.loss_fn(outputs, label_indices)
925
925
 
926
926
  _, pred_label_indices = torch.max(outputs, dim=1)
927
927
 
@@ -451,10 +451,13 @@ Please check your image paths and update them if necessary.'
451
451
  self.reviewed.loc[input_id, "label_index"] = self._get_label_index(
452
452
  input_label
453
453
  )
454
- assert (
454
+ if not (
455
455
  self.annotations[self.label_col].value_counts().tolist()
456
456
  == self.annotations["label_index"].value_counts().tolist()
457
- )
457
+ ):
458
+ raise RuntimeError(
459
+ f"[ERROR] Label indices do not match label counts. Please check the label indices for label '{input_label}'."
460
+ )
458
461
  print(
459
462
  f'[INFO] Image {input_id} has been relabelled as "{input_label}"'
460
463
  )
@@ -607,12 +610,18 @@ Please check your image paths and update them if necessary.'
607
610
  test_size=float(relative_frac_test),
608
611
  random_state=random_state,
609
612
  )
610
- assert len(self.annotations) == len(df_train) + len(df_val) + len(df_test)
613
+ if not len(self.annotations) == len(df_train) + len(df_val) + len(df_test):
614
+ raise ValueError(
615
+ "[ERROR] Number of annotations in the split dataframes does not match the number of annotations in the original dataframe."
616
+ )
611
617
 
612
618
  else:
613
619
  df_val = df_temp
614
620
  df_test = None
615
- assert len(self.annotations) == len(df_train) + len(df_val)
621
+ if not len(self.annotations) == len(df_train) + len(df_val):
622
+ raise ValueError(
623
+ "[ERROR] Number of annotations in the split dataframes does not match the number of annotations in the original dataframe."
624
+ )
616
625
 
617
626
  if context_datasets:
618
627
  datasets = self.create_patch_context_datasets(
@@ -14,8 +14,11 @@ class Coordinate:
14
14
  lon : float
15
15
  longitude value (in range [-180°, 180°] )
16
16
  """
17
- assert -90 <= lat <= 90
18
- assert -180 <= lon <= 180
17
+ if not -90 <= lat <= 90:
18
+ raise ValueError("Latitude must be in range [-90, 90]")
19
+ if not -180 <= lon <= 180:
20
+ raise ValueError("Longitude must be in range [-180, 180]")
21
+
19
22
  self.lat = lat
20
23
  self.lon = lon
21
24
 
@@ -37,9 +40,12 @@ class GridIndex:
37
40
  z : int
38
41
  Zoom level
39
42
  """
40
- assert z >= 0
41
- assert 0 <= x < 2**z
42
- assert 0 <= y < 2**z
43
+ if not z >= 0:
44
+ raise ValueError("Zoom level must be greater than or equal to 0")
45
+ if not 0 <= x < 2**z:
46
+ raise ValueError(f"X value must be in range [0, {2**z}]")
47
+ if not 0 <= y < 2**z:
48
+ raise ValueError(f"Y value must be in range [0, {2**z}]")
43
49
  self.x = x
44
50
  self.y = y
45
51
  self.z = z
@@ -61,7 +67,9 @@ class GridBoundingBox:
61
67
  cell1 : GridIndex
62
68
  cell2 : GridIndex
63
69
  """
64
- assert cell1.z == cell2.z, "Can't calculate a grid on different scales yet"
70
+ if cell1.z != cell2.z:
71
+ raise NotImplementedError("Can't calculate a grid on different scales yet")
72
+
65
73
  start_x = min(cell1.x, cell2.x)
66
74
  end_x = max(cell1.x, cell2.x)
67
75
  start_y = min(cell1.y, cell2.y)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import shutil
5
+ import time
5
6
  import urllib
6
7
  import urllib.request
7
8
 
@@ -144,12 +145,24 @@ class Downloader:
144
145
  map_name = self.merger._get_output_name(grid_bb)
145
146
  self.downloader.download_tiles(grid_bb, download_in_parallel=False)
146
147
  success = self.merger.merge(grid_bb, map_name)
148
+
147
149
  if success:
148
150
  print(f'[INFO] Downloaded "{map_name}.png"')
149
151
  else:
150
152
  print(f'[WARNING] Download of "{map_name}.png" was unsuccessful.')
151
153
 
152
- shutil.rmtree(DEFAULT_TEMP_FOLDER)
154
+ # Try to remove the temporary folder
155
+ try:
156
+ shutil.rmtree(DEFAULT_TEMP_FOLDER)
157
+ except PermissionError:
158
+ # try again
159
+ time.sleep(5)
160
+ shutil.rmtree(DEFAULT_TEMP_FOLDER)
161
+ except OSError:
162
+ # try again
163
+ time.sleep(5)
164
+ shutil.rmtree(DEFAULT_TEMP_FOLDER)
165
+
153
166
  return success
154
167
 
155
168
  def download_map_by_polygon(
@@ -180,11 +193,12 @@ class Downloader:
180
193
  Additional keyword arguments to pass to the `_download_map` method
181
194
  """
182
195
 
183
- assert isinstance(
184
- polygon, Polygon
185
- ), "[ERROR] \
186
- Please pass polygon as shapely.geometry.Polygon object.\n\
187
- [HINT] Use ``create_polygon_from_latlons()`` to create polygon."
196
+ if type(polygon) is not Polygon:
197
+ raise ValueError(
198
+ "[ERROR] \
199
+ Please pass polygon as shapely.geometry.Polygon object.\n\
200
+ [HINT] Use ``create_polygon_from_latlons()`` to create polygon."
201
+ )
188
202
 
189
203
  min_x, min_y, max_x, max_y = polygon.bounds
190
204
 
@@ -155,6 +155,12 @@ def get_coordinate_from_index(grid_index: GridIndex) -> Coordinate:
155
155
  return Coordinate(lat, lon)
156
156
 
157
157
 
158
+ def _check_z(z):
159
+ if not z >= 0:
160
+ raise ValueError("Zoom level must be positive")
161
+ return True
162
+
163
+
158
164
  def _get_index_from_coordinate(lon: float, lat: float, z: int) -> tuple[(int, int)]:
159
165
  """Generate (x,y) tuple from Coordinate latitudes and longitudes.
160
166
 
@@ -163,7 +169,7 @@ def _get_index_from_coordinate(lon: float, lat: float, z: int) -> tuple[(int, in
163
169
  Tuple
164
170
  (x,y) tuple.
165
171
  """
166
- assert z >= 0, "Zoom level must be positive"
172
+ _check_z(z)
167
173
  n = 2**z
168
174
  x = int((lon + 180) / 360 * n)
169
175
  lat_rad = math.radians(lat)
@@ -179,7 +185,7 @@ def _get_coordinate_from_index(x: int, y: int, z: int) -> tuple[(float, float)]:
179
185
  Tuple
180
186
  (lon, lat) tuple representing the upper left corner of the tile.
181
187
  """
182
- assert z >= 0, "Zoom level must be positive"
188
+ _check_z(z)
183
189
  n = 2**z
184
190
  lon = (x / n) * 360 - 180
185
191
  lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * y / n)))
@@ -4,6 +4,7 @@ import json
4
4
  import os
5
5
  import re
6
6
  import shutil
7
+ import time
7
8
  import urllib
8
9
  import urllib.request
9
10
  from functools import reduce
@@ -622,7 +623,18 @@ class SheetDownloader:
622
623
  else:
623
624
  print(f'[WARNING] Download of "{img_path}" was unsuccessful.')
624
625
 
625
- shutil.rmtree(DEFAULT_TEMP_FOLDER)
626
+ # Try to remove the temporary folder
627
+ try:
628
+ shutil.rmtree(DEFAULT_TEMP_FOLDER)
629
+ except PermissionError:
630
+ # try again
631
+ time.sleep(5)
632
+ shutil.rmtree(DEFAULT_TEMP_FOLDER)
633
+ except OSError:
634
+ # try again
635
+ time.sleep(5)
636
+ shutil.rmtree(DEFAULT_TEMP_FOLDER)
637
+
626
638
  return img_path
627
639
 
628
640
  def _save_metadata(
@@ -136,9 +136,10 @@ class TileMerger:
136
136
  )
137
137
 
138
138
  img_size = start_image.size
139
- assert (
140
- img_size[0] == img_size[1]
141
- ), f"Tiles must be quadratic. This tile, however, is rectangular: {img_size}"
139
+ if not (img_size[0] == img_size[1]):
140
+ raise ValueError(
141
+ f"[ERROR] Tiles must be square: {img_size[0]}x{img_size[1]}."
142
+ )
142
143
  tile_size = img_size[0]
143
144
  return tile_size
144
145
 
@@ -8,7 +8,7 @@ import pandas as pd
8
8
  from tqdm import tqdm
9
9
 
10
10
 
11
- class PostProcessor:
11
+ class ContextPostProcessor:
12
12
  """A class for post-processing predictions on patches using the surrounding context.
13
13
 
14
14
  Parameters