mapreader 1.6.3.post0.dev18__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.
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/PKG-INFO +2 -1
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/__init__.py +6 -1
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/_version.py +3 -3
- mapreader-1.7.0.post0.dev1/mapreader/download/iiif_downloader.py +402 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/PKG-INFO +2 -1
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/SOURCES.txt +1 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/requires.txt +1 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/setup.py +2 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/tests/test_import.py +2 -1
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/LICENSE +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/README.md +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/__main__.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/annotate/__init__.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/annotate/annotator.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/classify/__init__.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/classify/classifier.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/classify/custom_models.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/classify/datasets.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/classify/load_annotations.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/__init__.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/data_structures.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/downloader.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/downloader_utils.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/sheet_downloader.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/tile_loading.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/tile_merging.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/load/__init__.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/load/geo_utils.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/load/images.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/load/loader.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/process/__init__.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/process/context_post_process.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/process/occlusion_analysis.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/process/process.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/__init__.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/dataclasses.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/deepsolo_runner.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/dptext_detr_runner.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/maptext_runner.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/runner_base.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/utils/__init__.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/utils/compute_and_save_stats.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/utils/load_frames.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/utils/slice_parallel.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/dependency_links.txt +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/entry_points.txt +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/not-zip-safe +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/top_level.txt +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/setup.cfg +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/tests/test_geo_pipeline.py +0 -0
- {mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/tests/test_utils_load_frames.py +0 -0
- {mapreader-1.6.3.post0.dev18 → 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.
|
|
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-
|
|
11
|
+
"date": "2025-04-10T09:06:03+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "1.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/classify/custom_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/classify/load_annotations.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/data_structures.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/downloader_utils.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/sheet_downloader.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/tile_loading.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/download/tile_merging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/process/context_post_process.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/process/occlusion_analysis.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/dataclasses.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/deepsolo_runner.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/dptext_detr_runner.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/maptext_runner.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/spot_text/runner_base.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/utils/compute_and_save_stats.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader/utils/slice_parallel.py
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{mapreader-1.6.3.post0.dev18 → mapreader-1.7.0.post0.dev1}/mapreader.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|