mapreader 1.1.0__tar.gz → 1.1.1__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 (44) hide show
  1. {mapreader-1.1.0 → mapreader-1.1.1}/PKG-INFO +4 -1
  2. {mapreader-1.1.0 → mapreader-1.1.1}/README.md +3 -0
  3. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/_version.py +3 -3
  4. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/annotate/annotator.py +45 -49
  5. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/classify/load_annotations.py +2 -0
  6. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/load/images.py +201 -20
  7. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader.egg-info/PKG-INFO +4 -1
  8. {mapreader-1.1.0 → mapreader-1.1.1}/tests/test_annotator.py +10 -10
  9. {mapreader-1.1.0 → mapreader-1.1.1}/tests/test_sheet_downloader.py +154 -61
  10. {mapreader-1.1.0 → mapreader-1.1.1}/LICENSE +0 -0
  11. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/__init__.py +0 -0
  12. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/annotate/__init__.py +0 -0
  13. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/annotate/utils.py +0 -0
  14. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/classify/__init__.py +0 -0
  15. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/classify/classifier.py +0 -0
  16. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/classify/classifier_context.py +0 -0
  17. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/classify/custom_models.py +0 -0
  18. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/classify/datasets.py +0 -0
  19. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/download/__init__.py +0 -0
  20. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/download/data_structures.py +0 -0
  21. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/download/downloader.py +0 -0
  22. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/download/downloader_utils.py +0 -0
  23. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/download/sheet_downloader.py +0 -0
  24. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/download/tile_loading.py +0 -0
  25. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/download/tile_merging.py +0 -0
  26. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/load/__init__.py +0 -0
  27. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/load/geo_utils.py +0 -0
  28. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/load/loader.py +0 -0
  29. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/process/__init__.py +0 -0
  30. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/process/process.py +0 -0
  31. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/utils/__init__.py +0 -0
  32. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/utils/compute_and_save_stats.py +0 -0
  33. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader/utils/slice_parallel.py +0 -0
  34. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader.egg-info/SOURCES.txt +0 -0
  35. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader.egg-info/dependency_links.txt +0 -0
  36. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader.egg-info/entry_points.txt +0 -0
  37. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader.egg-info/not-zip-safe +0 -0
  38. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader.egg-info/requires.txt +0 -0
  39. {mapreader-1.1.0 → mapreader-1.1.1}/mapreader.egg-info/top_level.txt +0 -0
  40. {mapreader-1.1.0 → mapreader-1.1.1}/setup.cfg +0 -0
  41. {mapreader-1.1.0 → mapreader-1.1.1}/setup.py +0 -0
  42. {mapreader-1.1.0 → mapreader-1.1.1}/tests/test_geo_pipeline.py +0 -0
  43. {mapreader-1.1.0 → mapreader-1.1.1}/tests/test_import.py +0 -0
  44. {mapreader-1.1.0 → mapreader-1.1.1}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mapreader
3
- Version: 1.1.0
3
+ Version: 1.1.1
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
@@ -83,6 +83,9 @@ Requires-Dist: flake8<7.0.0,>=6.0.0; extra == "dev"
83
83
  </a>
84
84
  <a href="https://zenodo.org/badge/latestdoi/430661738"><img src="https://zenodo.org/badge/430661738.svg" alt="DOI"></a>
85
85
  <br/>
86
+ <a href="https://codecov.io/github/Living-with-machines/MapReader" >
87
+ <img src="https://codecov.io/github/Living-with-machines/MapReader/graph/badge.svg?token=38GQ3O1GB5"/>
88
+ </a>
86
89
  </p>
87
90
 
88
91
 
@@ -18,6 +18,9 @@
18
18
  </a>
19
19
  <a href="https://zenodo.org/badge/latestdoi/430661738"><img src="https://zenodo.org/badge/430661738.svg" alt="DOI"></a>
20
20
  <br/>
21
+ <a href="https://codecov.io/github/Living-with-machines/MapReader" >
22
+ <img src="https://codecov.io/github/Living-with-machines/MapReader/graph/badge.svg?token=38GQ3O1GB5"/>
23
+ </a>
21
24
  </p>
22
25
 
23
26
 
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2023-12-14T11:30:18+0000",
11
+ "date": "2024-01-08T14:36:56+0000",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "8eda75e8ddc1b4d5bb22cc2fcf5ebc0954947d71",
15
- "version": "1.1.0"
14
+ "full-revisionid": "8997b4d7fe7c82d5579ffd13daaa90e7253fc0c2",
15
+ "version": "1.1.1"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -22,8 +22,6 @@ from ..load.loader import load_patches
22
22
 
23
23
  warnings.filterwarnings("ignore", category=UserWarning)
24
24
 
25
- MAX_SIZE = 1000
26
-
27
25
  _CENTER_LAYOUT = widgets.Layout(
28
26
  display="flex", flex_flow="column", align_items="center"
29
27
  )
@@ -64,8 +62,23 @@ class Annotator(pd.DataFrame):
64
62
  sortby : str or None, optional
65
63
  Name of the column to use to sort the patch DataFrame, by default None.
66
64
  Default sort order is ``ascending=True``. Pass ``ascending=False`` keyword argument to sort in descending order.
67
- **kwargs
68
- Additional keyword arguments
65
+ ascending : bool, optional
66
+ Whether to sort the DataFrame in ascending order when using the ``sortby`` argument, by default True.
67
+ username : str or None, optional
68
+ Username to use when saving annotations file, by default None.
69
+ If not provided, a random string is generated.
70
+ task_name : str or None, optional
71
+ Name of the annotation task, by default None.
72
+ min_values : dict, optional
73
+ A dictionary consisting of column names (keys) and minimum values as floating point values (values), by default None.
74
+ max_values : dict, optional
75
+ A dictionary consisting of column names (keys) and maximum values as floating point values (values), by default None.
76
+ surrounding : int, optional
77
+ The number of surrounding images to show for context, by default 1.
78
+ max_size : int, optional
79
+ The size in pixels for the longest side to which constrain each patch image, by default 1000.
80
+ resize_to : int or None, optional
81
+ The size in pixels for the longest side to which resize each patch image, by default None.
69
82
 
70
83
  Raises
71
84
  ------
@@ -79,21 +92,6 @@ class Annotator(pd.DataFrame):
79
92
  If labels provided are not in the form of a list
80
93
  SyntaxError
81
94
  If labels provided are not in the form of a list
82
-
83
- Notes
84
- -----
85
-
86
- Additional kwargs:
87
-
88
- - ``username``: Username to use when saving annotations file. Default: Randomly generated string.
89
- - ``task_name``: Name of the annotation task. Default: "task".
90
- - ``min_values``: A dictionary consisting of column names (keys) and minimum values as floating point values (values). Default: {}.
91
- - ``max_values``: A dictionary consisting of column names (keys) and maximum values as floating point values (values). Default: {}.
92
- - ``buttons_per_row``: Number of buttons to display per row. Default: None.
93
- - ``ascending``: Whether to sort the DataFrame in ascending order. Default: True.
94
- - ``surrounding``: The number of surrounding images to show for context. Default: 1.
95
- - ``max_size``: The size in pixels for the longest side to which constrain each patch image. Default: 1000.
96
- - ``resize_to``: The size in pixels for the longest side to which resize each patch image. Default: None.
97
95
  """
98
96
 
99
97
  def __init__(
@@ -111,7 +109,14 @@ class Annotator(pd.DataFrame):
111
109
  auto_save: bool = True,
112
110
  delimiter: str = ",",
113
111
  sortby: str | None = None,
114
- **kwargs,
112
+ ascending: bool = True,
113
+ username: str | None = None,
114
+ task_name: str | None = None,
115
+ min_values: dict | None = None,
116
+ max_values: dict | None = None,
117
+ surrounding: int = 1,
118
+ max_size: int = 1000,
119
+ resize_to: int | None = None,
115
120
  ):
116
121
  if labels is None:
117
122
  labels = []
@@ -174,10 +179,6 @@ class Annotator(pd.DataFrame):
174
179
  # Check for url column and add to patch dataframe
175
180
  if "url" in parent_df.columns:
176
181
  patch_df = patch_df.join(parent_df["url"], on="parent_id")
177
- else:
178
- raise ValueError(
179
- "[ERROR] Metadata (parent data) should contain a 'url' column."
180
- )
181
182
 
182
183
  # Add label column if not present
183
184
  if label_col not in patch_df.columns:
@@ -195,13 +196,12 @@ class Annotator(pd.DataFrame):
195
196
  )
196
197
 
197
198
  # Set up annotations file
198
- username = kwargs.get(
199
- "username",
200
- "".join(
199
+ if not username:
200
+ username = "".join(
201
201
  [random.choice(string.ascii_letters + string.digits) for n in range(30)]
202
- ),
203
- )
204
- task_name = kwargs.get("task_name", "task")
202
+ )
203
+ if not task_name:
204
+ task_name = "task"
205
205
  id = hashlib.md5(image_list.encode("utf-8")).hexdigest()
206
206
 
207
207
  annotations_file = task_name.replace(" ", "_") + f"_#{username}#-{id}.csv"
@@ -269,9 +269,7 @@ class Annotator(pd.DataFrame):
269
269
  # Sort by sortby column if provided
270
270
  if isinstance(sortby, str):
271
271
  if sortby in self.columns:
272
- self.sort_values(
273
- sortby, ascending=kwargs.get("ascending", True), inplace=True
274
- )
272
+ self.sort_values(sortby, ascending=ascending, inplace=True)
275
273
  else:
276
274
  raise ValueError(f"[ERROR] {sortby} is not a column in the DataFrame.")
277
275
  elif sortby is not None:
@@ -287,9 +285,8 @@ class Annotator(pd.DataFrame):
287
285
  self.task_name = task_name
288
286
 
289
287
  # set up for the annotator
290
- self.buttons_per_row = kwargs.get("buttons_per_row", None)
291
- self._min_values = kwargs.get("min_values", {})
292
- self._max_values = kwargs.get("max_values", {}) # pixel_bounds = x0, y0, x1, y1
288
+ self._min_values = min_values or {}
289
+ self._max_values = max_values or {}
293
290
 
294
291
  self.patch_width, self.patch_height = self.get_patch_size()
295
292
 
@@ -297,25 +294,24 @@ class Annotator(pd.DataFrame):
297
294
  Path(annotations_dir).mkdir(parents=True, exist_ok=True)
298
295
 
299
296
  # Set up standards for context display
300
- self.surrounding = kwargs.get("surrounding", 1)
301
- self.max_size = kwargs.get("max_size", MAX_SIZE)
302
- self.resize_to = kwargs.get("resize_to", None)
297
+ self.surrounding = surrounding
298
+ self.max_size = max_size
299
+ self.resize_to = resize_to
303
300
 
304
301
  # set up buttons
305
302
  self._buttons = []
306
303
 
307
304
  # Set max buttons
308
- if not self.buttons_per_row:
309
- if (len(self._labels) % 2) == 0:
310
- if len(self._labels) > 4:
311
- self.buttons_per_row = 4
312
- else:
313
- self.buttons_per_row = 2
305
+ if (len(self._labels) % 2) == 0:
306
+ if len(self._labels) > 4:
307
+ self.buttons_per_row = 4
314
308
  else:
315
- if len(self._labels) == 3:
316
- self.buttons_per_row = 3
317
- else:
318
- self.buttons_per_row = 5
309
+ self.buttons_per_row = 2
310
+ else:
311
+ if len(self._labels) == 3:
312
+ self.buttons_per_row = 3
313
+ else:
314
+ self.buttons_per_row = 5
319
315
 
320
316
  # Set indices
321
317
  self.current_index = -1
@@ -180,6 +180,8 @@ class AnnotationsLoader:
180
180
  if os.path.isfile(annotations):
181
181
  print(f'[INFO] Reading "{annotations}"')
182
182
  annotations = pd.read_csv(annotations, sep=delimiter, index_col=0)
183
+ if annotations.index.name in ["name", "image_id"]:
184
+ annotations.reset_index(inplace=True, drop=False)
183
185
  else:
184
186
  raise ValueError(f'[ERROR] "{annotations}" cannot be found.')
185
187
 
@@ -7,11 +7,12 @@ except ImportError:
7
7
 
8
8
  import os
9
9
  import random
10
+ import re
10
11
  import warnings
12
+ from ast import literal_eval
11
13
  from glob import glob
12
14
  from typing import Literal
13
15
 
14
- import matplotlib.image as mpimg
15
16
  import matplotlib.patches as patches
16
17
  import matplotlib.pyplot as plt
17
18
  import numpy as np
@@ -21,9 +22,13 @@ import rasterio
21
22
  from PIL import Image, ImageStat
22
23
  from pyproj import Transformer
23
24
  from rasterio.plot import reshape_as_raster
24
- from shapely.geometry import box
25
+ from shapely import wkt
26
+ from shapely.geometry import Polygon, box
25
27
  from tqdm.auto import tqdm
26
28
 
29
+ from mapreader.download.data_structures import GridBoundingBox, GridIndex
30
+ from mapreader.download.downloader_utils import get_polygon_from_grid_bb
31
+
27
32
  os.environ[
28
33
  "USE_PYGEOS"
29
34
  ] = "0" # see here https://github.com/geopandas/geopandas/issues/2691
@@ -419,7 +424,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
419
424
  data_series = metadata_df[metadata_df[image_id_col] == key].squeeze()
420
425
  for column, item in data_series.items():
421
426
  try:
422
- self.images[tree_level][key][column] = eval(item)
427
+ self.images[tree_level][key][column] = literal_eval(item)
423
428
  except:
424
429
  self.images[tree_level][key][column] = item
425
430
 
@@ -466,9 +471,12 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
466
471
  plt.subplot(num_samples // 3 + 1, 3, i + 1)
467
472
  img = Image.open(self.images[tree_level][image_id]["image_path"])
468
473
  plt.title(image_id, size=8)
469
- plt.imshow(
470
- img,
471
- )
474
+
475
+ # check if grayscale
476
+ if len(img.getbands()) == 1:
477
+ plt.imshow(img, cmap="gray", vmin=0, vmax=255)
478
+ else:
479
+ plt.imshow(img)
472
480
  plt.xticks([])
473
481
  plt.yticks([])
474
482
 
@@ -509,6 +517,20 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
509
517
  for image_id in image_ids:
510
518
  self._add_shape_id(image_id=image_id)
511
519
 
520
+ def add_coords_from_grid_bb(self, verbose: bool = False) -> None:
521
+ print("[INFO] Adding coordinates, tree level: parent")
522
+
523
+ parent_list = self.list_parents()
524
+
525
+ for parent_id in parent_list:
526
+ if "grid_bb" not in self.parents[parent_id].keys():
527
+ print(
528
+ f"[WARNING] No grid bounding box found for {parent_id}. Suggestion: run add_metadata or add_geo_info." # noqa
529
+ )
530
+ continue
531
+
532
+ self._add_coords_from_grid_bb_id(image_id=parent_id, verbose=verbose)
533
+
512
534
  def add_coord_increments(self, verbose: bool | None = False) -> None:
513
535
  """
514
536
  Adds coordinate increments to each image at the parent level.
@@ -652,15 +674,42 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
652
674
  tree_level = self._get_tree_level(image_id)
653
675
 
654
676
  try:
655
- myimg = mpimg.imread(self.images[tree_level][image_id]["image_path"])
677
+ img = Image.open(self.images[tree_level][image_id]["image_path"])
656
678
  # shape = (hwc)
657
- myimg_shape = myimg.shape
658
- self.images[tree_level][image_id]["shape"] = myimg_shape
679
+ height = img.height
680
+ width = img.width
681
+ channels = len(img.getbands())
682
+
683
+ self.images[tree_level][image_id]["shape"] = (height, width, channels)
659
684
  except OSError:
660
685
  raise ValueError(
661
686
  f'[ERROR] Problem with "{image_id}". Please either redownload or remove from list of images to load.'
662
687
  )
663
688
 
689
+ def _add_coords_from_grid_bb_id(
690
+ self, image_id: int | str, verbose: bool = False
691
+ ) -> None:
692
+ grid_bb = self.parents[image_id]["grid_bb"]
693
+
694
+ if isinstance(grid_bb, str):
695
+ cell1, cell2 = re.findall(r"\(.*?\)", grid_bb)
696
+
697
+ z1, x1, y1 = literal_eval(cell1)
698
+ z2, x2, y2 = literal_eval(cell2)
699
+
700
+ cell1 = GridIndex(x1, y1, z1)
701
+ cell2 = GridIndex(x2, y2, z2)
702
+
703
+ grid_bb = GridBoundingBox(cell1, cell2)
704
+
705
+ if isinstance(grid_bb, GridBoundingBox):
706
+ polygon = get_polygon_from_grid_bb(grid_bb)
707
+ coordinates = polygon.bounds
708
+ self.parents[image_id]["coordinates"] = coordinates
709
+
710
+ else:
711
+ raise ValueError(f"[ERROR] Unexpected grid_bb format for {image_id}.")
712
+
664
713
  def _add_coord_increments_id(
665
714
  self, image_id: int | str, verbose: bool | None = False
666
715
  ) -> None:
@@ -759,19 +808,19 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
759
808
  self._add_coord_increments_id(parent_id)
760
809
 
761
810
  # get min_x and min_y and pixel-wise dlon and dlat for parent image
762
- parent_min_x = self.parents[parent_id]["coordinates"][0]
763
- parent_min_y = self.parents[parent_id]["coordinates"][1]
811
+ parent_min_x, parent_min_y, parent_max_x, parent_max_y = self.parents[
812
+ parent_id
813
+ ]["coordinates"]
764
814
  dlon = self.parents[parent_id]["dlon"]
765
815
  dlat = self.parents[parent_id]["dlat"]
766
816
 
767
- # get patch bounds
768
817
  pixel_bounds = self.patches[image_id]["pixel_bounds"]
769
818
 
770
819
  # get patch coords
771
820
  min_x = (pixel_bounds[0] * dlon) + parent_min_x
772
- min_y = (pixel_bounds[1] * dlat) + parent_min_y
821
+ min_y = parent_max_y - (pixel_bounds[3] * dlat)
773
822
  max_x = (pixel_bounds[2] * dlon) + parent_min_x
774
- max_y = (pixel_bounds[3] * dlat) + parent_min_y
823
+ max_y = parent_max_y - (pixel_bounds[1] * dlat)
775
824
 
776
825
  self.patches[image_id]["coordinates"] = (min_x, min_y, max_x, max_y)
777
826
  self.patches[image_id]["crs"] = self.parents[parent_id]["crs"]
@@ -837,6 +886,7 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
837
886
  self._print_if_verbose(
838
887
  f"[INFO] Reading 'coordinates' from {image_id}.", verbose
839
888
  )
889
+
840
890
  min_x, min_y, max_x, max_y = self.images[tree_level][image_id][
841
891
  "coordinates"
842
892
  ]
@@ -1440,7 +1490,12 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1440
1490
 
1441
1491
  fig = plt.figure(figsize=figsize)
1442
1492
  plt.axis("off")
1443
- plt.imshow(img, zorder=1)
1493
+
1494
+ # check if grayscale
1495
+ if len(img.getbands()) == 1:
1496
+ plt.imshow(img, cmap="gray", vmin=0, vmax=255, zorder=1)
1497
+ else:
1498
+ plt.imshow(img, zorder=1)
1444
1499
 
1445
1500
  if column_to_plot:
1446
1501
  print(
@@ -1558,7 +1613,11 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
1558
1613
  parent_path = parent_images[parent_id]["image_path"]
1559
1614
  parent_image = Image.open(parent_path)
1560
1615
 
1561
- ax.imshow(parent_image)
1616
+ # check if grayscale
1617
+ if len(parent_image.getbands()) == 1:
1618
+ ax.imshow(parent_image, cmap="gray", vmin=0, vmax=255)
1619
+ else:
1620
+ ax.imshow(parent_image)
1562
1621
 
1563
1622
  if save_kml_dir:
1564
1623
  os.makedirs(save_kml_dir, exist_ok=True)
@@ -2076,6 +2135,116 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
2076
2135
  tree_level = "parent" if bool(self.parents.get(image_id)) else "patch"
2077
2136
  return tree_level
2078
2137
 
2138
+ def save_parents_as_geotiffs(
2139
+ self,
2140
+ rewrite: bool = False,
2141
+ verbose: bool = False,
2142
+ crs: str | None = None,
2143
+ ) -> None:
2144
+ """Save all parents in MapImages instance as geotiffs.
2145
+
2146
+ Parameters
2147
+ ----------
2148
+ rewrite : bool, optional
2149
+ Whether to rewrite files if they already exist, by default False
2150
+ verbose : bool, optional
2151
+ Whether to print verbose outputs, by default False
2152
+ crs : str, optional
2153
+ The CRS of the coordinates.
2154
+ If None, the method will first look for ``crs`` in the parents dictionary and use those. If ``crs`` cannot be found in the dictionary, the method will use "EPSG:4326".
2155
+ By default None.
2156
+ """
2157
+
2158
+ parents_list = self.list_parents()
2159
+
2160
+ for parent_id in tqdm(parents_list):
2161
+ self._save_parent_as_geotiff(parent_id, rewrite, verbose, crs)
2162
+
2163
+ def _save_parent_as_geotiff(
2164
+ self,
2165
+ parent_id: str,
2166
+ rewrite: bool = False,
2167
+ verbose: bool = False,
2168
+ crs: str | None = None,
2169
+ ) -> None:
2170
+ """Save a parent image as a geotiff.
2171
+
2172
+ Parameters
2173
+ ----------
2174
+ parent_id : str
2175
+ The ID of the parent to write.
2176
+ rewrite : bool, optional
2177
+ Whether to rewrite files if they already exist, by default False
2178
+ verbose : bool, optional
2179
+ Whether to print verbose outputs, by default False
2180
+ crs : Optional[str], optional
2181
+ The CRS of the coordinates.
2182
+ If None, the method will first look for ``crs`` in the parents dictionary and use those. If ``crs`` cannot be found in the dictionary, the method will use "EPSG:4326".
2183
+ By default None.
2184
+
2185
+ Raises
2186
+ ------
2187
+ ValueError
2188
+ If parent directory does not exist.
2189
+ """
2190
+
2191
+ parent_path = self.parents[parent_id]["image_path"]
2192
+ parent_dir = os.path.dirname(parent_path)
2193
+
2194
+ if not os.path.exists(parent_dir):
2195
+ raise ValueError(f'[ERROR] Parent directory "{parent_dir}" does not exist.')
2196
+
2197
+ parent_id_no_ext = os.path.splitext(parent_id)[0]
2198
+ geotiff_path = f"{parent_dir}/{parent_id_no_ext}.tif"
2199
+
2200
+ self.parents[parent_id]["geotiff_path"] = geotiff_path
2201
+
2202
+ if os.path.isfile(f"{geotiff_path}"):
2203
+ if not rewrite:
2204
+ self._print_if_verbose(
2205
+ f"[INFO] File already exists: {geotiff_path}.", verbose
2206
+ )
2207
+ return
2208
+
2209
+ self._print_if_verbose(
2210
+ f"[INFO] Creating: {geotiff_path}.",
2211
+ verbose,
2212
+ )
2213
+
2214
+ if "shape" not in self.parents[parent_id].keys():
2215
+ self._add_shape_id(parent_id)
2216
+ height, width, channels = self.parents[parent_id]["shape"]
2217
+
2218
+ if "coordinates" not in self.parents[parent_id].keys():
2219
+ print(self.parents[parent_id].keys())
2220
+ raise ValueError(f"[ERROR] Cannot locate coordinates for {parent_id}")
2221
+ coords = self.parents[parent_id]["coordinates"]
2222
+
2223
+ if not crs:
2224
+ crs = self.parents[parent_id].get("crs", "EPSG:4326")
2225
+
2226
+ parent_affine = rasterio.transform.from_bounds(*coords, width, height)
2227
+ parent = Image.open(parent_path)
2228
+
2229
+ with rasterio.open(
2230
+ f"{geotiff_path}",
2231
+ "w",
2232
+ driver="GTiff",
2233
+ height=parent.height,
2234
+ width=parent.width,
2235
+ count=channels,
2236
+ transform=parent_affine,
2237
+ dtype="uint8",
2238
+ nodata=0,
2239
+ crs=crs,
2240
+ ) as dst:
2241
+ if len(parent.getbands()) == 1:
2242
+ parent_array = np.array(parent)
2243
+ dst.write(parent_array, indexes=1)
2244
+ else:
2245
+ parent_array = reshape_as_raster(parent)
2246
+ dst.write(parent_array)
2247
+
2079
2248
  def save_patches_as_geotiffs(
2080
2249
  self,
2081
2250
  rewrite: bool | None = False,
@@ -2152,10 +2321,12 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
2152
2321
  verbose,
2153
2322
  )
2154
2323
 
2324
+ # get shape
2155
2325
  if "shape" not in self.patches[patch_id].keys():
2156
2326
  self._add_shape_id(patch_id)
2157
2327
  height, width, channels = self.patches[patch_id]["shape"]
2158
2328
 
2329
+ # get coords
2159
2330
  if "coordinates" not in self.patches[patch_id].keys():
2160
2331
  self._add_patch_coords_id(patch_id)
2161
2332
  coords = self.patches[patch_id]["coordinates"]
@@ -2165,7 +2336,6 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
2165
2336
 
2166
2337
  patch_affine = rasterio.transform.from_bounds(*coords, width, height)
2167
2338
  patch = Image.open(patch_path)
2168
- patch_array = reshape_as_raster(patch)
2169
2339
 
2170
2340
  with rasterio.open(
2171
2341
  f"{geotiff_path}",
@@ -2179,7 +2349,12 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
2179
2349
  nodata=0,
2180
2350
  crs=crs,
2181
2351
  ) as dst:
2182
- dst.write(patch_array)
2352
+ if len(patch.getbands()) == 1:
2353
+ patch_array = np.array(patch)
2354
+ dst.write(patch_array, indexes=1)
2355
+ else:
2356
+ patch_array = reshape_as_raster(patch)
2357
+ dst.write(patch_array)
2183
2358
 
2184
2359
  def save_patches_to_geojson(
2185
2360
  self,
@@ -2213,6 +2388,10 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
2213
2388
  self.add_patch_polygons()
2214
2389
  _, patch_df = self.convert_images()
2215
2390
 
2391
+ patch_df["polygon"] = patch_df["polygon"].apply(
2392
+ lambda x: x if isinstance(x, Polygon) else wkt.loads(x)
2393
+ )
2394
+
2216
2395
  if not crs:
2217
2396
  if "crs" in patch_df.columns:
2218
2397
  if len(patch_df["crs"].unique()) == 1:
@@ -2220,14 +2399,16 @@ See https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes for mor
2220
2399
  else:
2221
2400
  crs = "EPSG:4326"
2222
2401
 
2402
+ if "image_id" in patch_df.columns:
2403
+ patch_df.drop(columns=["image_id"], inplace=True)
2223
2404
  patch_df.reset_index(names="image_id", inplace=True)
2224
2405
 
2225
2406
  # drop pixel stats columns
2226
2407
  patch_df.drop(columns=patch_df.filter(like="pixel", axis=1), inplace=True)
2227
- # drop tuple columns - cause errors
2408
+ # change tuple columns to strings
2228
2409
  for col in patch_df.columns:
2229
2410
  if isinstance(patch_df[col][0], tuple):
2230
- patch_df.drop(columns=col, inplace=True)
2411
+ patch_df[col] = patch_df[col].apply(str)
2231
2412
 
2232
2413
  geo_patch_df = geopd.GeoDataFrame(patch_df, geometry="polygon", crs=crs)
2233
2414
  geo_patch_df.to_file(geojson_fname, driver="GeoJSON")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mapreader
3
- Version: 1.1.0
3
+ Version: 1.1.1
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
@@ -83,6 +83,9 @@ Requires-Dist: flake8<7.0.0,>=6.0.0; extra == "dev"
83
83
  </a>
84
84
  <a href="https://zenodo.org/badge/latestdoi/430661738"><img src="https://zenodo.org/badge/430661738.svg" alt="DOI"></a>
85
85
  <br/>
86
+ <a href="https://codecov.io/github/Living-with-machines/MapReader" >
87
+ <img src="https://codecov.io/github/Living-with-machines/MapReader/graph/badge.svg?token=38GQ3O1GB5"/>
88
+ </a>
86
89
  </p>
87
90
 
88
91
 
@@ -211,16 +211,6 @@ def test_init_dfs_value_error(load_dfs):
211
211
  )
212
212
 
213
213
 
214
- def test_no_url_col(load_dfs):
215
- parent_df, patch_df, _ = load_dfs
216
- parent_df = parent_df.drop(columns=["url"])
217
- with pytest.raises(ValueError, match="should contain a 'url' column"):
218
- Annotator(
219
- patch_df=patch_df,
220
- parent_df=parent_df,
221
- )
222
-
223
-
224
214
  def test_no_image_path_col(load_dfs):
225
215
  parent_df, patch_df, _ = load_dfs
226
216
  patch_df = patch_df.drop(columns=["image_path"])
@@ -255,3 +245,13 @@ def test_fpaths_metadata_filenotfound_error(load_dfs, sample_dir):
255
245
  parent_paths=f"{sample_dir}/cropped_74488689.png",
256
246
  metadata_path="fake_df.csv",
257
247
  )
248
+
249
+
250
+ def test_unknown_arg_error(load_dfs):
251
+ parent_df, patch_df, _ = load_dfs
252
+ with pytest.raises(TypeError):
253
+ Annotator(
254
+ patch_df=patch_df,
255
+ parent_df=parent_df,
256
+ fake_arg=1,
257
+ )
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from ast import literal_eval
4
5
  from pathlib import Path
5
6
 
7
+ import pandas as pd
6
8
  import pytest
9
+ from pytest import approx
7
10
  from shapely.geometry import LineString, MultiPolygon, Polygon
8
11
 
9
12
  from mapreader import SheetDownloader
@@ -286,11 +289,54 @@ def test_download_all(sheet_downloader, tmp_path):
286
289
  sd.download_all_map_sheets(maps_path, metadata_fname)
287
290
  assert os.path.exists(f"{maps_path}/map_102352861.png")
288
291
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
289
- with open(f"{maps_path}/{metadata_fname}") as f:
290
- csv = f.readlines()
291
- assert len(csv) == 5
292
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb\n"
293
- assert csv[3].startswith("2,map_102352861.png")
292
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
293
+ assert len(df) == 4
294
+ assert list(df.columns) == [
295
+ "name",
296
+ "url",
297
+ "coordinates",
298
+ "crs",
299
+ "published_date",
300
+ "grid_bb",
301
+ ]
302
+ assert list(df["name"]) == [
303
+ "map_101602026.png",
304
+ "map_101602038.png",
305
+ "map_102352861.png",
306
+ "map_91617032.png",
307
+ ]
308
+ # test coords
309
+ assert literal_eval(df.loc[0, "coordinates"]) == approx(
310
+ (-1.0546875, 53.33087298301705, -0.703125, 53.54030739150021), rel=1e-6
311
+ )
312
+
313
+ sd.get_grid_bb(14)
314
+ maps_path = tmp_path / "test_maps_14/"
315
+ metadata_fname = "test_metadata.csv"
316
+ sd.download_all_map_sheets(maps_path, metadata_fname)
317
+ assert os.path.exists(f"{maps_path}/map_102352861.png")
318
+ assert os.path.exists(f"{maps_path}/{metadata_fname}")
319
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
320
+ assert len(df) == 4
321
+ assert list(df.columns) == [
322
+ "name",
323
+ "url",
324
+ "coordinates",
325
+ "crs",
326
+ "published_date",
327
+ "grid_bb",
328
+ ]
329
+ assert list(df["name"]) == [
330
+ "map_101602026.png",
331
+ "map_101602038.png",
332
+ "map_102352861.png",
333
+ "map_91617032.png",
334
+ ]
335
+ # test coords
336
+ assert literal_eval(df.loc[0, "coordinates"]) == approx(
337
+ (-0.98876953125, 53.448806835427575, -0.90087890625, 53.48804553605621),
338
+ rel=1e-6,
339
+ )
294
340
 
295
341
 
296
342
  def test_download_all_kwargs(sheet_downloader, tmp_path):
@@ -304,12 +350,27 @@ def test_download_all_kwargs(sheet_downloader, tmp_path):
304
350
  }
305
351
  sd.download_all_map_sheets(maps_path, metadata_fname, **kwargs)
306
352
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
307
- with open(f"{maps_path}/{metadata_fname}") as f:
308
- csv = f.readlines()
309
- assert len(csv) == 5
310
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb,test1,test2\n"
311
- assert "Six_Inch_GB_WFS.132" in csv[4]
312
- assert "2021" in csv[4]
353
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
354
+ assert len(df) == 4
355
+ assert list(df.columns) == [
356
+ "name",
357
+ "url",
358
+ "coordinates",
359
+ "crs",
360
+ "published_date",
361
+ "grid_bb",
362
+ "test1",
363
+ "test2",
364
+ ]
365
+ assert list(df["name"]) == [
366
+ "map_101602026.png",
367
+ "map_101602038.png",
368
+ "map_102352861.png",
369
+ "map_91617032.png",
370
+ ]
371
+ assert df.loc[3, "published_date"] == 2021
372
+ assert df.loc[3, "test1"] == "test"
373
+ assert df.loc[3, "test2"] == "Six_Inch_GB_WFS.132"
313
374
 
314
375
 
315
376
  def test_download_by_wfs_ids(sheet_downloader, tmp_path):
@@ -322,27 +383,32 @@ def test_download_by_wfs_ids(sheet_downloader, tmp_path):
322
383
  ) # test single wfs_id
323
384
  assert os.path.exists(f"{maps_path}/map_101602026.png")
324
385
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
325
- with open(f"{maps_path}/{metadata_fname}") as f:
326
- csv = f.readlines()
327
- assert len(csv) == 2
328
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb\n"
329
- assert csv[1].startswith("0,map_101602026.png")
386
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
387
+ assert len(df) == 1
388
+ assert list(df.columns) == [
389
+ "name",
390
+ "url",
391
+ "coordinates",
392
+ "crs",
393
+ "published_date",
394
+ "grid_bb",
395
+ ]
396
+ assert df.loc[0, "name"] == "map_101602026.png"
330
397
 
331
398
  sd.download_map_sheets_by_wfs_ids(
332
399
  [16320, 16321], maps_path, metadata_fname
333
400
  ) # test list of wfs_ids
334
401
  assert os.path.exists(f"{maps_path}/map_101602038.png")
335
- with open(f"{maps_path}/{metadata_fname}") as f:
336
- csv = f.readlines()
337
- assert len(csv) == 3 # should have only downloaded/added one extra map
338
-
402
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
403
+ assert len(df) == 2 # should have only downloaded/added one extra map
404
+ assert df.loc[1, "name"] == "map_101602038.png"
339
405
  sd.download_map_sheets_by_wfs_ids(
340
406
  16320, maps_path, metadata_fname, overwrite=True
341
407
  ) # test overwrite
342
- with open(f"{maps_path}/{metadata_fname}") as f:
343
- csv = f.readlines()
344
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb\n"
345
- assert csv[1].startswith("0,map_101602026.png")
408
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
409
+ assert len(df) == 2
410
+ assert df.loc[0, "name"] == "map_101602026.png"
411
+ assert df.loc[1, "name"] == "map_101602038.png"
346
412
 
347
413
 
348
414
  def test_download_by_wfs_ids_errors(sheet_downloader, tmp_path):
@@ -378,19 +444,25 @@ def test_download_by_polygon(sheet_downloader, tmp_path):
378
444
  ) # test mode = 'within'
379
445
  assert os.path.exists(f"{maps_path}/map_101602026.png")
380
446
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
381
- with open(f"{maps_path}/{metadata_fname}") as f:
382
- csv = f.readlines()
383
- assert len(csv) == 2
384
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb\n"
385
- assert csv[1].startswith("0,map_101602026.png")
447
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
448
+ assert len(df) == 1
449
+ assert list(df.columns) == [
450
+ "name",
451
+ "url",
452
+ "coordinates",
453
+ "crs",
454
+ "published_date",
455
+ "grid_bb",
456
+ ]
457
+ assert df.loc[0, "name"] == "map_101602026.png"
386
458
 
387
459
  sd.download_map_sheets_by_polygon(
388
460
  polygon, maps_path, metadata_fname, mode="intersects"
389
461
  ) # test mode = 'intersects', now 2 maps
390
462
  assert os.path.exists(f"{maps_path}/map_101602038.png")
391
- with open(f"{maps_path}/{metadata_fname}") as f:
392
- csv = f.readlines()
393
- assert len(csv) == 3 # should have only downloaded/added one extra map
463
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
464
+ assert len(df) == 2 # should have only downloaded/added one extra map
465
+ assert df.loc[1, "name"] == "map_101602038.png"
394
466
 
395
467
 
396
468
  def test_download_by_polygon_errors(sheet_downloader, tmp_path):
@@ -417,11 +489,17 @@ def test_download_by_coords(sheet_downloader, tmp_path):
417
489
  sd.download_map_sheets_by_coordinates((-0.99, 53.43), maps_path, metadata_fname)
418
490
  assert os.path.exists(f"{maps_path}/map_101602038.png")
419
491
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
420
- with open(f"{maps_path}/{metadata_fname}") as f:
421
- csv = f.readlines()
422
- assert len(csv) == 2
423
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb\n"
424
- assert csv[1].startswith("0,map_101602038.png")
492
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
493
+ assert len(df) == 1
494
+ assert list(df.columns) == [
495
+ "name",
496
+ "url",
497
+ "coordinates",
498
+ "crs",
499
+ "published_date",
500
+ "grid_bb",
501
+ ]
502
+ assert df.loc[0, "name"] == "map_101602038.png"
425
503
 
426
504
 
427
505
  def test_download_by_coords_errors(sheet_downloader, tmp_path):
@@ -442,11 +520,17 @@ def test_download_by_line(sheet_downloader, tmp_path):
442
520
  sd.download_map_sheets_by_line(line, maps_path, metadata_fname)
443
521
  assert os.path.exists(f"{maps_path}/map_101602026.png")
444
522
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
445
- with open(f"{maps_path}/{metadata_fname}") as f:
446
- csv = f.readlines()
447
- assert len(csv) == 3
448
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb\n"
449
- assert csv[1].startswith("0,map_101602026.png")
523
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
524
+ assert len(df) == 2
525
+ assert list(df.columns) == [
526
+ "name",
527
+ "url",
528
+ "coordinates",
529
+ "crs",
530
+ "published_date",
531
+ "grid_bb",
532
+ ]
533
+ assert list(df["name"]) == ["map_101602026.png", "map_101602038.png"]
450
534
 
451
535
 
452
536
  def test_download_by_line_errors(sheet_downloader, tmp_path):
@@ -469,30 +553,34 @@ def test_download_by_string(sheet_downloader, tmp_path):
469
553
  ) # test w/ keys list
470
554
  assert os.path.exists(f"{maps_path}/map_91617032.png")
471
555
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
472
- with open(f"{maps_path}/{metadata_fname}") as f:
473
- csv = f.readlines()
474
- assert len(csv) == 2
475
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb\n"
476
- assert csv[1].startswith("0,map_91617032.png")
556
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
557
+ assert len(df) == 1
558
+ assert list(df.columns) == [
559
+ "name",
560
+ "url",
561
+ "coordinates",
562
+ "crs",
563
+ "published_date",
564
+ "grid_bb",
565
+ ]
566
+ assert df.loc[0, "name"] == "map_91617032.png"
477
567
 
478
568
  sd.download_map_sheets_by_string(
479
569
  "Six_Inch_GB_WFS.16320", "id", maps_path, metadata_fname
480
570
  ) # test append + w/ keys as string
481
571
  assert os.path.exists(f"{maps_path}/map_101602026.png")
482
- with open(f"{maps_path}/{metadata_fname}") as f:
483
- csv = f.readlines()
484
- assert len(csv) == 3
485
- assert csv[2].startswith("1,map_101602026.png")
572
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
573
+ assert len(df) == 2
574
+ assert df.loc[1, "name"] == "map_101602026.png"
486
575
 
487
576
  sd.download_map_sheets_by_string(
488
577
  "III.SW", path_save=maps_path, metadata_fname=metadata_fname
489
578
  ) # test w/ no keys
490
579
  assert os.path.exists(f"{maps_path}/map_101602038.png")
491
580
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
492
- with open(f"{maps_path}/{metadata_fname}") as f:
493
- csv = f.readlines()
494
- assert len(csv) == 4
495
- assert csv[3].startswith("2,map_101602038.png")
581
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
582
+ assert len(df) == 3
583
+ assert df.loc[2, "name"] == "map_101602038.png"
496
584
 
497
585
 
498
586
  def test_download_by_string_value_errors(sheet_downloader, tmp_path):
@@ -531,12 +619,17 @@ def test_download_by_queries(sheet_downloader, tmp_path):
531
619
  assert os.path.exists(f"{maps_path}/map_101602026.png")
532
620
  assert os.path.exists(f"{maps_path}/map_91617032.png")
533
621
  assert os.path.exists(f"{maps_path}/{metadata_fname}")
534
- with open(f"{maps_path}/{metadata_fname}") as f:
535
- csv = f.readlines()
536
- assert len(csv) == 3
537
- assert csv[0] == ",name,url,coordinates,crs,published_date,grid_bb\n"
538
- assert csv[1].startswith("0,map_101602026.png,")
539
- assert csv[2].startswith("1,map_91617032.png,")
622
+ df = pd.read_csv(f"{maps_path}/{metadata_fname}", sep=",", index_col=0)
623
+ assert len(df) == 2
624
+ assert list(df.columns) == [
625
+ "name",
626
+ "url",
627
+ "coordinates",
628
+ "crs",
629
+ "published_date",
630
+ "grid_bb",
631
+ ]
632
+ assert list(df["name"]) == ["map_101602026.png", "map_91617032.png"]
540
633
 
541
634
 
542
635
  def test_download_by_queries_errors(sheet_downloader, tmp_path):
File without changes
File without changes
File without changes
File without changes