mapreader 1.6.3.post0.dev19__tar.gz → 1.7.0.post0.dev1__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.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/PKG-INFO +2 -1
  2. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/__init__.py +6 -1
  3. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/_version.py +3 -3
  4. mapreader-1.7.0.post0.dev1/mapreader/download/iiif_downloader.py +402 -0
  5. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/PKG-INFO +2 -1
  6. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/SOURCES.txt +1 -0
  7. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/requires.txt +1 -0
  8. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/setup.py +2 -0
  9. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/tests/test_import.py +2 -1
  10. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/LICENSE +0 -0
  11. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/README.md +0 -0
  12. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/__main__.py +0 -0
  13. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/annotate/__init__.py +0 -0
  14. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/annotate/annotator.py +0 -0
  15. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/classify/__init__.py +0 -0
  16. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/classify/classifier.py +0 -0
  17. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/classify/custom_models.py +0 -0
  18. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/classify/datasets.py +0 -0
  19. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/classify/load_annotations.py +0 -0
  20. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/download/__init__.py +0 -0
  21. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/download/data_structures.py +0 -0
  22. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/download/downloader.py +0 -0
  23. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/download/downloader_utils.py +0 -0
  24. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/download/sheet_downloader.py +0 -0
  25. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/download/tile_loading.py +0 -0
  26. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/download/tile_merging.py +0 -0
  27. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/load/__init__.py +0 -0
  28. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/load/geo_utils.py +0 -0
  29. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/load/images.py +0 -0
  30. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/load/loader.py +0 -0
  31. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/process/__init__.py +0 -0
  32. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/process/context_post_process.py +0 -0
  33. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/process/occlusion_analysis.py +0 -0
  34. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/process/process.py +0 -0
  35. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/__init__.py +0 -0
  36. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/dataclasses.py +0 -0
  37. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/deepsolo_runner.py +0 -0
  38. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/dptext_detr_runner.py +0 -0
  39. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/maptext_runner.py +0 -0
  40. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/runner_base.py +0 -0
  41. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/utils/__init__.py +0 -0
  42. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/utils/compute_and_save_stats.py +0 -0
  43. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/utils/load_frames.py +0 -0
  44. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader/utils/slice_parallel.py +0 -0
  45. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/dependency_links.txt +0 -0
  46. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/entry_points.txt +0 -0
  47. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/not-zip-safe +0 -0
  48. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/top_level.txt +0 -0
  49. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/setup.cfg +0 -0
  50. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/tests/test_geo_pipeline.py +0 -0
  51. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/tests/test_utils_load_frames.py +0 -0
  52. {mapreader-1.6.3.post0.dev19 → mapreader-1.7.0.post0.dev1}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapreader
3
- Version: 1.6.3.post0.dev19
3
+ Version: 1.7.0.post0.dev1
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
@@ -62,6 +62,7 @@ Requires-Dist: opencv-python<5.0.0.0,>=4.8.1.78
62
62
  Requires-Dist: folium<1.0.0,>=0.12
63
63
  Requires-Dist: mapclassify<3.0.0,>=2.0.0
64
64
  Requires-Dist: xyzservices==2024.9.0
65
+ Requires-Dist: lxml
65
66
  Provides-Extra: dev
66
67
  Requires-Dist: pytest<9.0.0; extra == "dev"
67
68
  Requires-Dist: pytest-cov<6.0.0,>=4.1.0; extra == "dev"
@@ -1,5 +1,10 @@
1
1
  # download
2
2
  from mapreader.download.sheet_downloader import SheetDownloader
3
+ try:
4
+ from mapreader.download.iiif_downloader import IIIFDownloader
5
+ except ImportError:
6
+ print("[WARNING] Piffle not installed, please install it from the `iiif_dataclasses` branch of https://github.com/rwood-97/piffle/tree/iiif_dataclasses")
7
+ pass
3
8
  from mapreader.download.downloader import Downloader
4
9
  from mapreader.download.downloader_utils import create_polygon_from_latlons, create_line_from_latlons
5
10
 
@@ -52,4 +57,4 @@ import mapreader
52
57
 
53
58
  def print_version():
54
59
  """Print the current version of mapreader."""
55
- print(mapreader.__version__)
60
+ print(mapreader.__version__)
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-03-24T08:13:15+0000",
11
+ "date": "2025-04-10T09:06:03+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "6a1154eaa44a30358fa4146cac49af242e1b51b0",
15
- "version": "1.6.3.post0.dev19"
14
+ "full-revisionid": "f7bfc0a12e8f33f0a79343b15bf2c1f8282ff294",
15
+ "version": "1.7.0.post0.dev1"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -0,0 +1,402 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ import pathlib
6
+
7
+ import geopandas as gpd
8
+ import numpy as np
9
+ import pandas as pd
10
+ import rasterio
11
+ import rasterio.mask
12
+ import requests
13
+ from bs4 import BeautifulSoup
14
+ from piffle.iiif_dataclasses.presentation2 import IIIFPresentation2
15
+ from piffle.iiif_dataclasses.presentation3 import (
16
+ GeoreferenceAnnotation3,
17
+ IIIFPresentation3,
18
+ )
19
+ from piffle.load_iiif import load_iiif_image, load_iiif_presentation
20
+ from PIL import Image, ImageDraw
21
+ from rasterio import transform
22
+ from rasterio.plot import reshape_as_raster
23
+ from shapely import Polygon
24
+ from tqdm.auto import tqdm
25
+
26
+
27
+ class IIIFDownloader:
28
+ def __init__(
29
+ self,
30
+ iiif: str
31
+ | IIIFPresentation3
32
+ | IIIFPresentation2
33
+ | list[str | IIIFPresentation3 | IIIFPresentation2],
34
+ iiif_versions: int | float | str | list[int | float | str] | None = None,
35
+ iiif_uris: str | list[str] | None = None,
36
+ ):
37
+ """A class to download images from IIIF presentation jsons.
38
+
39
+ Parameters
40
+ ----------
41
+ iiif : str | IIIFPresentation3 | IIIFPresentation2 | list[str | IIIFPresentation3 | IIIFPresentation2]
42
+ The IIIF url or IIIF presentation API object(s) containing the maps to download
43
+ iiif_versions : int | float | str | list[int | float | str] | None
44
+ The IIIF version(s) of the IIIF object(s). Can only be None if `iiif` is/are already IIIF object(s). Default is None.
45
+ iiif_uris : str | list[str] | None
46
+ The URI(s) of the IIIF object(s), needed if the IIIF object is missing an 'id' field. Default is None.
47
+
48
+ If only one IIIF object is missing an `id`, create a list with None values for the other objects.
49
+ e.g. iiif_uri=[None, "https://example.com/iiif/manifest.json", None]
50
+ """
51
+ if not isinstance(iiif, list):
52
+ iiif = [iiif]
53
+ if iiif_versions is None:
54
+ iiif_versions = [None] * len(iiif)
55
+ if not isinstance(iiif_versions, list):
56
+ iiif_versions = [iiif_versions]
57
+ if len(iiif) != len(iiif_versions):
58
+ raise ValueError(
59
+ "[ERROR] Length of `iiif` must match length of `iiif_versions`."
60
+ )
61
+
62
+ iiif_objs = []
63
+ for iiif_obj, iiif_version in zip(iiif, iiif_versions):
64
+ if isinstance(iiif_obj, str):
65
+ iiif_obj = load_iiif_presentation(iiif_obj, iiif_version)
66
+ if isinstance(iiif_obj, (IIIFPresentation3, IIIFPresentation2)):
67
+ iiif_objs.append(iiif_obj)
68
+ else:
69
+ raise ValueError("[ERROR] Each `iiif` must be a string or IIIF object.")
70
+
71
+ if iiif_uris is None:
72
+ iiif_uris = [None] * len(iiif_objs)
73
+ if not isinstance(iiif_uris, list):
74
+ iiif_uris = [iiif_uris]
75
+ if len(iiif_uris) != len(iiif_objs):
76
+ raise ValueError(
77
+ "[ERROR] Length of `iiif_uris` must match length of `iiif`."
78
+ )
79
+
80
+ for iiif_obj, iiif_uri in zip(iiif_objs, iiif_uris):
81
+ if not hasattr(iiif_obj, "id"):
82
+ if iiif_uri is not None:
83
+ iiif_obj.id = iiif_uri
84
+ else:
85
+ raise ValueError(
86
+ "[ERROR] One of your IIIF objects is missing an 'id' field so we cannot identify it's URL. Please pass the `iiif_uris` argument."
87
+ )
88
+
89
+ self.iiif = iiif_objs
90
+
91
+ def save_georeferenced_maps(
92
+ self,
93
+ path_save: str | pathlib.Path = "maps",
94
+ ):
95
+ """Save georefereced maps from a IIIF presentation 3 json as tiffs.
96
+
97
+ Parameters
98
+ ----------
99
+ path_save : str | pathlib.Path
100
+ Path to save the images
101
+
102
+ Notes
103
+ -----
104
+ Calls the `save_georeferenced_map` method for each IIIF object.
105
+ """
106
+ for iiif in self.iiif:
107
+ self.save_georeferenced_map(iiif, path_save=path_save)
108
+
109
+ def save_georeferenced_map(
110
+ self,
111
+ iiif: str | IIIFPresentation3,
112
+ path_save: str | pathlib.Path = "maps",
113
+ iiif_uri: str = None,
114
+ ):
115
+ """Save a single georefereced map from a IIIF presentation 3 json as a tiff.
116
+
117
+ Parameters
118
+ ----------
119
+ iiif : str | dict | IIIFPresentation3
120
+ the IIIF url or IIIF presentation API object containing the map(s)
121
+ path_save : str | pathlib.Path
122
+ Path to save the images
123
+ iiif_uri : str
124
+ The URI of the IIIF object, needed if the IIIF object is missing an 'id' field.
125
+
126
+ Notes
127
+ -----
128
+ Only first order polynomial transformations are currently supported, see the [allmaps documentation](https://allmaps.org/) for more information on the transformation options.
129
+
130
+ Raises
131
+ ------
132
+ ValueError
133
+ Only first order polynomial transformations are currently supported.
134
+ """
135
+ if isinstance(iiif, str):
136
+ iiif_obj = load_iiif_presentation(iiif, 3)
137
+ elif isinstance(iiif, IIIFPresentation3):
138
+ iiif_obj = iiif
139
+ else:
140
+ raise ValueError("[ERROR] `iiif` must be a string or IIIF3 object.")
141
+
142
+ if not hasattr(iiif_obj, "id"):
143
+ if iiif_uri is not None:
144
+ iiif_obj.id = iiif_uri
145
+ else:
146
+ raise ValueError(
147
+ "[ERROR] IIIF object is missing an 'id' field so we cannot identify its URI. Please manually pass the `iiif_uri` argument."
148
+ )
149
+ iiif_uri = iiif_obj.id
150
+
151
+ metadata = pd.DataFrame(columns=["name", "id", "iiif_uri"])
152
+ for annot in tqdm(iiif_obj.collect_annotations()):
153
+ if not isinstance(annot, GeoreferenceAnnotation3):
154
+ print(
155
+ f"[WARNING] Skipping annotation '{annot.id}' as it is not a georeference annotation."
156
+ )
157
+ continue
158
+
159
+ # Check we are working with first order polynomial transformation (i.e. affine)
160
+ if (annot.body["transformation"]["type"] != "polynomial") or (
161
+ annot.body["transformation"]["options"]["order"] != 1
162
+ ):
163
+ raise ValueError(
164
+ "[ERROR] Only first order polynomial transformations are currently supported"
165
+ )
166
+
167
+ # Get filename
168
+ # host, prefix, identifier
169
+ id = annot.resource["service"]["id"] if annot.id is None else annot.id
170
+ fname = ".".join(id.removeprefix("https://").split("/")[2:])
171
+
172
+ if not os.path.exists(path_save):
173
+ os.makedirs(path_save, exist_ok=True)
174
+ if os.path.exists(f"{path_save}/{fname}.tif"):
175
+ print(f"[INFO] '{fname}' already exists. Skipping download.")
176
+ continue
177
+
178
+ # Download image
179
+ image = self.download_image(3, annot)
180
+
181
+ # Get GCPs
182
+ features = gpd.GeoDataFrame.from_features(
183
+ annot.body["features"], crs="WGS84"
184
+ )
185
+ resource_points = features[
186
+ "resourceCoords"
187
+ ].to_list() # in image coordinates
188
+ gcp_points = features["geometry"].to_list() # in WGS84
189
+
190
+ # resource points are x, y (i.e col, row)
191
+ # gcp points are lon, lat (i.e x, y)
192
+
193
+ # Create affine transform
194
+ affine = transform.from_gcps(
195
+ [
196
+ transform.GroundControlPoint(
197
+ resource_point[1], resource_point[0], gcp_point.x, gcp_point.y
198
+ )
199
+ for resource_point, gcp_point in zip(resource_points, gcp_points)
200
+ ]
201
+ )
202
+
203
+ # Idenfity annotation bounds
204
+ svg = BeautifulSoup(annot.target["selector"]["value"], "lxml").find("svg")
205
+ points = svg.find("polygon")["points"]
206
+ points = np.array(
207
+ [
208
+ [int(x), int(y)]
209
+ for x, y in [point.split(",") for point in points.split()]
210
+ ]
211
+ ) # points are x, y (i.e col, row)
212
+
213
+ # Apply affine transform to annotation bounds
214
+ t = transform.AffineTransformer(affine)
215
+ x1, y1 = t.xy(*points[1][::-1]) # input as row, col (i.e y, x)
216
+ x0, y0 = t.xy(*points[0][::-1])
217
+ x2, y2 = t.xy(*points[2][::-1])
218
+ x3, y3 = t.xy(*points[3][::-1])
219
+
220
+ # Save tiff
221
+ height, width, channels = image.height, image.width, len(image.getbands())
222
+
223
+ with rasterio.open(
224
+ f"{path_save}/{fname}.tif",
225
+ "w",
226
+ driver="GTiff",
227
+ height=height,
228
+ width=width,
229
+ count=channels,
230
+ transform=affine,
231
+ dtype="uint8",
232
+ nodata=0,
233
+ crs="WGS84",
234
+ ) as dst:
235
+ array = reshape_as_raster(
236
+ image
237
+ ) # (row, col, bands) to (bands, row, col)
238
+ dst.write(array)
239
+
240
+ # Mask tiff to annotation bounds
241
+ tiff_image = rasterio.open(f"{path_save}/{fname}.tif")
242
+
243
+ polygon = Polygon([(x0, y0), (x1, y1), (x2, y2), (x3, y3)])
244
+ masked_image, out_transform = rasterio.mask.mask(
245
+ tiff_image, [polygon], crop=False
246
+ )
247
+ out_meta = tiff_image.meta.copy()
248
+
249
+ out_meta.update(
250
+ {
251
+ "driver": "GTiff",
252
+ "height": masked_image.shape[1],
253
+ "width": masked_image.shape[2],
254
+ "transform": out_transform,
255
+ }
256
+ )
257
+
258
+ with rasterio.open(
259
+ f"{path_save}/{fname}_masked.tif", "w", **out_meta
260
+ ) as dst:
261
+ dst.write(masked_image)
262
+
263
+ metadata.loc[len(metadata)] = [f"{fname}_masked.tif", fname, iiif_obj.id]
264
+ metadata.to_csv(f"{path_save}/metadata.csv")
265
+
266
+ def save_maps(
267
+ self,
268
+ path_save: str | pathlib.Path = "maps",
269
+ ):
270
+ """Save maps from a IIIF presentation json as pngs.
271
+
272
+ Parameters
273
+ ----------
274
+ path_save : str | pathlib.Path
275
+ Path to save the images
276
+
277
+ Notes
278
+ -----
279
+ Calls the `save_map` method for each IIIF object.
280
+ """
281
+ for iiif in self.iiif:
282
+ self.save_map(iiif, path_save=path_save)
283
+
284
+ def save_map(
285
+ self,
286
+ iiif: str | IIIFPresentation3 | IIIFPresentation2,
287
+ iiif_version: int | float | str = 3,
288
+ path_save: str | pathlib.Path = "maps",
289
+ iiif_uri: str = None,
290
+ ):
291
+ """Save a single map from a IIIF presentation json as a png.
292
+
293
+ Parameters
294
+ ----------
295
+ iiif : str | dict | IIIFPresentation3 | IIIFPresentation2
296
+ the IIIF url or IIIF presentation API object containing the map(s)
297
+ iiif_version : int | float | str
298
+ The IIIF version, ignored if iiif is already a IIIF object
299
+ path_save : str | pathlib.Path
300
+ Path to save the images
301
+ iiif_uri : str
302
+ The URI of the IIIF object, needed if the IIIF object is missing an 'id' field.
303
+ """
304
+ if isinstance(iiif, str):
305
+ iiif_obj = load_iiif_presentation(iiif, iiif_version)
306
+ elif isinstance(iiif, (IIIFPresentation3, IIIFPresentation2)):
307
+ iiif_obj = iiif
308
+ else:
309
+ raise ValueError("`iiif` must be a string or IIIF object.")
310
+
311
+ if not hasattr(iiif_obj, "id"):
312
+ if iiif_uri is not None:
313
+ iiif_obj.id = iiif_uri
314
+ else:
315
+ raise ValueError(
316
+ "[ERROR] IIIF object is missing an 'id' field so we cannot identify its URL. Please manually pass the `iiif_url` argument."
317
+ )
318
+ iiif_uri = iiif_obj.id
319
+
320
+ metadata = pd.DataFrame(columns=["filename", "iiif_uri"])
321
+ for annot in tqdm(iiif_obj.collect_annotations()):
322
+ # Get filename
323
+ # host, prefix, identifier
324
+ id = annot.resource["service"]["id"] if annot.id is None else annot.id
325
+ fname = ".".join(id.removeprefix("https://").split("/")[2:])
326
+
327
+ if not os.path.exists(path_save):
328
+ os.makedirs(path_save, exist_ok=True)
329
+ if os.path.exists(f"{path_save}/{fname}.png"):
330
+ print(f"[INFO] '{fname}' already exists. Skipping download.")
331
+ continue
332
+
333
+ metadata.loc[len(metadata)] = [fname, iiif_obj.id]
334
+
335
+ # Download image
336
+ image = self.download_image(iiif_version, annot)
337
+ image.save(f"{path_save}/{fname}.png")
338
+
339
+ if isinstance(iiif_obj, IIIFPresentation3):
340
+ if "selector" in annot.target.keys():
341
+ # Idenfity annotation bounds
342
+ svg = BeautifulSoup(annot.target["selector"]["value"], "xml").find(
343
+ "svg"
344
+ )
345
+ points = svg.find("polygon")["points"]
346
+ points = np.array(
347
+ [
348
+ [int(x), int(y)]
349
+ for x, y in [point.split(",") for point in points.split()]
350
+ ]
351
+ ) # points are x, y (i.e col, row)
352
+
353
+ # -- Mask image to annotation bounds
354
+ # Convert to numpy array
355
+ image_array = np.array(image)
356
+
357
+ # Create mask
358
+ polygon = Polygon(points)
359
+ mask_image = Image.new(
360
+ "L", (image_array.shape[1], image_array.shape[0]), 0
361
+ )
362
+ ImageDraw.Draw(mask_image).polygon(
363
+ polygon.exterior.coords, outline=1, fill=1
364
+ )
365
+ mask = np.array(mask_image)
366
+
367
+ # Apply mask
368
+ image_array = image_array * mask[:, :, None]
369
+
370
+ masked_image = Image.fromarray(image_array)
371
+ masked_image.save(f"{path_save}/{fname}_masked.png")
372
+
373
+ metadata.to_csv(f"{path_save}/metadata.csv")
374
+
375
+ def download_image(iiif, iiif_version, annot):
376
+ image_url = annot.get_image_url()
377
+ image_manifest = load_iiif_image(
378
+ image_url, iiif_version
379
+ ) # TODO: method to get image version
380
+
381
+ width = image_manifest.width
382
+ height = image_manifest.height
383
+ tile_width = image_manifest.tiles[0]["width"]
384
+ tile_height = image_manifest.tiles[0].get("height", tile_width)
385
+
386
+ image = Image.new("RGB", (width, height))
387
+ for x in np.arange(0, ((width // tile_width) + 1) * tile_width, tile_width):
388
+ for y in np.arange(
389
+ 0, ((height // tile_height) + 1) * tile_height, tile_height
390
+ ):
391
+ if x + tile_width > width:
392
+ x = width - tile_width
393
+ if y + tile_height > height:
394
+ y = height - tile_height
395
+ response = requests.get(
396
+ f"{image_url}/{x},{y},512,512/full/0/default.jpg"
397
+ )
398
+ response.raise_for_status()
399
+
400
+ tile = Image.open(io.BytesIO(response.content))
401
+ image.paste(tile, (x, y))
402
+ return image
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapreader
3
- Version: 1.6.3.post0.dev19
3
+ Version: 1.7.0.post0.dev1
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
@@ -62,6 +62,7 @@ Requires-Dist: opencv-python<5.0.0.0,>=4.8.1.78
62
62
  Requires-Dist: folium<1.0.0,>=0.12
63
63
  Requires-Dist: mapclassify<3.0.0,>=2.0.0
64
64
  Requires-Dist: xyzservices==2024.9.0
65
+ Requires-Dist: lxml
65
66
  Provides-Extra: dev
66
67
  Requires-Dist: pytest<9.0.0; extra == "dev"
67
68
  Requires-Dist: pytest-cov<6.0.0,>=4.1.0; extra == "dev"
@@ -24,6 +24,7 @@ mapreader/download/__init__.py
24
24
  mapreader/download/data_structures.py
25
25
  mapreader/download/downloader.py
26
26
  mapreader/download/downloader_utils.py
27
+ mapreader/download/iiif_downloader.py
27
28
  mapreader/download/sheet_downloader.py
28
29
  mapreader/download/tile_loading.py
29
30
  mapreader/download/tile_merging.py
@@ -33,6 +33,7 @@ opencv-python<5.0.0.0,>=4.8.1.78
33
33
  folium<1.0.0,>=0.12
34
34
  mapclassify<3.0.0,>=2.0.0
35
35
  xyzservices==2024.9.0
36
+ lxml
36
37
 
37
38
  [dev]
38
39
  pytest<9.0.0
@@ -63,6 +63,8 @@ setuptools.setup(
63
63
  "folium>=0.12,<1.0.0",
64
64
  "mapclassify>=2.0.0,<3.0.0",
65
65
  "xyzservices==2024.9.0",
66
+ # "piffle @ git+https://github.com/rwood-97/piffle.git@iiif_dataclasses",
67
+ "lxml",
66
68
  ],
67
69
  extras_require={
68
70
  "dev": [
@@ -12,4 +12,5 @@ def test_import():
12
12
  PatchContextDataset,
13
13
  Downloader,
14
14
  SheetDownloader,
15
- )
15
+ # IIIFDownloader,
16
+ )