mapreader 1.3.0__tar.gz → 1.3.2__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 (51) hide show
  1. {mapreader-1.3.0 → mapreader-1.3.2}/PKG-INFO +4 -3
  2. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/__init__.py +5 -0
  3. mapreader-1.3.2/mapreader/__main__.py +6 -0
  4. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/_version.py +3 -3
  5. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/annotate/annotator.py +33 -27
  6. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/classify/classifier.py +5 -5
  7. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/classify/load_annotations.py +9 -2
  8. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/download/sheet_downloader.py +74 -27
  9. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/download/tile_merging.py +22 -8
  10. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/load/images.py +4 -3
  11. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/process/post_process.py +40 -23
  12. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader.egg-info/PKG-INFO +4 -3
  13. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader.egg-info/SOURCES.txt +1 -0
  14. mapreader-1.3.2/mapreader.egg-info/entry_points.txt +2 -0
  15. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader.egg-info/requires.txt +3 -2
  16. {mapreader-1.3.0 → mapreader-1.3.2}/setup.py +4 -3
  17. {mapreader-1.3.0 → mapreader-1.3.2}/tests/test_annotator.py +4 -4
  18. mapreader-1.3.2/tests/test_post_processing.py +171 -0
  19. {mapreader-1.3.0 → mapreader-1.3.2}/tests/test_sheet_downloader.py +152 -51
  20. mapreader-1.3.0/mapreader.egg-info/entry_points.txt +0 -2
  21. mapreader-1.3.0/tests/test_post_processing.py +0 -152
  22. {mapreader-1.3.0 → mapreader-1.3.2}/LICENSE +0 -0
  23. {mapreader-1.3.0 → mapreader-1.3.2}/README.md +0 -0
  24. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/annotate/__init__.py +0 -0
  25. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/annotate/utils.py +0 -0
  26. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/classify/__init__.py +0 -0
  27. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/classify/custom_models.py +0 -0
  28. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/classify/datasets.py +0 -0
  29. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/download/__init__.py +0 -0
  30. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/download/data_structures.py +0 -0
  31. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/download/downloader.py +0 -0
  32. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/download/downloader_utils.py +0 -0
  33. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/download/tile_loading.py +0 -0
  34. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/load/__init__.py +0 -0
  35. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/load/geo_utils.py +0 -0
  36. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/load/loader.py +0 -0
  37. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/process/__init__.py +0 -0
  38. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/process/process.py +0 -0
  39. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/spot_text/__init__.py +0 -0
  40. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/spot_text/deepsolo_runner.py +0 -0
  41. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/spot_text/dptext_detr_runner.py +0 -0
  42. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/utils/__init__.py +0 -0
  43. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/utils/compute_and_save_stats.py +0 -0
  44. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader/utils/slice_parallel.py +0 -0
  45. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader.egg-info/dependency_links.txt +0 -0
  46. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader.egg-info/not-zip-safe +0 -0
  47. {mapreader-1.3.0 → mapreader-1.3.2}/mapreader.egg-info/top_level.txt +0 -0
  48. {mapreader-1.3.0 → mapreader-1.3.2}/setup.cfg +0 -0
  49. {mapreader-1.3.0 → mapreader-1.3.2}/tests/test_geo_pipeline.py +0 -0
  50. {mapreader-1.3.0 → mapreader-1.3.2}/tests/test_import.py +0 -0
  51. {mapreader-1.3.0 → mapreader-1.3.2}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mapreader
3
- Version: 1.3.0
3
+ Version: 1.3.2
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
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
29
29
  License-File: LICENSE
30
30
  Requires-Dist: matplotlib<4.0.0,>=3.5.0
31
31
  Requires-Dist: numpy<2.0.0,>=1.21.5
32
- Requires-Dist: pandas<2.0.0,>=1.3.4
32
+ Requires-Dist: pandas>=2.0.0
33
33
  Requires-Dist: pyproj<4.0.0,>=3.2.0
34
34
  Requires-Dist: azure-storage-blob<13.0.0,>=12.9.0
35
35
  Requires-Dist: aiohttp<4.0.0,>=3.8.1
@@ -53,10 +53,11 @@ Requires-Dist: simplekml<2.0.0,>=1.3.6
53
53
  Requires-Dist: versioneer>=0.28
54
54
  Requires-Dist: tqdm<5.0.0
55
55
  Requires-Dist: torchinfo<2.0.0
56
- Requires-Dist: openpyxl<4.0.0
56
+ Requires-Dist: openpyxl<4.0.0,>=3.1.2
57
57
  Requires-Dist: geopandas<1.0.0
58
58
  Requires-Dist: pyogrio>=0.7.2
59
59
  Requires-Dist: cartopy>=0.22.0
60
+ Requires-Dist: joblib>=1.4.0
60
61
  Provides-Extra: dev
61
62
  Requires-Dist: pytest<9.0.0; extra == "dev"
62
63
  Requires-Dist: pytest-cov<6.0.0,>=4.1.0; extra == "dev"
@@ -31,3 +31,8 @@ from . import _version
31
31
  __version__ = _version.get_versions()["version"]
32
32
 
33
33
  from mapreader.load import geo_utils
34
+
35
+ import mapreader
36
+
37
+ def print_version():
38
+ print(mapreader.__version__)
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from . import print_version
4
+
5
+ if __name__ == "__main__":
6
+ print_version()
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2024-04-29T15:52:44+0100",
11
+ "date": "2024-05-15T11:10:51+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "f256ae9b73ae9dd14d8bd72b61a86088f8f1905a",
15
- "version": "1.3.0"
14
+ "full-revisionid": "282b8733c2c83412b979c2c234adda1432691862",
15
+ "version": "1.3.2"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -27,7 +27,7 @@ _CENTER_LAYOUT = widgets.Layout(
27
27
  )
28
28
 
29
29
 
30
- class Annotator(pd.DataFrame):
30
+ class Annotator:
31
31
  """
32
32
  Annotator class for annotating patches with labels.
33
33
 
@@ -227,28 +227,29 @@ class Annotator(pd.DataFrame):
227
227
  delimiter=delimiter,
228
228
  )
229
229
 
230
- # initiate as a DataFrame
231
- super().__init__(patch_df)
232
-
233
230
  ## pixel_bounds = x0, y0, x1, y1
234
- self["min_x"] = self.pixel_bounds.apply(lambda x: x[0])
235
- self["min_y"] = self.pixel_bounds.apply(lambda x: x[1])
236
- self["max_x"] = self.pixel_bounds.apply(lambda x: x[2])
237
- self["max_y"] = self.pixel_bounds.apply(lambda x: x[3])
231
+ patch_df["min_x"] = patch_df["pixel_bounds"].apply(lambda x: x[0])
232
+ patch_df["min_y"] = patch_df["pixel_bounds"].apply(lambda x: x[1])
233
+ patch_df["max_x"] = patch_df["pixel_bounds"].apply(lambda x: x[2])
234
+ patch_df["max_y"] = patch_df["pixel_bounds"].apply(lambda x: x[3])
238
235
 
239
236
  # Sort by sortby column if provided
240
237
  if isinstance(sortby, str):
241
- if sortby in self.columns:
238
+ if sortby in patch_df.columns:
242
239
  self._sortby = sortby
243
240
  self._ascending = ascending
244
241
  else:
245
- raise ValueError(f"[ERROR] {sortby} is not a column in the DataFrame.")
242
+ raise ValueError(
243
+ f"[ERROR] {sortby} is not a column in the patch DataFrame."
244
+ )
246
245
  elif sortby is not None:
247
246
  raise ValueError("[ERROR] ``sortby`` must be a string or None.")
248
247
  else:
249
248
  self._sortby = None
250
249
  self._ascending = True
251
250
 
251
+ self.patch_df = patch_df
252
+
252
253
  self._labels = labels
253
254
  self.label_col = label_col
254
255
  self.patch_paths_col = patch_paths_col
@@ -299,6 +300,9 @@ class Annotator(pd.DataFrame):
299
300
  # Setup queue
300
301
  self._queue = []
301
302
 
303
+ def __len__(self):
304
+ return len(self.patch_df)
305
+
302
306
  @staticmethod
303
307
  def _load_dataframes(
304
308
  patch_paths: str | None = None,
@@ -499,7 +503,7 @@ class Annotator(pd.DataFrame):
499
503
 
500
504
  return True
501
505
 
502
- queue_df = self.copy(deep=True)
506
+ queue_df = self.patch_df.copy(deep=True)
503
507
  queue_df = queue_df[queue_df[self.label_col].isna()] # only unlabelled
504
508
  queue_df["eligible"] = queue_df.apply(check_eligibility, axis=1)
505
509
 
@@ -562,20 +566,20 @@ class Annotator(pd.DataFrame):
562
566
 
563
567
  ix = self._queue[self.current_index]
564
568
 
565
- min_x = self.at[ix, "min_x"]
566
- min_y = self.at[ix, "min_y"]
569
+ min_x = self.patch_df.at[ix, "min_x"]
570
+ min_y = self.patch_df.at[ix, "min_y"]
567
571
 
568
572
  # cannot assume all patches are same size
569
573
  try:
570
- height, width, _ = self.at[ix, "shape"]
574
+ height, width, _ = self.patch_df.at[ix, "shape"]
571
575
  except KeyError:
572
- im_path = self.at[ix, self.patch_paths_col]
576
+ im_path = self.patch_df.at[ix, self.patch_paths_col]
573
577
  im = Image.open(im_path)
574
578
  height = im.height
575
579
  width = im.width
576
580
 
577
- current_parent = self.at[ix, "parent_id"]
578
- parent_frame = self.query(f"parent_id=='{current_parent}'")
581
+ current_parent = self.patch_df.at[ix, "parent_id"]
582
+ parent_frame = self.patch_df.query(f"parent_id=='{current_parent}'")
579
583
 
580
584
  deltas = list(range(-self.surrounding, self.surrounding + 1))
581
585
  y_and_x = list(
@@ -770,7 +774,7 @@ class Annotator(pd.DataFrame):
770
774
 
771
775
  ix = self._queue[self.current_index]
772
776
 
773
- img_path = self.at[ix, self.patch_paths_col]
777
+ img_path = self.patch_df.at[ix, self.patch_paths_col]
774
778
 
775
779
  self.render()
776
780
  return self.previous_index, self.current_index, img_path
@@ -794,7 +798,7 @@ class Annotator(pd.DataFrame):
794
798
 
795
799
  ix = self._queue[self.current_index]
796
800
 
797
- img_path = self.at[ix, self.patch_paths_col]
801
+ img_path = self.patch_df.at[ix, self.patch_paths_col]
798
802
 
799
803
  self.render()
800
804
  return self.previous_index, self.current_index, img_path
@@ -826,7 +830,7 @@ class Annotator(pd.DataFrame):
826
830
  # disable skip button when at last example
827
831
  button.disabled = self.current_index >= len(self) - 1
828
832
  elif button.description != "submit":
829
- if self.at[ix, self.label_col] == button.description:
833
+ if self.patch_df.at[ix, self.label_col] == button.description:
830
834
  button.icon = "check"
831
835
  else:
832
836
  button.icon = ""
@@ -842,8 +846,8 @@ class Annotator(pd.DataFrame):
842
846
  else:
843
847
  display(image.convert("RGB"))
844
848
  add_ins = []
845
- if "url" in self.loc[ix].keys():
846
- url = self.at[ix, "url"]
849
+ if "url" in self.patch_df.loc[ix].keys():
850
+ url = self.patch_df.at[ix, "url"]
847
851
  text = f'<p><a href="{url}" target="_blank">Click to see entire map.</a></p>'
848
852
  add_ins += [widgets.HTML(text)]
849
853
 
@@ -881,7 +885,7 @@ class Annotator(pd.DataFrame):
881
885
  PIL.Image
882
886
  A PIL.Image object of the image at the given index.
883
887
  """
884
- image_path = self.at[ix, self.patch_paths_col]
888
+ image_path = self.patch_df.at[ix, self.patch_paths_col]
885
889
  image = Image.open(image_path)
886
890
 
887
891
  if self.resize_to is not None:
@@ -907,7 +911,7 @@ class Annotator(pd.DataFrame):
907
911
  """
908
912
  # ix = self.iloc[self.current_index].name
909
913
  ix = self._queue[self.current_index]
910
- self.at[ix, self.label_col] = annotation
914
+ self.patch_df.at[ix, self.label_col] = annotation
911
915
  if self.auto_save:
912
916
  self._auto_save()
913
917
  self._next_example()
@@ -950,7 +954,9 @@ class Annotator(pd.DataFrame):
950
954
  A dataframe containing the labelled images and their associated
951
955
  label index.
952
956
  """
953
- filtered_df = self[self[self.label_col].notna()].copy(deep=True)
957
+ filtered_df = self.patch_df[self.patch_df[self.label_col].notna()].copy(
958
+ deep=True
959
+ )
954
960
 
955
961
  # force image_id to be index (incase of integer index)
956
962
  # TODO: Force all indices to be integers so this is not needed
@@ -973,8 +979,8 @@ class Annotator(pd.DataFrame):
973
979
 
974
980
  @property
975
981
  def filtered(self) -> pd.DataFrame:
976
- _filter = ~self[self.label_col].isna()
977
- return self[_filter]
982
+ _filter = ~self.patch_df[self.label_col].isna()
983
+ return self.patch_df[_filter]
978
984
 
979
985
  def render_complete(self):
980
986
  """
@@ -1367,7 +1367,7 @@ Use ``add_criterion`` to define one."
1367
1367
  def _initialize_model(
1368
1368
  self,
1369
1369
  model_name: str,
1370
- pretrained: bool | None = True,
1370
+ weights: str | None = "DEFAULT",
1371
1371
  last_layer_num_classes: str | int | None = "default",
1372
1372
  ) -> tuple[Any, int, bool]:
1373
1373
  """
@@ -1378,8 +1378,9 @@ Use ``add_criterion`` to define one."
1378
1378
  ----------
1379
1379
  model_name : str
1380
1380
  Name of a PyTorch model. See https://pytorch.org/vision/0.8/models.html for options.
1381
- pretrained : bool, optional
1382
- Use pretrained version, by default ``True``
1381
+ weights : str, optional
1382
+ Weights to load into the model. If ``"DEFAULT"``, loads the default weights for the chosen model.
1383
+ By default, ``"DEFAULT"``.
1383
1384
  last_layer_num_classes : str or int, optional
1384
1385
  Number of elements in the last layer. If ``"default"``, sets it to
1385
1386
  the number of classes. By default, ``"default"``.
@@ -1408,8 +1409,7 @@ Use ``add_criterion`` to define one."
1408
1409
 
1409
1410
  # Initialize these variables which will be set in this if statement.
1410
1411
  # Each of these variables is model specific.
1411
- model_dw = models.__getattribute__(model_name)
1412
- model_dw = model_dw(pretrained)
1412
+ model_dw = models.get_model(model_name, weights=weights)
1413
1413
  input_size = (224, 224)
1414
1414
  is_inception = False
1415
1415
 
@@ -395,7 +395,9 @@ Please check your image paths and update them if necessary.'
395
395
  )
396
396
  iter_ids.append(annots2review.iloc[image_idx].name)
397
397
  # Add to reviewed
398
- self.reviewed = self.reviewed.append(annots2review.iloc[image_idx])
398
+ self.reviewed = pd.concat(
399
+ [self.reviewed, annots2review.iloc[image_idx : image_idx + 1]]
400
+ )
399
401
  try:
400
402
  self.reviewed.drop_duplicates(subset=[deduplicate_col])
401
403
  except Exception:
@@ -416,9 +418,14 @@ Please check your image paths and update them if necessary.'
416
418
  ]:
417
419
  list_input_ids = user_input_ids.split(",")
418
420
  print(
419
- f"[INFO] Options for labels (or create a new label):{list(self.annotations[self.label_col].unique())}"
421
+ f"[INFO] Options for labels:{list(self.annotations[self.label_col].unique())}"
420
422
  )
421
423
  input_label = input("Enter new label: ")
424
+ if input_label not in list(self.annotations[self.label_col].unique()):
425
+ print(
426
+ f'[ERROR] Label "{input_label}" not found in the annotations. Please enter a valid label.'
427
+ )
428
+ continue
422
429
 
423
430
  for input_id in list_input_ids:
424
431
  input_id = int(input_id)
@@ -533,7 +533,7 @@ class SheetDownloader:
533
533
  """
534
534
  self.merger = TileMerger(output_folder=f"{path_save}/")
535
535
 
536
- def _check_map_sheet_exists(self, feature: dict) -> bool:
536
+ def _check_map_sheet_exists(self, feature: dict, metadata_fname) -> bool:
537
537
  """
538
538
  Checks if a map sheet is already saved.
539
539
 
@@ -544,51 +544,87 @@ class SheetDownloader:
544
544
  Returns
545
545
  -------
546
546
  bool
547
- True if file exists, False if not.
547
+ img_path if file exists, False if not.
548
548
  """
549
- map_name = str("map_" + feature["properties"]["IMAGE"])
550
549
  path_save = self.merger.output_folder
551
- if os.path.exists(f"{path_save}{map_name}.png"):
550
+
551
+ try:
552
+ # get image id with same coords in metadata
553
+ existing_metadata_df = pd.read_csv(
554
+ f"{path_save}{metadata_fname}", sep=",", index_col=0
555
+ )
556
+ except FileNotFoundError:
557
+ return False
558
+
559
+ polygon = get_polygon_from_grid_bb(feature["grid_bb"])
560
+ if str(polygon.bounds) in existing_metadata_df["coordinates"].values:
561
+ image_id = existing_metadata_df[
562
+ existing_metadata_df["coordinates"] == str(polygon.bounds)
563
+ ].iloc[0]["name"]
564
+ else:
565
+ return False # coordinates not in metadata means image doesn't exist
566
+
567
+ if os.path.exists(f"{path_save}{image_id}"):
552
568
  try:
553
- mpimg.imread(f"{path_save}{map_name}.png")
554
- print(
555
- f'[INFO] "{path_save}{map_name}.png" already exists. Skipping download.'
556
- )
557
- return True
569
+ # check image is valid
570
+ mpimg.imread(f"{path_save}{image_id}")
571
+ return image_id
558
572
  except OSError:
559
573
  return False
560
574
  return False
561
575
 
562
- def _download_map(self, feature: dict, download_in_parallel: bool = True) -> bool:
576
+ def _download_map(
577
+ self,
578
+ feature: dict,
579
+ existing_id: str | bool,
580
+ download_in_parallel: bool = True,
581
+ overwrite: bool = False,
582
+ ) -> str | bool:
563
583
  """
564
584
  Downloads a single map sheet and saves as png file.
565
585
 
566
586
  Parameters
567
587
  ----------
568
588
  feature : dict
589
+ The feature for which to download the map sheet.
590
+ existing_id : str | bool
591
+ The existing image id if the map sheet already exists.
592
+ download_in_parallel : bool, optional
593
+ Whether to download tiles in parallel, by default ``True``.
594
+ overwrite : bool, optional
595
+ Whether to overwrite existing maps, by default ``False``.
569
596
 
570
597
  Returns
571
598
  -------
572
- bool
573
- True if map was downloaded successfully, False if not.
599
+ str or bool
600
+ image path if map was downloaded successfully, False if not.
574
601
  """
575
- map_name = str("map_" + feature["properties"]["IMAGE"])
576
602
  self.downloader.download_tiles(
577
603
  feature["grid_bb"], download_in_parallel=download_in_parallel
578
604
  )
579
- success = self.merger.merge(feature["grid_bb"], map_name)
580
- if success:
581
- print(f'[INFO] Downloaded "{map_name}.png"')
605
+
606
+ if existing_id is False:
607
+ map_name = f"map_{feature['properties']['IMAGE']}"
608
+ else:
609
+ map_name = existing_id[:-4] # remove file extension (assuming .png)
610
+
611
+ img_path = self.merger.merge(
612
+ feature["grid_bb"], file_name=map_name, overwrite=overwrite
613
+ )
614
+
615
+ if img_path is not False:
616
+ print(f'[INFO] Downloaded "{img_path}"')
582
617
  else:
583
- print(f'[WARNING] Download of "{map_name}.png" was unsuccessful.')
618
+ print(f'[WARNING] Download of "{img_path}" was unsuccessful.')
584
619
 
585
620
  shutil.rmtree(DEFAULT_TEMP_FOLDER)
586
- return success
621
+ return img_path
587
622
 
588
623
  def _save_metadata(
589
624
  self,
590
625
  feature: dict,
591
626
  out_filepath: str,
627
+ img_path: str,
592
628
  metadata_to_save: dict | None = None,
593
629
  **kwargs: dict | None,
594
630
  ) -> None:
@@ -602,6 +638,8 @@ class SheetDownloader:
602
638
  The feature for which to extract the metadata from
603
639
  out_filepath : str
604
640
  The path to save metadata csv.
641
+ img_path : str
642
+ The path to the downloaded map sheet.
605
643
  metadata_to_save : dict, optional
606
644
  A dictionary containing column names (str) and metadata keys (str or list) to save to metadata csv.
607
645
  Multilayer keys should be passed as a list, i.e. ["key1","key2"] will search for ``self.features[i]["key1"]["key2"]``.
@@ -631,7 +669,7 @@ class SheetDownloader:
631
669
  metadata_dict = {col: None for col in metadata_cols}
632
670
 
633
671
  # get default metadata
634
- metadata_dict["name"] = str("map_" + feature["properties"]["IMAGE"] + ".png")
672
+ metadata_dict["name"] = os.path.basename(img_path)
635
673
  metadata_dict["url"] = str(feature["properties"]["IMAGEURL"])
636
674
  if not self.published_dates:
637
675
  date_col = kwargs.get("date_col", None)
@@ -705,16 +743,25 @@ class SheetDownloader:
705
743
  """
706
744
 
707
745
  for feature in tqdm(features):
708
- if not overwrite:
709
- if self._check_map_sheet_exists(feature):
710
- continue
711
- success = self._download_map(
712
- feature, download_in_parallel=download_in_parallel
746
+ existing_id = self._check_map_sheet_exists(feature, metadata_fname)
747
+ if (
748
+ not overwrite and existing_id is not False
749
+ ): # if map already exists and overwrite is False then skip
750
+ print(f'[INFO] "{existing_id}" already exists. Skipping download.')
751
+ continue
752
+ img_path = self._download_map(
753
+ feature,
754
+ existing_id,
755
+ download_in_parallel=download_in_parallel,
756
+ overwrite=overwrite,
713
757
  )
714
- if success:
758
+ if img_path is not False:
715
759
  metadata_path = f"{path_save}/{metadata_fname}"
716
760
  self._save_metadata(
717
- feature=feature, out_filepath=metadata_path, **kwargs
761
+ feature=feature,
762
+ out_filepath=metadata_path,
763
+ img_path=img_path,
764
+ **kwargs,
718
765
  )
719
766
 
720
767
  def download_all_map_sheets(
@@ -1242,7 +1289,6 @@ Try passing coordinates (min_x, max_x, min_y, max_y) instead or leave blank to a
1242
1289
  pass
1243
1290
 
1244
1291
  for feature in features:
1245
- text_id = feature["wfs_id_no"]
1246
1292
  coords = np.array(feature["geometry"]["coordinates"][0][0])
1247
1293
 
1248
1294
  # Plot coordinates
@@ -1255,6 +1301,7 @@ Try passing coordinates (min_x, max_x, min_y, max_y) instead or leave blank to a
1255
1301
  )
1256
1302
 
1257
1303
  if add_id:
1304
+ text_id = feature["wfs_id_no"]
1258
1305
  plt.text(
1259
1306
  np.mean(coords[:, 0]) - 0.15,
1260
1307
  np.mean(coords[:, 1]) - 0.05,
@@ -142,7 +142,12 @@ class TileMerger:
142
142
  tile_size = img_size[0]
143
143
  return tile_size
144
144
 
145
- def merge(self, grid_bb: GridBoundingBox, file_name: str | None = None) -> bool:
145
+ def merge(
146
+ self,
147
+ grid_bb: GridBoundingBox,
148
+ file_name: str | None = None,
149
+ overwrite: bool = False,
150
+ ) -> str | bool:
146
151
  """Merges cells contained within GridBoundingBox.
147
152
 
148
153
  Parameters
@@ -151,11 +156,13 @@ class TileMerger:
151
156
  GridBoundingBox containing tiles to merge
152
157
  file_name : Union[str, None], optional
153
158
  Name to use when saving map
154
-
159
+ If None, default name will be used, by default None
160
+ overwrite : bool, optional
161
+ Whether or not to overwrite existing files, by default False
155
162
  Returns
156
163
  -------
157
- bool
158
- True if file has successfully downloaded, False if not.
164
+ str or bool
165
+ out path if file has successfully downloaded, False if not.
159
166
  """
160
167
  os.makedirs(self.output_folder, exist_ok=True)
161
168
 
@@ -191,11 +198,18 @@ class TileMerger:
191
198
  file_name = self._get_output_name(grid_bb)
192
199
 
193
200
  out_path = f"{self.output_folder}{file_name}.{self.img_output_format[0]}"
201
+ if not overwrite:
202
+ i = 1
203
+ while os.path.exists(out_path):
204
+ out_path = (
205
+ f"{self.output_folder}{file_name}_{i}.{self.img_output_format[0]}"
206
+ )
207
+ i += 1
194
208
  merged_image.save(out_path, self.img_output_format[1])
195
- success = True if os.path.exists(out_path) else False
196
- if success:
197
- logger.info(f"Merge successful! The image has been stored at '{out_path}'")
198
- else:
209
+ success = out_path if os.path.exists(out_path) else False
210
+ if success is False:
199
211
  logger.warning(f"Merge unsuccessful! '{out_path}' not saved.")
212
+ else:
213
+ logger.info(f"Merge successful! The image has been stored at '{out_path}'")
200
214
 
201
215
  return success
@@ -1392,7 +1392,6 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1392
1392
  min_x, min_y, max_x, max_y = self.patches[patch]["pixel_bounds"]
1393
1393
  if width != max_x - min_x:
1394
1394
  width = max_x - min_x
1395
- img = img.crop((0, 0, width, height))
1396
1395
  if height != max_y - min_y:
1397
1396
  height = max_y - min_y
1398
1397
  img = img.crop((0, 0, width, height))
@@ -1400,22 +1399,24 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1400
1399
  bands = img.getbands()
1401
1400
 
1402
1401
  if calc_mean:
1403
- if all(f"mean_pixel_{band}" in patch_keys for band in bands):
1402
+ if "mean_pixel" in patch_keys:
1404
1403
  calc_mean = False
1405
1404
  if calc_std:
1406
- if all(f"std_pixel_{band}" in patch_keys for band in bands):
1405
+ if "std_pixel" in patch_keys:
1407
1406
  calc_std = False
1408
1407
 
1409
1408
  img_stat = ImageStat.Stat(img)
1410
1409
 
1411
1410
  if calc_mean:
1412
1411
  img_mean = img_stat.mean
1412
+ self.patches[patch]["mean_pixel"] = np.mean(img_mean) / 255
1413
1413
  for i, band in enumerate(bands):
1414
1414
  # Calculate mean pixel values
1415
1415
  self.patches[patch][f"mean_pixel_{band}"] = img_mean[i] / 255
1416
1416
 
1417
1417
  if calc_std:
1418
1418
  img_std = img_stat.stddev
1419
+ self.patches[patch]["std_pixel"] = np.mean(img_std) / 255
1419
1420
  for i, band in enumerate(bands):
1420
1421
  # Calculate std pixel values
1421
1422
  self.patches[patch][f"std_pixel_{band}"] = img_std[i] / 255